skip to content

In SQL, what happens if you issue BEGIN while a transaction is already open?

level: middleimportance: nice to knowfreq 28%

answer

  1. standard SQL has no nesting here
  2. the second one does not stack
  3. one engine commits the first silently
  4. another just warns and carries on

basics

~20 s

Standard SQL has no nested transactions, so a second BEGIN never stacks. PostgreSQL warns and keeps the single existing transaction; MySQL implicitly commits the transaction in progress and starts a new one, silently making earlier work permanent.

solid answer

~50 s

There is no nesting in standard SQL: transactions do not stack, and the second `BEGIN` cannot create an inner one. What engines do instead diverges dangerously. **PostgreSQL** emits `WARNING: there is already a transaction in progress` and carries on with the one transaction you already had — harmless, and easy to miss in logs. **MySQL** treats `START TRANSACTION` or `BEGIN` as an implicit `COMMIT` of any transaction in progress, so the work you did before the second `BEGIN` is now permanent and a later `ROLLBACK` cannot reach it. That bites when two layers each think they own the transaction: a helper opens one, calls a service that opens another, and the outer unit of work quietly splits in two. The fix is to have one owner of the boundary — libraries join the caller's transaction rather than opening their own, and true nesting is emulated with savepoints.

code

sql · 6 lines
sql
-- MySQL: a second START TRANSACTION commits the first one
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
START TRANSACTION;  -- implicit COMMIT of the UPDATE above
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
ROLLBACK;  -- only the second UPDATE is undone; the debit is permanent

go deeper

for a junior

Remember that transactions do not stack: writing BEGIN twice does not give you an inner transaction you can roll back independently.

for a middle

State what each engine does — PostgreSQL warns and keeps one transaction, MySQL implicitly commits the pending one — and show why the MySQL case can leave books unbalanced with no error.

for a senior

Explain how layering produces this in real code and give the rule that prevents it: the outermost caller owns the boundary and everything beneath joins the transaction it is handed.

for a principal

Set the convention across services so transaction ownership is explicit and testable, including how nested-transaction abstractions in the framework map onto real database behaviour.

## Transactions do not nest The SQL standard defines one active transaction per session. `START TRANSACTION` opens it; `COMMIT` or `ROLLBACK` ends it. There is no construct that opens a transaction inside a transaction and no rule for what committing an inner one would mean while the outer one is still undecided. So the question "what does a second `BEGIN` do?" has no standard answer — and the engines filled the gap differently. ## PostgreSQL: warn and ignore ```sql BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; BEGIN; -- WARNING: there is already a transaction in progress UPDATE accounts SET balance = balance + 100 WHERE id = 2; ROLLBACK; -- both updates are undone: it was always one transaction ``` The second `BEGIN` does nothing except produce a warning. You still have exactly one transaction, and the eventual `COMMIT` or `ROLLBACK` covers everything. This is the benign behaviour: the worst outcome is a warning nobody reads and a mental model that is wrong but harmless here. ## MySQL: the second BEGIN commits the first transaction ```sql START TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE id = 1; START TRANSACTION; -- implicit COMMIT of the update above UPDATE accounts SET balance = balance + 100 WHERE id = 2; ROLLBACK; -- only the second update is undone ``` MySQL documents that beginning a transaction causes any pending transaction to be committed. The debit is now permanent, the credit is rolled back, and the accounts do not balance — with no error anywhere. This is the same failure family as DDL's implicit commit: your transaction ended without you writing an ending. ## Where this actually happens Nobody types two `BEGIN`s in a row on purpose. It happens through layering. A repository helper opens a transaction, does some work, and calls a service method that also opens a transaction because it was written to be usable standalone. On PostgreSQL the inner call quietly joins the outer transaction and the inner `COMMIT` ends the *whole* thing early — a different surprise, and often the more damaging one, since the outer caller believes it can still roll back. On MySQL the inner `BEGIN` commits the outer work outright. Either way, two layers believing they own the boundary is the bug; the engine differences only decide which shape it takes. ## The rules that avoid it 1. **One owner per unit of work.** The transaction boundary belongs to the outermost caller — a request handler, a job runner, a script. Everything below it takes part in whatever transaction it is given. 2. **Library and helper code must not open transactions.** It should either require an open one or explicitly detect that it is being called standalone. Silently opening one makes the function unusable inside a larger unit of work. 3. **Emulate nesting with savepoints when you genuinely need an inner scope.** That is the only mechanism SQL offers for undoing part of a transaction; the inner "transaction" becomes a named point, only the outermost `COMMIT` is a real commit, and frameworks that advertise nested transactions are doing exactly this underneath. 4. **Check, don't guess.** If code must be safe standalone and nested, ask the connection whether a transaction is already active rather than issuing a hopeful `BEGIN` — because what that `BEGIN` does depends on the engine. ## The interview-ready summary "Transactions don't nest in SQL. PostgreSQL warns and keeps the one you had; MySQL implicitly commits it and starts a fresh one, which can make half your work permanent. So I never let two layers own the boundary: the outermost caller opens and ends the transaction, everything below joins it, and if I need an inner scope I use a savepoint." Short, correct on both engines, and it names the design rule rather than just the trivia.

  • How do frameworks offer nested transactions if SQL has none?
    They map the inner scope onto a savepoint: entering a nested block sets a named point, an inner rollback returns to it, and only the outermost boundary issues a real `COMMIT`. The database still sees exactly one transaction. That is why an inner "commit" in such a framework guarantees nothing until the outer one succeeds.
  • Why should library or helper code avoid opening its own transaction?
    Because it takes a boundary the caller may already own. On MySQL the helper's `BEGIN` commits the caller's work; on PostgreSQL the helper's `COMMIT` ends the caller's transaction early. Either way the caller loses the ability to roll back its own unit of work. Helpers should require an open transaction, or check for one, rather than assuming.
  • How do you find this bug in an existing codebase?
    Look for every place that issues a transaction-opening statement and ask whether it can be reached from another one. On PostgreSQL the warning is in the server log and is a free detector; on MySQL there is no signal at all, so the check has to be a code-level audit plus a test that runs the inner path inside an outer transaction and asserts a rollback really undoes everything.

saying these in an interview costs you the question

  • Believes a second BEGIN opens a nested transaction
  • Thinks an inner COMMIT only commits the inner block
  • Assumes the engine raises an error rather than committing silently
  • Lets helper functions open transactions of their own
  • Expects PostgreSQL and MySQL to behave identically here

context