Databases · lesson 2 of 6
Intermediate
2Advanced
4Also part of Databases
12- + Schema Design from SQL
- + Indexing Strategy: When an Index Actually Helps from PostgreSQL
- + Composite Indexes: The Filesort a Single-Column Index Can't Avoid from MySQL
- + Clustered vs Nonclustered: When an Index Makes a Query Slower from SQL Server
- + A Real Deadlock, and Why Lock Order Fixed It from PostgreSQL
- + Isolation Levels: The Same Transaction Reading Two Different Values from MySQL
- + A Real Dirty Read: Seeing a Value That Never Actually Existed from SQL Server
- + Replication: A Second Server, and the read_only Gap Nobody Mentions from MySQL
- + Killing the Primary and Measuring the Real Failover Time from MongoDB
- + Point-in-Time Recovery: Undoing a Bad DELETE That Already Committed from PostgreSQL
- + Backup and Recovery: What a Restore Actually Gives Back from MySQL
- + Quorum Consistency: When a Read Is Guaranteed to See the Latest Write from System Design
Transactions Across Engines: Who Can Actually Roll Back
PostgreSQL and MongoDB both undo a partially-applied multi-step change when one step fails. Redis's MULTI/EXEC does not — a real queued transaction that keeps its successful command and drops only the failed one.
By SkillByExample · Updated
Two of these three engines undo a partially-applied transaction when one step fails. The third one — despite calling the feature “transactions” too — does not, and the difference only shows up when something inside the transaction actually goes wrong.
PostgreSQL: a duplicate key aborts the whole transaction
psql -U postgres -c "SELECT * FROM accounts;"
id | balance
----+---------
1 | 100
2 | 50
(2 rows)
psql -U postgres -c "BEGIN; UPDATE accounts SET balance = balance - 30 WHERE id = 1; INSERT INTO accounts VALUES (2, 999); ROLLBACK;"
BEGIN
UPDATE 1
ERROR: duplicate key value violates unique constraint "accounts_pkey"
DETAIL: Key (id)=(2) already exists.
The UPDATE succeeded — UPDATE 1 confirms one row changed. The INSERT then failed on a
real primary-key collision (id=2 already exists). Checking the table afterward:
psql -U postgres -c "SELECT * FROM accounts;"
id | balance
----+---------
1 | 100
2 | 50
(2 rows)
Still 100. The successful UPDATE was undone along with the failed INSERT, because
Postgres marks the entire transaction as aborted the moment any statement inside it fails —
“all or nothing” means the whole block, not each statement judged on its own.
MongoDB: a multi-document transaction, aborted the same way
const session = db.getMongo().startSession();
const coll = session.getDatabase("test").accounts;
session.startTransaction();
try {
coll.updateOne({_id: 1}, {$inc: {balance: -30}});
coll.insertOne({_id: 2, balance: 999}); // duplicate key, will fail
session.commitTransaction();
} catch (e) {
session.abortTransaction();
print("aborted: " + e.message);
}
session.endSession();
printjson(db.getSiblingDB("test").accounts.find().toArray());
aborted: E11000 duplicate key error collection: test.accounts index: _id_ dup key: { _id: 2 }
[
{ _id: 1, balance: 100 },
{ _id: 2, balance: 50 }
]
Same result as Postgres: _id: 1’s balance is still 100, not 70 — the updateOne that
ran first inside the transaction was undone along with the failed insertOne. MongoDB’s
multi-document transactions (available since 4.0 on replica sets) give the same all-or-nothing
guarantee as a relational database, just applied to documents instead of rows.
The bug: Redis’s MULTI/EXEC does not roll back
redis-cli SET counter 100
redis-cli SET name "Dune"
redis-cli
> MULTI
> DECRBY counter 30
> INCR name
> EXEC
OK
QUEUED
QUEUED
70
ERR value is not an integer or out of range
DECRBY counter 30 returned 70 — it actually ran and succeeded. INCR name failed, because
"Dune" isn’t numeric. Checking both keys afterward:
redis-cli GET counter
redis-cli GET name
70
Dune
counter is permanently 70. The failed command didn’t roll anything back — MULTI/EXEC
guarantees the queued commands run back-to-back with nothing else interleaved between them,
but it does not guarantee that a failure in one command undoes another. This is
documented Redis behavior, not a bug: catching a type mismatch before queuing is the client’s
job, because EXEC treats each queued command as independent once it starts running them.
Practice
1. Reset the Redis keys and confirm queuing a genuinely malformed command (not just a type mismatch) behaves differently.
redis-cli SET counter 100
redis-cli
> MULTI
> DECRBY counter 30
> NOTACOMMAND
> EXEC
OK
QUEUED
ERR unknown command 'NOTACOMMAND'
EXECABORT Transaction discarded because of previous errors.
This is the one case Redis does abort entirely — a command that doesn’t exist at all fails
at queue time, not at execution time, and EXEC refuses to run any of the queued commands
once that happens. The distinction is queue-time errors (unknown command, wrong number of
arguments) vs. run-time errors (right command, wrong data) — only the first kind aborts the
whole transaction.
2. Confirm Postgres does NOT roll back if you never call ROLLBACK or hit an error — a normal successful transaction commits everything.
psql -U postgres -c "BEGIN; UPDATE accounts SET balance = balance + 10 WHERE id = 1; COMMIT;"
psql -U postgres -c "SELECT * FROM accounts WHERE id = 1;"
BEGIN
UPDATE 1
COMMIT
id | balance
----+---------
1 | 110
(1 row)
No error, no rollback — a transaction that completes without failing commits normally.
Rollback only happens on an actual error or an explicit ROLLBACK, in both Postgres and
MongoDB alike.
3. Check whether MongoDB's transaction still aborts if the *first* operation is the one that fails, instead of the second.
session.startTransaction();
try {
coll.insertOne({_id: 2, balance: 999}); // fails first this time
coll.updateOne({_id: 1}, {$inc: {balance: -30}});
session.commitTransaction();
} catch (e) {
session.abortTransaction();
print("aborted: " + e.message);
}
aborted: E11000 duplicate key error collection: test.accounts index: _id_ dup key: { _id: 2 }
Same abort, regardless of which operation inside the transaction fails first — the guarantee is about the whole transaction block, not about ordering.
Next: indexing across engines — the same B-tree idea, five very different real costs.