skip to content

How do you write a backfill or seed insert so that re-running it after an interruption is harmless and resumes?

level: middleimportance: must knowfreq 60%

answer

  1. assume it stops halfway and restarts
  2. two properties, not one
  3. predicate describes work still outstanding
  4. absolute assignment, never relative
  5. unique key plus conditional insert

basics

~20 s

Write the predicate so it selects only outstanding work, compute values absolutely from source columns rather than adding to the current one, guard inserts on a unique key, and commit progress so a restart continues where it stopped.

solid answer

~50 s

Assume the run will be interrupted and then started again over the same data. Two properties make that safe. **Idempotence**: running it twice leaves the same end state as running it once. Get it by predicating each write on the *un-migrated* state (`WHERE new_column IS NULL`), by computing values **absolutely** from source columns rather than relatively (`SET total = qty * price`, never `SET total = total + x`), and by guarding inserts with a unique key. **Resumability**: an interrupted run can continue without redoing everything. Get it by ordering on a stable key, committing progress as the run advances, and — ideally — making the driving predicate itself the cursor, so "what is left" is a query rather than remembered state. Anything with an external side effect, such as sending a message, can be made neither and must be kept out of the run.

go deeper

for a junior

Recall the two words and what they mean: idempotent means running it twice leaves the same result, resumable means it can continue after being stopped. Remember that adding to a current value is not idempotent.

for a middle

Explain how a predicate over the un-migrated state gives you both properties at once, why an insert guard needs a unique constraint behind it, and why absolute assignments are safe where relative ones are not.

for a senior

Show how you would actually operate it: an immutable ordering key, progress committed with the rows it covers, external side effects moved out of the run, and a deliberate interrupt-and-restart test before it goes anywhere near production.

for a principal

Frame re-runnability as a design constraint on the data model, not a scripting trick: whether a re-run is even detectable depends on whether the processed state is representable, and that is decided when the columns are designed.

## Two different properties They are often said in one breath, but they are distinct and you need both. | Property | Question it answers | What gives it to you | | --- | --- | --- | | **Idempotent** | If this runs twice over the same rows, is the result the same? | Absolute assignments, predicates on un-migrated state, guarded inserts | | **Resumable** | If this stops halfway, can it continue without starting over? | A stable ordering, committed progress, a predicate that describes remaining work | A run can be idempotent but not resumable: correct on a second run, but it redoes everything from the beginning each time — fine for a hundred rows, hopeless for a hundred million. It can also be resumable but not idempotent: it remembers where it stopped, but the last partly-committed unit is applied twice and the numbers are wrong. Aim for both. ## Make the predicate describe the remaining work The strongest technique is to make the query for "what still needs doing" the run's only state. If the new column is null exactly when a row has not been processed, then: ```sql UPDATE orders SET currency_code = 'EUR' WHERE currency_code IS NULL; ``` is both idempotent and self-resuming: after any interruption, re-issuing it processes precisely what is left, and issuing it against a finished table changes nothing. No progress needs to be recorded because the data itself records it. This works whenever the target state is distinguishable from the source state. When it is not — the backfilled value may legitimately be the same as the un-backfilled one, or the column is not nullable — you need an explicit marker instead: a boolean or timestamp column set as part of the same write, or a small progress table holding the highest key processed. Whatever the marker is, it must be written **in the same transaction** as the change it records, or a crash between them reintroduces the problem. ## Absolute, never relative The classic non-idempotent write is arithmetic against the current value: - `SET balance = balance + adjustment` — doubles on a second run. - `SET notes = notes || ' migrated'` — appends twice. - `SET version = version + 1` — desynchronises anything comparing versions. - Inserting a child row unconditionally — one child on the first run, two on the second. Every one of these has an absolute equivalent that is safe: derive the target value from immutable source columns, or predicate the write on not having been done. When the target genuinely is an accumulation, record what was already contributed so the run can compute the remainder rather than adding blindly. ## Guarded inserts For seed and lookup rows, the guard is a **unique constraint plus a conditional insert**: insert the row only where no row with that key exists. Two points are easy to miss. 1. The unique constraint is what makes it safe. Without it, two concurrent runs both observe "absent" and both insert; the guard is only advisory. 2. Deciding what a duplicate *is* is a modelling decision. A backfill that creates a derived row per parent must key that row on the parent, or a re-run cannot tell an already-created row from a missing one. ## Recording progress When the predicate cannot carry the state, record it explicitly: 1. Order the work by a **stable, immutable key** — a primary key or a created-at timestamp that never changes — so "everything above X" is well defined. 2. Commit as you advance rather than holding one transaction over the entire run. A single giant transaction is not resumable at all: an interruption discards everything. 3. Write the high-water mark in the same transaction as the rows it covers. 4. On restart, read the mark and continue above it; on a fresh run with no mark, start from the beginning. One caution: a high-water mark over an ordering that can change — a mutable `updated_at`, say — silently skips rows. The key must not move. ## What cannot be made idempotent Side effects outside the database are the limit. Sending a notification, publishing an event other systems consume, calling a payment interface, writing a file: a re-run repeats them, and no predicate inside the database prevents that. The standard answer is to keep them out of the data run entirely — record the intent in a row, and let a separate, deliberately deduplicated process act on it. If they must stay, they need their own persisted record of what was already sent, keyed so the check and the send cannot both be repeated. Finally, expect the re-run. A run that has never been interrupted in testing has never been tested; deliberately stopping it halfway and starting it again is the only honest check that both properties hold.

  • Why does a unique constraint matter if the insert already checks whether the row exists?
    Because the check and the insert are two steps. Two concurrent runs, or one run overlapping its own restart, can both see the row as absent and both insert it. The constraint turns a race into a rejected write, which the run can treat as "already present"; without it the guard only narrows the window.
  • When can a backfill be resumable without recording any progress at all?
    When the un-processed state is distinguishable in the data itself — typically a null target column, or a value that only the backfill can produce. The driving predicate then selects exactly the remaining rows, so restarting is just issuing it again. That is the preferred design; an explicit marker is the fallback when the states cannot be told apart.
  • What goes wrong if the high-water mark is kept over a mutable ordering column?
    Rows move past the mark. If ordering is by a column that later updates, a row that changes after being skipped over can end up below the mark and never be processed, or be processed twice. Order by an immutable key — the primary key or a creation timestamp that is never rewritten.
  • A backfill must also notify another system for each affected row. How do you keep it re-runnable?
    Do not send from the run. Write the intent as rows in a table, idempotently and keyed by the affected record, and let a separate consumer send them with its own deduplication. Re-running the data change then re-asserts rows rather than re-sending messages, and the send path owns its own once-only guarantee.

An idempotent run is a shopping list you tick off, not a running tally you add to. Read the list again after being interrupted and you buy only what is still missing; re-read a tally and you double the total.

saying these in an interview costs you the question

  • Assumes the run will complete first time, so re-running is never considered.
  • Uses relative arithmetic such as adding to the current value in a backfill.
  • Relies on a conditional insert with no unique constraint behind it.
  • Holds one transaction over the entire run and calls that resumable.
  • Records the progress marker in a separate transaction from the rows it covers.
  • Sends messages or calls other systems from inside the data run.