After a failed statement, how does ROLLBACK TO SAVEPOINT let the transaction continue?
answer
- the mark has to exist before the failure
- a rewind clears the failed state too
- engines disagree on what an error aborts
- release on the success path
- granularity is the real tradeoff
basics
~20 sSet a savepoint before the risky statement. If it fails, ROLLBACK TO SAVEPOINT clears the failed statement's effects and returns the transaction to a usable state, so the earlier work and the remaining statements can still commit together.
solid answer
~40 sEngines differ in what a statement error does to an open transaction. Some roll back only the failing statement and let you continue; PostgreSQL puts the whole transaction into an aborted state, and every later statement is rejected with "current transaction is aborted, commands ignored until end of transaction block" until you end it. The portable escape is a savepoint: `SAVEPOINT sp` before the statement that might fail, and `ROLLBACK TO SAVEPOINT sp` in the error path. That rewind both undoes the failed statement's partial effects and clears the aborted state, so the transaction is usable again and the work done before the savepoint survives to the final `COMMIT`. On success you `RELEASE SAVEPOINT sp` so the marks do not accumulate across iterations.
code
sql · 13 linesBEGIN;
INSERT INTO import_run (id, started_at) VALUES (17, CURRENT_TIMESTAMP);
SAVEPOINT row_sp;
INSERT INTO customers (id, email) VALUES (7, '[email protected]'); -- unique violation
ROLLBACK TO SAVEPOINT row_sp; -- undoes the failure, transaction usable again
INSERT INTO import_error (run_id, row_id, reason) VALUES (17, 7, 'duplicate email');
SAVEPOINT row_sp2;
INSERT INTO customers (id, email) VALUES (8, '[email protected]');
RELEASE SAVEPOINT row_sp2; -- success path: drop the mark
COMMIT;go deeper
Recall the shape of the pattern: savepoint before a statement that might fail, ROLLBACK TO SAVEPOINT if it does, then carry on. Know that the savepoint must be set beforehand.
Explain what the rewind actually restores — the failed statement's effects undone and the transaction usable again — and that the surviving work still depends on the final COMMIT.
Name that engines differ on whether a statement error aborts the whole transaction, and show the loop you would write: savepoint, attempt, rewind-and-record on failure, release on success, with a defensible granularity.
Own the tradeoff: per-row recovery versus per-chunk cost, how failures get recorded for reprocessing, and when the honest design is many short transactions rather than one long transaction full of savepoints.
## The problem the pattern solves You are inside an explicit transaction, several statements in, and one statement raises an error — a unique violation, a check-constraint failure, a bad cast. What can the session do next? That depends on the engine, and this is one of the few places where the difference is loud enough that a portable answer has to name it: - Some engines perform **statement-level rollback**: the failing statement's own partial effects are undone, the transaction stays usable, and the application decides what to do next. - PostgreSQL aborts the **whole transaction**. Every subsequent statement is rejected with `current transaction is aborted, commands ignored until end of transaction block`. The only accepted moves are ending the transaction — or rewinding to a savepoint. Either way, the language-level tool that makes error recovery explicit and portable is the savepoint. ## The pattern ```sql BEGIN; INSERT INTO import_run (id, started_at) VALUES (17, CURRENT_TIMESTAMP); SAVEPOINT row_sp; INSERT INTO customers (id, email) VALUES (7, '[email protected]'); -- on error: ROLLBACK TO SAVEPOINT row_sp; INSERT INTO import_error (run_id, row_id, reason) VALUES (17, 7, 'duplicate email'); -- on success instead: -- RELEASE SAVEPOINT row_sp; SAVEPOINT row_sp2; INSERT INTO customers (id, email) VALUES (8, '[email protected]'); RELEASE SAVEPOINT row_sp2; COMMIT; ``` The savepoint is set **before** the statement that may fail — after the failure it is too late, and on an engine that has aborted the transaction you cannot even execute `SAVEPOINT` any more. `ROLLBACK TO SAVEPOINT` in the error path does two jobs at once: it undoes whatever the failed statement managed to change, and it clears the transaction's failed state so that ordinary statements are accepted again. Note that the error-logging `INSERT` above runs *after* the rewind — it would be rejected if issued while the transaction was still in the aborted state. ## What the rewind does not do It does not re-run the failed statement, and it does not turn the error into a success. Your code still has to decide: skip the row, substitute a value, log it, or give up and roll the whole transaction back. The savepoint only preserves the option of continuing. It also does not commit anything. The pre-savepoint work is still pending; if the loop later hits a fatal condition and issues a plain `ROLLBACK`, all of it goes. Partial rollback protects work from *this statement's* failure, not from the transaction's overall fate. ## Success path: release If every iteration sets a savepoint and none of them ever releases, a long batch ends up carrying one live savepoint per row. `RELEASE SAVEPOINT` on the success path keeps exactly one mark live at a time. Distinct names per iteration are safer than reusing one name, because engines disagree about what re-establishing an existing name means. ## Cost, briefly A savepoint per statement is not free — each one is bookkeeping the engine carries for the life of the transaction, and wrapping every row of a large import in its own savepoint measurably slows the import. The judgment call is granularity: a savepoint per row buys row-level recovery at row-level cost, a savepoint per chunk of rows loses the whole chunk when any row in it fails but costs far less. Choose per how likely failures are and how expensive re-doing a chunk would be. ## Portability notes The statements themselves are widely supported, though spellings vary (`ROLLBACK TO SAVEPOINT name`, with `WORK` and `SAVEPOINT` optional in some dialects; T-SQL spells the feature `SAVE TRANSACTION` / `ROLLBACK TRANSACTION name`). What is *not* portable is the assumption about what a statement error does to the transaction — so write the savepoint even against an engine that would have let you continue anyway, and the code behaves the same everywhere. ## What interviewers listen for The strong answer names the divergence in error behaviour, places the `SAVEPOINT` before the risky statement rather than after the failure, notes that the rewind restores a usable transaction, and mentions releasing on success plus the granularity tradeoff. The weak answer assumes errors always just skip, or reaches for `COMMIT` in the error handler and expects partial success.
- Why must the SAVEPOINT be issued before the risky statement rather than in the error handler?Because a savepoint marks a point in the past you can return to, and on an engine that aborts the transaction on error you cannot execute SAVEPOINT at all once the failure has occurred. Setting it afterwards would mark a state that already contains the failure.
- Is a savepoint per row always the right granularity for a large batch?No. Per-row savepoints buy row-level recovery but add per-row bookkeeping that slows big imports. A savepoint per chunk is cheaper and loses the whole chunk on any failure. Pick by expected failure rate and by how expensive re-doing a chunk is.
- After ROLLBACK TO SAVEPOINT in an error handler, can you simply COMMIT?Yes — once the rewind has cleared the failed state, COMMIT is accepted and makes everything that survived permanent. What you cannot do is COMMIT while the transaction is still in the aborted state; engines that abort on error reject it, and some treat it as a rollback instead.
saying these in an interview costs you the question
- Assumes a failed statement always just skips and the transaction continues
- Issues SAVEPOINT after the failure instead of before
- Calls COMMIT in the error handler expecting partial success
- Thinks ROLLBACK TO SAVEPOINT retries the failed statement
- Wraps every statement in a savepoint without considering the cost