Prev Next

Database / Mnesia basics Interview questions

1. What is Mnesia? 2. What is a Mnesia table? 3. What are the storage types available for a Mnesia table? 4. What is a Mnesia schema? 5. How do you create a Mnesia schema? 6. How do you create a Mnesia table? 7. What is a Mnesia transaction? 8. How do you write a record to a Mnesia table? 9. How do you read a record from a Mnesia table? 10. What are dirty operations in Mnesia? 11. What is mnesia:dirty_read/1? 12. What is a Mnesia record? 13. How do you delete a record from a Mnesia table? 14. What is the purpose of mnesia:start/0? 15. How do you stop Mnesia? 16. What are the Mnesia table types (set, ordered_set, bag)? 17. What is a Mnesia index? 18. How do you add a secondary index to a Mnesia table? 19. What is mnesia:info/0 used for? 20. How do you check if Mnesia is running? 21. What is the Mnesia directory (Mnesia.dir)? 22. What is the primary key in a Mnesia table? 23. What is mnesia:select/2 used for? 24. What is record_info used for when creating a Mnesia table? 25. What are the attributes of a Mnesia table? 26. What is a Mnesia node? 27. How do you replicate a table across multiple Mnesia nodes? 28. What is mnesia:table_info/2 used for? 29. Describe the difference between a Mnesia table and an ETS table? 30. Why should Mnesia operations generally run inside a transaction? 31. When should you use dirty operations instead of a transaction? 32. What is the difference between ram_copies and disc_copies? 33. What is disc_only_copies and when would you use it? 34. What happens to a disc_copies table when its node restarts? 35. How do you change a table's storage type at runtime? 36. What is a Mnesia table lock and how does it work? 37. How does Mnesia keep replicated tables consistent across nodes? 38. How do you migrate a Mnesia record definition when its fields change? 39. What is mnesia:transform_table/3 used for? 40. When should you choose bag over set for a Mnesia table? 41. How do you back up a Mnesia database? 42. How do you restore a Mnesia database from a backup? 43. Explain the lifecycle of a Mnesia transaction from start to commit? 44. How do you troubleshoot a Mnesia table stuck in the "loading" state? 45. What is mnesia:force_load_table/1 and when should you use it? 46. What is the difference between mnesia:read/1 and mnesia:wread/1? 47. What is a Mnesia checkpoint used for? 48. How does Mnesia resolve conflicts after a network partition heals?
Could not find what you were looking for? send us the question and we would be happy to answer your question.

1. What is Mnesia?

Mnesia is Erlang/OTP's built-in distributed database management system, designed to run inside the same BEAM nodes as your application rather than as a separate external service. It stores ordinary Erlang terms directly — tuples and records — so there's no object-relational mapping layer between your code and the data.

It ships with the OTP standard library, meaning any Erlang system can use it without adding an external dependency. Its core selling points are:

  • Tables can be replicated across multiple nodes in a cluster
  • Reads and writes can be wrapped in ACID-style transactions
  • Data can live purely in memory, on disk, or both
  • Schema and data changes can happen while the system keeps running

It's commonly used for cluster-local configuration, session state, and routing data — anywhere you want a shared, consistent, in-cluster store without standing up a separate database.

What is Mnesia, at its core?
Where does Mnesia typically run relative to an Erlang application?

2. What is a Mnesia table?

A Mnesia table is a named collection of records, conceptually similar to a database table but storing ordinary Erlang tuples rather than rows in a relational sense. Each table is backed by an ETS (or disc-based DETS) table under the hood, with Mnesia layering transactions, replication, and schema management on top.

mnesia:create_table(person,
    [{attributes, [id, name, age]}]).

Every table has a name, a list of attribute (field) names, and a storage type describing where its data lives (in memory, on disk, or both). Records inserted into the table must match the declared attribute shape, and the first attribute listed is always treated as the table's primary key.

What does a Mnesia table use under the hood to actually store its rows?
Which attribute of a Mnesia table is treated as its primary key?

3. What are the storage types available for a Mnesia table?

Mnesia supports three storage types per table, letting you trade off speed against durability on a per-table basis:

ram_copiesdisc_copiesdisc_only_copies
In memory only; fastest, lost on node restart. In memory AND on disk; fast reads, durable across restarts. On disk only; slower, used when the dataset is too large to fit comfortably in RAM.
mnesia:create_table(session,
    [{ram_copies, [node()]},
     {attributes, [id, user, expiry]}]).

A table can even use different storage types on different nodes — for example, keeping disc_copies on a couple of durable nodes while other nodes hold only ram_copies for fast local reads, all replicating the same logical table.

Which Mnesia storage type keeps data purely in memory with no disk persistence?
When would disc_only_copies be the preferred storage type?

4. What is a Mnesia schema?

The schema is Mnesia's own metadata table — it tracks which tables exist, their storage types, which nodes hold a copy of each, and other structural information about the database itself, separate from the actual row data inside your tables.

mnesia:create_schema([node()]).
mnesia:start().

mnesia:create_schema/1 must be run once, before Mnesia starts, to initialize this metadata on disk for the given list of nodes. Every table you later create with mnesia:create_table/2 gets registered into this schema, which is itself replicated to every node listed when the schema was created, so each participating node has a consistent view of what tables exist and where their copies live.

What does the Mnesia schema actually store?
When must mnesia:create_schema/1 be run relative to mnesia:start/0?

5. How do you create a Mnesia schema?

Creating a schema is a one-time setup step, typically run from an Erlang shell before your application starts Mnesia for the first time, specifying which nodes will participate in the database.

1> mnesia:create_schema([node()]).
ok
2> mnesia:start().
ok

mnesia:create_schema/1 takes a list of node names and writes the initial schema metadata to disk on each of them — it will fail if Mnesia is already running on any of those nodes, since the schema has to be laid down before the system is live. For a multi-node cluster, you'd list every participating node in that same call so they all start out with a consistent, shared schema from the beginning.

What must be true before you can successfully call mnesia:create_schema/1?
What does mnesia:create_schema([node()]) set up?

6. How do you create a Mnesia table?

mnesia:create_table/2 takes a table name and a list of options describing its attributes, storage type, and table type, registering it in the schema and allocating the underlying storage.

-record(person, {id, name, age}).

mnesia:create_table(person,
    [{attributes, record_info(fields, person)},
     {disc_copies, [node()]},
     {type, set}]).

The attributes option is almost always built from record_info(fields, RecordName) so the table's field order automatically matches an existing -record definition rather than being retyped by hand. Other common options include type (set, ordered_set, or bag) and the storage type option matching where you want copies to live.

What is the typical source of the `attributes` list when creating a Mnesia table?
What does mnesia:create_table/2 do besides allocating storage?

7. What is a Mnesia transaction?

A Mnesia transaction wraps a set of reads and writes so they either all succeed together or none of them take effect — the same all-or-nothing guarantee a traditional database transaction provides, implemented here across Erlang processes and, if the table is replicated, across nodes.

mnesia:transaction(fun() ->
    mnesia:write(#person{id = 1, name = "Ada", age = 34}),
    mnesia:write(#person{id = 2, name = "Grace", age = 41})
end).

The function passed to mnesia:transaction/1 runs with automatic locking and rollback: if any operation inside it fails (a conflict, an exception), Mnesia aborts the whole transaction and none of the writes are applied. This is the safe, default way to interact with Mnesia data, and is what you should reach for unless you have a specific reason to use faster, less safe "dirty" operations instead.

What guarantee does wrapping operations in mnesia:transaction/1 provide?
What happens if an operation inside a Mnesia transaction fails?

8. How do you write a record to a Mnesia table?

mnesia:write/1 inserts or updates a record inside a transaction — if a record with the same primary key already exists, it's overwritten; otherwise a new one is added.

-record(person, {id, name, age}).

mnesia:transaction(fun() ->
    mnesia:write(#person{id = 1, name = "Ada", age = 34})
end).

Because mnesia:write/1 must run inside a transaction context (typically via mnesia:transaction/1 or mnesia:sync_transaction/1), Mnesia can guarantee the write is applied consistently across every replicated copy of the table, or not at all if the transaction aborts. Trying to call mnesia:write/1 outside of any transaction context raises an error, since there's no transaction for it to participate in.

What happens when mnesia:write/1 is called with a record whose key already exists?
What is required in order to call mnesia:write/1 successfully?

9. How do you read a record from a Mnesia table?

mnesia:read/1 (or the 2-arity form naming the table explicitly) fetches all records matching a given primary key, returning a list — empty if nothing matches, one element for a set table, or possibly several for a bag table where duplicate keys are allowed.

mnesia:transaction(fun() ->
    mnesia:read({person, 1})
end).
%% -> {atomic, [#person{id = 1, name = "Ada", age = 34}]}

Like write/1, read/1 must run inside a transaction so Mnesia can apply proper locking — a read inside a transaction takes a read lock, ensuring the data can't change underneath it before the transaction completes. The result of the whole transaction function is wrapped as {atomic, Result} on success, or {aborted, Reason} if it failed.

What does mnesia:read({Table, Key}) return for a `set` table when the key exists?
What does a successful mnesia:transaction/1 call return?

10. What are dirty operations in Mnesia?

Dirty operations (mnesia:dirty_read/1, mnesia:dirty_write/1, mnesia:dirty_delete/1, and similar) perform a single read or write directly against the table, bypassing transaction locking and the coordination needed to keep replicated copies perfectly in sync during the operation.

mnesia:dirty_write(#person{id = 1, name = "Ada", age = 34}),
mnesia:dirty_read({person, 1}).

They're significantly faster than their transactional counterparts precisely because they skip that coordination, which makes them a reasonable choice for high-throughput, low-consistency-requirement workloads (a hit counter, a cache-like table) where an occasional inconsistency during a concurrent update or a node failure mid-write is tolerable. They're the wrong choice whenever an operation depends on multiple related writes succeeding together, since dirty operations offer no atomicity across more than one call.

What do dirty operations sacrifice in exchange for speed?
When are dirty operations a poor choice?

11. What is mnesia:dirty_read/1?

mnesia:dirty_read({Table, Key}) fetches matching records directly, without opening a transaction or taking any lock — it's the fastest way to read a single record when you don't need the consistency guarantees a transaction provides.

mnesia:dirty_read({person, 1}).
%% -> [#person{id = 1, name = "Ada", age = 34}]

Unlike mnesia:read/1, it can be called directly from anywhere in your code without wrapping it in mnesia:transaction/1 first, and it returns the raw list of matching records rather than an {atomic, ...}-wrapped tuple. The tradeoff is that it can read a value mid-update on a replicated table if another node is writing at the same instant, since there's no lock coordinating the two.

Do you need to wrap mnesia:dirty_read/1 in a transaction?
What risk does dirty_read/1 carry that a transactional read avoids?

12. What is a Mnesia record?

A Mnesia record is simply an ordinary Erlang -record, whose field names match a table's declared attributes — Mnesia doesn't introduce a separate record concept of its own, it reuses the language's existing record syntax as the shape of each row.

-record(person, {id, name, age}).

Row = #person{id = 1, name = "Ada", age = 34}.

When you call mnesia:write/1 with a #person{} record, Mnesia inspects the record's tag (person) to know which table it belongs to, and uses the first field (id) as the primary key by convention. Because it's just a tuple under the hood, you can pattern match on fields the same way you would with any other Erlang record.

How does Mnesia know which table a #person{} record belongs to?
What determines the primary key of a record inserted into a Mnesia table?

13. How do you delete a record from a Mnesia table?

mnesia:delete/1 removes the record matching a given key, run inside a transaction just like write/1 and read/1, so the deletion participates in the same atomicity and locking guarantees.

mnesia:transaction(fun() ->
    mnesia:delete({person, 1})
end).

There's also mnesia:delete_object/1, which deletes a specific record by matching its full content rather than just its key — useful on bag tables where multiple records can share the same key and you only want to remove one particular one. A fast, non-transactional mnesia:dirty_delete/1 is also available, following the same speed-versus-consistency tradeoff as other dirty operations.

What does mnesia:delete({Table, Key}) remove?
When is mnesia:delete_object/1 preferred over mnesia:delete/1?

14. What is the purpose of mnesia:start/0?

mnesia:start/0 boots the Mnesia application on the current node: it loads the schema created earlier by mnesia:create_schema/1, opens any disk-based tables, and begins participating in replication with any other already-running nodes that share tables with this one.

mnesia:start().
%% ok

It's asynchronous with respect to fully loading every table — tables with disk copies may still be loading in the background right after start/0 returns ok. Code that needs to be sure specific tables are ready before proceeding typically follows it with mnesia:wait_for_tables(TableList, Timeout) to block until those tables have finished loading.

What does mnesia:start/0 do on a node?
How do you ensure specific tables are fully loaded before using them after start/0?

15. How do you stop Mnesia?

mnesia:stop/0 shuts down the Mnesia application on the current node cleanly, flushing any pending disk writes and detaching from replication with other nodes, without affecting Mnesia's state on other nodes in the cluster.

mnesia:stop().
%% stopped

This is a graceful local shutdown, distinct from a node crashing outright: other nodes holding replicated copies of the same tables notice this node went down and can continue operating on their own copies, while this node's disk-based tables remain intact on disk for the next mnesia:start/0. It's typically called as part of an orderly application or node shutdown sequence rather than something you'd invoke mid-operation on a live production node without reason.

What does mnesia:stop/0 do to disk-based table data on that node?
How does stopping Mnesia on one node affect other nodes holding replicated copies?

16. What are the Mnesia table types (set, ordered_set, bag)?

The type option on mnesia:create_table/2 controls how a table handles keys and ordering, mirroring the same distinction ETS makes.

setordered_setbag
Unique keys; one record per key. Default type. Unique keys, kept in a sorted order for efficient range-style traversal. Keys may repeat; multiple distinct records can share the same key.
mnesia:create_table(tag,
    [{attributes, [item_id, tag_name]},
     {type, bag}]).   %% one item_id can have many tag_name entries

set is the right default for most tables where each key genuinely identifies one row. bag fits naturally modeled one-to-many relationships (an item with multiple tags) without a separate join table. ordered_set is chosen specifically when you need to iterate keys in sorted order efficiently.

Which Mnesia table type allows multiple distinct records to share the same key?
What is the default table type if you don't specify one?

17. What is a Mnesia index?

By default, only a table's primary key can be looked up efficiently; searching by any other field means scanning every record. A Mnesia index adds a secondary lookup structure on a non-key attribute so queries on that field can be answered directly, without a full table scan.

mnesia:add_table_index(person, name),

mnesia:transaction(fun() ->
    mnesia:index_read(person, "Ada", name)
end).

Once an index exists on the name field, mnesia:index_read/3 can fetch matching records directly through that index rather than iterating the whole table checking each one's name field by hand. Indexes add some write overhead (the index structure must be maintained alongside every insert/update) in exchange for much faster reads on that field.

What problem does adding a table index solve?
What is the tradeoff of adding an index to a table?

18. How do you add a secondary index to a Mnesia table?

mnesia:add_table_index/2 adds an index on a given attribute of an already-existing table, which can be done at runtime, even while the system is live and serving traffic.

mnesia:add_table_index(person, age).

Once added, queries using mnesia:index_read(Table, Value, Attribute) can look up records by that attribute's value directly. Building the index does require scanning the existing table once to populate it, so adding an index on a very large, already-populated table is a heavier one-time operation than adding it to an empty table up front as part of initial schema design.

Can you add a table index to an already-populated Mnesia table at runtime?
What one-time cost does adding an index to a large existing table incur?

19. What is mnesia:info/0 used for?

mnesia:info/0 prints a human-readable summary of the running Mnesia system directly to the shell: which tables exist, their sizes, storage types, which nodes hold copies, and general system status — a quick way to sanity-check the state of the database without writing any query code.

1> mnesia:info().
---> Processes holding locks <---
---> Processes waiting for locks <---
---> Participant transactions <---
...
Table Type Size ...

It's mainly a diagnostic/debugging tool used interactively in a shell session, not something application code calls programmatically — for structured, code-level introspection of a specific table, you'd use mnesia:table_info/2 instead, which returns a specific piece of information you can pattern match on or act on programmatically.

What kind of tool is mnesia:info/0 best described as?
What would you use instead of info/0 to get a specific piece of table data programmatically?

20. How do you check if Mnesia is running?

The most direct check is mnesia:system_info(is_running), which returns an atom describing the current state rather than a plain boolean, since Mnesia can be in intermediate states like starting up or stopping rather than strictly on or off.

mnesia:system_info(is_running).
%% -> yes | no | starting | stopping

Checking for yes specifically (rather than just "not no") is important in startup code that needs to know Mnesia is fully operational before proceeding — a value of starting means the application is initializing but not yet ready to serve reads/writes reliably.

What does mnesia:system_info(is_running) return?
Why does startup code typically check specifically for the atom `yes`?

21. What is the Mnesia directory (Mnesia.dir)?

The Mnesia directory is the filesystem location where a node stores all of its disk-based Mnesia data — the schema file, disc_copies table files, transaction logs, and backups. Its default name follows the pattern Mnesia.NodeName, but it's fully configurable.

%% in sys.config or via command line
{mnesia, [{dir, "/var/data/mnesia/mynode"}]}.

Every node running Mnesia with any disk-based storage type needs its own directory, and that directory is what makes a disc_copies table's data durable across a node restart — the schema and table data are read back from this directory when mnesia:start/0 runs again. It should live somewhere with reliable, persistent storage in a production deployment, since losing this directory on a disc_copies-only node means losing that node's durable copy of the data.

What does the Mnesia directory store?
Why does losing the Mnesia directory matter for a disc_copies-only node?

22. What is the primary key in a Mnesia table?

The primary key of a Mnesia table is always its first declared attribute — there's no separate syntax to mark a different field as the key the way some databases let you designate an arbitrary column. Every lookup by mnesia:read/1 or mnesia:dirty_read/1 is keyed on this first field.

-record(person, {id, name, age}).   %% id is the primary key

mnesia:read({person, 1}).            %% looks up by id = 1

On a set or ordered_set table, this key must be unique — writing a new record with an existing key's value overwrites the old one. On a bag table, the same key value can appear across multiple distinct records, since uniqueness isn't enforced for that table type.

How is a Mnesia table's primary key determined?
On a set table, what happens if you write a record whose key matches an existing one?

23. What is mnesia:select/2 used for?

mnesia:select/2 queries a table using a match specification, letting you filter on conditions beyond a simple primary-key lookup — the same match-spec mechanism ETS uses, run inside a Mnesia transaction for consistency.

mnesia:transaction(fun() ->
    mnesia:select(person,
        [{#person{age = '$1', _ = '_'}, [{'>', '$1', 30}], ['$_']}])
end).
%% returns every person record with age > 30

Where mnesia:read/1 only works by primary key, select/2 can filter on any field, including combinations of conditions, at the cost of scanning more of the table (unless an index exists on the filtered field). Like other match-spec code, it's often written using ets:fun2ms/1-style helpers to avoid hand-crafting the raw match specification tuple.

What can mnesia:select/2 filter on that mnesia:read/1 cannot?
What underlying mechanism does mnesia:select/2 use to filter data?

24. What is record_info used for when creating a Mnesia table?

record_info(fields, RecordName) is a compiler built-in that expands, at compile time, to the literal list of field names declared in a -record definition — it's the standard way to feed a table's attributes option without retyping the field list by hand.

-record(person, {id, name, age}).

mnesia:create_table(person,
    [{attributes, record_info(fields, person)}]).
%% expands to: {attributes, [id, name, age]}

Using record_info/2 instead of a hardcoded list keeps the table definition and the record definition from silently drifting apart — if you add a field to the -record, the attributes list picks it up automatically the next time the module compiles, rather than requiring you to remember to update two separate places by hand.

What does record_info(fields, person) expand to?
Why is record_info/2 preferred over hardcoding the attributes list?

25. What are the attributes of a Mnesia table?

A table's attributes are simply the ordered field names describing the shape of each record it stores — equivalent to column names in a relational table, though here they correspond directly to an Erlang record's fields.

-record(person, {id, name, age}).
%% attributes: id, name, age

The attributes option passed to mnesia:create_table/2 is what declares this list, almost always sourced from record_info(fields, RecordName). The first attribute in the list is always the primary key; every other attribute is just a regular field you can read, write, and (if indexed) query on. Every record written into the table must have exactly this shape — the same tag and field count as the record definition the table was created from.

What do a Mnesia table's attributes correspond to?
Which attribute in the list serves as the table's key?

26. What is a Mnesia node?

In Mnesia's context, a "node" is simply an Erlang node (a running BEAM instance with a name) that participates in a Mnesia database, either holding copies of tables directly or just being schema-aware of the cluster. A single logical Mnesia database can span several such nodes, all coordinating table replication and transactions between them.

mnesia:create_schema([nodea@host1, nodeb@host2]),
mnesia:create_table(person,
    [{disc_copies, [nodea@host1, nodeb@host2]},
     {attributes, [id, name, age]}]).

Which nodes hold a copy of a given table is decided per-table via the storage type options (ram_copies, disc_copies, disc_only_copies), and a node doesn't need to hold every table in the schema — you can have some nodes replicate only a subset of tables relevant to their role, while still being part of the same overall Mnesia cluster.

What is a 'node' in the context of Mnesia?
Does every node in a Mnesia cluster need to hold a copy of every table?

27. How do you replicate a table across multiple Mnesia nodes?

Replication is set up simply by listing more than one node in a table's storage type option when creating (or later reconfiguring) the table — Mnesia then keeps all listed nodes' copies in sync automatically as part of every transaction.

mnesia:create_table(session,
    [{disc_copies, [nodea@host1, nodeb@host2, nodec@host3]},
     {attributes, [id, user, expiry]}]).

Every write inside a transaction is propagated to all listed nodes as part of that same transaction's commit, using two-phase commit coordination so all replicas agree before the write is considered durable. To add a new replica to an already-running table, mnesia:add_table_copy/3 lets you extend replication to another node without recreating the table from scratch, copying the existing data to the new node as part of that operation.

How do you specify which nodes should hold a copy of a table?
How do you add a new node as a replica to an already-existing table?

28. What is mnesia:table_info/2 used for?

mnesia:table_info(Table, Item) returns a specific, structured piece of metadata about a table — its size, storage type, attribute list, index list, and more — suitable for use in application code, unlike the human-readable printout from mnesia:info/0.

mnesia:table_info(person, size).           %% -> 42
mnesia:table_info(person, disc_copies).    %% -> [node1@host, node2@host]
mnesia:table_info(person, attributes).     %% -> [id, name, age]

This is the function you'd reach for programmatically — for example, checking a table's current size before deciding whether to run a bulk migration, or verifying which nodes currently hold a copy before performing a maintenance operation — rather than parsing the free-form text that mnesia:info/0 prints for humans in a shell.

What does mnesia:table_info/2 return, compared to mnesia:info/0?
Which is a valid example of an Item argument to table_info/2?

29. Describe the difference between a Mnesia table and an ETS table?

Mnesia tables are actually built on top of ETS (or DETS for disk-only storage), so they share the same underlying storage engine — the difference is entirely in what Mnesia layers on top.

ETS tableMnesia table
Node-local only; no built-in replication.Can be replicated across multiple nodes.
No built-in transactions; operations are inherently "dirty." Supports ACID-style transactions across multiple tables and nodes.
In-memory only.Can be in-memory, on-disk, or both, per table.
Lower overhead per operation.Extra overhead for transaction coordination and replication.

In short: ETS gives you raw, fast, local storage; Mnesia adds the database-like guarantees (transactions, replication, durability) on top, at the cost of some overhead you don't pay when using ETS directly.

What does Mnesia use under the hood for its actual storage engine?
What does Mnesia add on top of plain ETS?

30. Why should Mnesia operations generally run inside a transaction?

Wrapping reads and writes in mnesia:transaction/1 gives you the same guarantee a relational database transaction gives you: either every operation in the block succeeds, or none of them take effect, even if the table is replicated across several nodes. Without that, a partial failure mid-sequence (a crash, a conflict with another writer) could leave related data in an inconsistent state — one write applied, another not.

Transactions also handle locking automatically: reads take read locks and writes take write locks as needed, so concurrent transactions touching overlapping data are serialized safely rather than racing each other. This is exactly why the transactional API is the default recommendation, reserving dirty operations for the narrower case where speed matters more than these guarantees and the operation genuinely stands alone.

What could go wrong without wrapping related writes in a transaction?
What does a transaction handle automatically that dirty operations don't?

31. When should you use dirty operations instead of a transaction?

Dirty operations fit specifically when an operation genuinely stands alone — a single read or write that doesn't depend on any other operation succeeding alongside it, and where occasional inconsistency during a race or a node failure mid-write is acceptable for that particular piece of data.

Good candidates: a hit counter, a best-effort cache entry, telemetry data, or high-frequency writes to a table where losing or slightly misordering an entry under heavy concurrent load isn't a correctness problem. Poor candidates: anything involving multiple related writes (transferring a value between two records), anything where a stale read could cause a real bug (checking a balance before debiting it), or any table where replication consistency genuinely matters to correctness.

The deciding question is usually: "if this specific operation raced with another one, or half-completed on a crash, would that actually break something a user or another part of the system depends on?" If yes, use a transaction; if a shrug is the honest answer, dirty operations are a reasonable, faster choice.

What kind of operation is a good candidate for dirty operations?
What question helps decide between dirty ops and a transaction?

32. What is the difference between ram_copies and disc_copies?

Both storage types keep an active, fast in-memory copy of the table data for reads and writes during normal operation — the difference is entirely about what happens to that data when the node restarts.

ram_copiesdisc_copies
In memory only.In memory AND mirrored to disk.
Data is lost if the node restarts (unless another node still has a copy). Data survives a node restart, reloaded from disk at startup.
Slightly less write overhead (no disk I/O per write). Small extra write overhead to persist changes to disk.

Read speed is essentially the same for both, since both serve reads from the in-memory copy — disc_copies only adds overhead on the write path, in exchange for durability across restarts.

What happens to a ram_copies-only table's data when its node restarts?
How do read speeds compare between ram_copies and disc_copies tables?

33. What is disc_only_copies and when would you use it?

disc_only_copies keeps a table's data purely on disk (backed by DETS), with no full in-memory copy the way ram_copies and disc_copies maintain — every read and write goes through disk I/O rather than being served from RAM.

mnesia:create_table(archive_log,
    [{disc_only_copies, [node()]},
     {attributes, [id, timestamp, payload]}]).

It's the right choice specifically when a table's data is too large to comfortably fit in the available RAM across your nodes, and you're willing to accept slower access in exchange for not needing that much memory. For most tables where the dataset is a reasonable size, disc_copies (memory-backed with disk durability) gives better performance with the same durability guarantee, making disc_only_copies a deliberate, size-driven tradeoff rather than a general-purpose default.

What is stored in memory for a disc_only_copies table during normal operation?
When is disc_only_copies the right choice over disc_copies?

34. What happens to a disc_copies table when its node restarts?

On restart, Mnesia reads the table's data back from the disk files in the Mnesia directory before the node rejoins the cluster and is considered ready — the in-memory copy is rebuilt from what was durably written to disk, plus replaying any transaction log entries recorded since the last full checkpoint.

sequenceDiagram
    participant Node
    participant Disk
    Node->>Disk: read schema + table files
    Disk-->>Node: table data loaded into memory
    Node->>Node: replay transaction log since last checkpoint
    Node-->>Node: table marked as loaded, ready for use

If this is the only node holding a copy of that table, code trying to use it must wait (typically via mnesia:wait_for_tables/2) until loading finishes. If other nodes in the cluster already have the table loaded and running, the restarting node can instead copy the current state from one of them, which is often faster than reading its own potentially-stale disk copy from scratch.

Where does a disc_copies table's data come from when its node restarts?
What can a restarting node do if other nodes already have the table loaded?

35. How do you change a table's storage type at runtime?

mnesia:change_table_copy_type/3 converts a table's storage type on a given node while the system stays live — for example, promoting a ram_copies table to disc_copies once you decide it needs durability, without dropping and recreating it.

mnesia:change_table_copy_type(person, node(), disc_copies).

This is a schema-level operation, so it's typically run once, deliberately, rather than as part of regular application logic. It doesn't move or duplicate data across nodes by itself — it changes how the copy on the specified node is persisted; adding an entirely new node as a replica is a separate operation handled by mnesia:add_table_copy/3.

What does mnesia:change_table_copy_type/3 let you do?
Is dropping and recreating the table required to change its storage type?

36. What is a Mnesia table lock and how does it work?

Inside a transaction, Mnesia automatically acquires locks on the specific records (or, in some cases, the whole table) a transaction touches, so concurrent transactions can't corrupt each other's view of the data. Reads take a read lock; writes take a write lock, which is exclusive and blocks other transactions from reading or writing that same record until the holding transaction commits or aborts.

sequenceDiagram
    participant T1
    participant T2
    participant Record
    T1->>Record: write lock acquired
    T2->>Record: attempts write, must wait
    T1->>Record: transaction commits, lock released
    Record-->>T2: lock granted, T2 proceeds

You don't request locks explicitly in ordinary code — mnesia:read/1 and mnesia:write/1 acquire the appropriate lock automatically as part of running inside a transaction. If two transactions would deadlock waiting on each other's locks, Mnesia detects it and aborts one of them, which the calling code should be prepared to retry.

What kind of lock does a write inside a Mnesia transaction take?
What happens if two Mnesia transactions deadlock waiting on each other's locks?

37. How does Mnesia keep replicated tables consistent across nodes?

Every write inside a transaction on a replicated table is propagated to all nodes holding a copy as part of that same transaction's commit, coordinated through a two-phase commit protocol: a coordinating node asks each replica to prepare the write, and only tells them all to actually commit once every replica has confirmed it can.

sequenceDiagram
    participant Coord
    participant NodeA
    participant NodeB
    Coord->>NodeA: prepare write
    Coord->>NodeB: prepare write
    NodeA-->>Coord: ready
    NodeB-->>Coord: ready
    Coord->>NodeA: commit
    Coord->>NodeB: commit

If any replica can't prepare (it's unreachable, or has a conflicting lock), the whole transaction aborts on every node rather than applying on some and not others — this is precisely what gives Mnesia its all-or-nothing consistency guarantee across a cluster rather than just within a single node.

What protocol coordinates a write across all replicas of a Mnesia table?
What happens if one replica can't prepare a replicated write?

38. How do you migrate a Mnesia record definition when its fields change?

Simply changing a -record definition in your code and recompiling doesn't retroactively update records already stored in a Mnesia table — existing rows keep whatever shape they were written with, which will mismatch the new record definition the next time your code tries to read them.

%% old: -record(person, {id, name, age}).
%% new: -record(person, {id, name, age, email}).

mnesia:transform_table(person,
    fun(P) -> P#person{email = undefined} end,
    record_info(fields, person)).

The standard fix is mnesia:transform_table/3: it walks every existing record in the table, applies a transformation function you provide to reshape each one into the new format, and updates the table's schema to match, all while the table can remain usable. This must be run once as a deliberate migration step whenever a record's field list changes, not something that happens automatically just from recompiling.

Does recompiling a changed -record definition automatically update existing Mnesia rows?
What function performs this kind of record shape migration on an existing table?

39. What is mnesia:transform_table/3 used for?

mnesia:transform_table(Table, Fun, NewAttributes) rewrites every record in a table according to a supplied function, and updates the table's declared attribute list to match the new shape — the primary tool for evolving a table's schema after data already exists in it.

mnesia:transform_table(person,
    fun({person, Id, Name, Age}) ->
        {person, Id, Name, Age, undefined}   %% add an email field, default undefined
    end,
    [id, name, age, email]).

The transform function receives each existing record and must return the new-shaped record; Mnesia applies this to every row in the table as part of the operation. It's typically run once during a planned schema upgrade (often alongside a release upgrade), rather than something you'd call routinely as part of everyday application logic.

What two things does mnesia:transform_table/3 update together?
When is transform_table/3 typically run?

40. When should you choose bag over set for a Mnesia table?

Choose bag when a single key naturally has multiple associated values that all need to coexist as separate records — a classic one-to-many relationship you'd otherwise model with a separate join table in a relational database.

mnesia:create_table(item_tag,
    [{attributes, [item_id, tag]},
     {type, bag}]),

mnesia:write({item_tag, 101, "sale"}),
mnesia:write({item_tag, 101, "clearance"}).  %% both coexist under item_id 101

set remains the right default whenever each key genuinely identifies exactly one record — most tables fall into this category. Reach for bag specifically when you catch yourself needing to store several distinct values under what is conceptually "the same key," rather than forcing that into a single record with a list-valued field, which would make querying and indexing individual associated values harder.

What relationship shape does a bag table naturally fit?
What should remain the default table type for most straightforward tables?

41. How do you back up a Mnesia database?

mnesia:backup/1 writes a point-in-time snapshot of the entire database (schema plus table data) to a file, which can later be restored on the same or a different set of nodes.

mnesia:backup("/var/backups/mnesia_20260101.bak").

For finer control — backing up only specific tables, or customizing how the backup interacts with a live, running system — the lower-level mnesia:activate_checkpoint/1 and mnesia:backup_checkpoint/2 functions let you take a consistent snapshot of chosen tables while the database keeps serving reads and writes, rather than a single all-or-nothing full backup call. Regularly scheduled backups are the standard safety net against data loss beyond what replication alone protects against (such as an operator error wiping data across every replica at once).

What does mnesia:backup/1 produce?
What lets you back up only specific tables while the system stays live?

42. How do you restore a Mnesia database from a backup?

mnesia:restore/2 loads data from a backup file back into a running (or freshly initialized) Mnesia instance, with options controlling exactly how the restored data merges with what's already there.

mnesia:restore("/var/backups/mnesia_20260101.bak",
    [{default_op, recreate_tables}]).

The options passed as the second argument matter a lot: recreate_tables drops and rebuilds each table fresh from the backup, while other option combinations can instead merge backup data into existing tables, or skip tables that already exist. Restoring is typically done onto a stopped or freshly started node as a deliberate recovery operation, and it's good practice to verify the restored table sizes and a few sample records afterward rather than assuming the restore silently succeeded.

What does the {default_op, recreate_tables} option do during a restore?
What is good practice immediately after running a restore?

43. Explain the lifecycle of a Mnesia transaction from start to commit?

Calling mnesia:transaction(Fun) kicks off a well-defined sequence: Mnesia runs your function, tracking every read and write it performs, acquiring the appropriate locks as it goes, before deciding whether the whole thing can be committed.

flowchart TD
    A[mnesia:transaction Fun called] --> B[Fun executes: reads take read locks, writes take write locks]
    B --> C{Fun completes without exception?}
    C -->|Yes| D[Two-phase commit across all replicas of touched tables]
    D --> E{All replicas prepared successfully?}
    E -->|Yes| F["{atomic, Result} returned; locks released"]
    E -->|No| G["{aborted, Reason} returned; no changes applied"]
    C -->|No, exception raised| G

If the function runs to completion without an exception, Mnesia attempts to commit — for a replicated table, this means the two-phase prepare/commit exchange across every node holding a copy. Only if every involved replica confirms it can commit does the transaction actually take effect everywhere, returning {atomic, Result}; any failure along the way aborts the whole thing, applying no changes and returning {aborted, Reason} instead.

What happens during a Mnesia transaction's function execution, before commit?
What does a Mnesia transaction return if it can't be committed on every replica?

44. How do you troubleshoot a Mnesia table stuck in the "loading" state?

A table stuck loading after a restart usually means Mnesia can't decide it has a safe, up-to-date copy to load from — commonly after an unclean shutdown, a network partition that separated replicas, or a node coming back up while unsure whether another replica holds newer data.

mnesia:table_info(person, where_to_read).   %% check which node it's trying to read from
mnesia:system_info(running_db_nodes).       %% confirm which nodes are actually connected

Practical steps: first confirm which other nodes are actually reachable and running (mnesia:system_info(running_db_nodes)), since the stuck node may simply be waiting on a replica that's down or unreachable. Check the logs for messages about inconsistent database state, which indicate Mnesia detected conflicting views of the data and is refusing to guess which copy is authoritative. If you can confirm which node's copy is actually the correct, most recent one, that's the point where mnesia:force_load_table/1 becomes the (last-resort) way to break the deadlock.

What commonly causes a table to get stuck in the loading state?
What should you check first when a table is stuck loading?

45. What is mnesia:force_load_table/1 and when should you use it?

mnesia:force_load_table/1 tells Mnesia to load a table from the local node's copy immediately, overriding its normal safety check that waits to confirm it has the most up-to-date, authoritative copy before proceeding. It's a deliberate override for a table that's stuck loading and blocking system startup.

mnesia:force_load_table(person).

This is explicitly a last-resort recovery tool, not a routine operation: forcing a load means accepting whatever data is on this node's disk as correct, even if another (currently unreachable) replica actually held more recent writes, which could mean silently losing those newer changes. It should only be used after you've confirmed, through investigation, that this node's copy really is the one you want treated as authoritative — not as a reflexive fix the moment a table appears stuck.

What safety check does mnesia:force_load_table/1 override?
Why is force_load_table/1 considered a last resort rather than routine?

46. What is the difference between mnesia:read/1 and mnesia:wread/1?

Both fetch a record by key inside a transaction, but they take different lock types up front. mnesia:read/1 takes a read lock, which is fine for a plain lookup, but if you intend to immediately write back an updated version of that same record, a read lock can force an extra lock upgrade step. mnesia:wread/1 takes a write lock right away instead.

mnesia:transaction(fun() ->
    [P] = mnesia:wread({person, 1}),
    mnesia:write(P#person{age = P#person.age + 1})
end).

Using wread/1 when you know you're about to write signals your intent up front, which can reduce the chance of a deadlock between two transactions that both read-then-write the same record concurrently — each avoids the read-lock-to-write-lock upgrade dance that could otherwise let two transactions each hold a read lock and wait on each other for the write lock.

What lock does mnesia:wread/1 take, compared to mnesia:read/1?
Why use wread/1 when you know you're about to update the record you're fetching?

47. What is a Mnesia checkpoint used for?

A checkpoint captures a consistent, point-in-time view of one or more tables while the system keeps running — it's the mechanism backups and certain replication operations use internally to get a stable snapshot without pausing the whole database.

{ok, Name, Nodes} = mnesia:activate_checkpoint(
    [{name, my_checkpoint}, {min, [person, session]}]),
mnesia:backup_checkpoint(Name, "/var/backups/partial.bak"),
mnesia:deactivate_checkpoint(Name).

mnesia:activate_checkpoint/1 retains the state of the specified tables as of that moment, even as new writes continue to happen afterward, so a subsequent backup_checkpoint/2 call reads a consistent snapshot rather than a moving target. Once the checkpoint is no longer needed, mnesia:deactivate_checkpoint/1 releases the extra bookkeeping Mnesia was doing to preserve that frozen view.

What does activating a checkpoint let you do?
What should you do once a checkpoint is no longer needed?

48. How does Mnesia resolve conflicts after a network partition heals?

When a network partition splits a cluster, each side can keep operating independently on its own copy of a replicated table, potentially applying different, conflicting writes to the same records on each side. When the partition heals and the nodes reconnect, Mnesia detects this as an inconsistent database state rather than silently merging the two histories.

flowchart TD
    A[Partition heals, nodes reconnect] --> B{Mnesia detects differing table state?}
    B -->|Yes| C[Reports inconsistent_database_event]
    C --> D[Application-supplied or default resolver decides which side wins]
    D --> E[Losing side's conflicting changes are discarded]

By default, Mnesia doesn't automatically pick a winner for you in a way that guarantees no data loss — it raises an inconsistent_database_event and expects either a configured mnesia_event handler or manual operator intervention (often via mnesia:force_load_table/1 on the side chosen as authoritative) to resolve it. This is precisely why systems that can't tolerate this ambiguity often add their own quorum or leader-election logic on top, rather than relying on Mnesia's partition handling alone.

What does Mnesia do by default when it detects conflicting table states after a partition heals?
Why do some systems add their own quorum/leader-election logic on top of Mnesia?
«
»

Comments & Discussions