Database / Mnesia basics Interview questions
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.
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.
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_copies | disc_copies | disc_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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
| set | ordered_set | bag |
| 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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 table | Mnesia 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.
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.
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.
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_copies | disc_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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
