← All lessons
Warehouses and pipelines · Lesson 6 of 7

Idempotent and backfill-safe jobs

A job is idempotent when running it twice leaves the same result as running it once. Jobs get rerun all the time (a retry after a timeout, a fix after a bug, a backfill of last month), so this is the property that keeps a warehouse right.

The classic mistake is a job that just appends. Run it twice and every row is there twice; totals double and nobody notices for a week.

One fix is to replace the run's slice of data: delete the partition (say, one day) and insert it again, in one transaction, or overwrite the partition where the warehouse supports it. The other is to upsert on a key: insert new rows and update the ones that already exist, with MERGE in most warehouses or INSERT … ON CONFLICT in PostgreSQL and SQLite. Here a daily revenue job runs twice and the table still has one row for the day:

CREATE TABLE daily_revenue (day TEXT PRIMARY KEY, revenue REAL);

-- the job for 2024-09-15, run twice
INSERT INTO daily_revenue (day, revenue)
SELECT o.ordered_on, SUM(i.qty * p.price)
FROM orders o JOIN order_items i ON i.order_id = o.id JOIN products p ON p.id = i.product_id
WHERE o.ordered_on = '2024-09-15' GROUP BY o.ordered_on
ON CONFLICT(day) DO UPDATE SET revenue = excluded.revenue;

INSERT INTO daily_revenue (day, revenue)
SELECT o.ordered_on, SUM(i.qty * p.price)
FROM orders o JOIN order_items i ON i.order_id = o.id JOIN products p ON p.id = i.product_id
WHERE o.ordered_on = '2024-09-15' GROUP BY o.ordered_on
ON CONFLICT(day) DO UPDATE SET revenue = excluded.revenue;

SELECT * FROM daily_revenue;
Returns
day        | revenue
-----------+--------
2024-09-15 | 93.5
(1 row)

Use the run's logical date, never "now". A job that processes WHERE day = today() does the wrong day when it's rerun tomorrow. Pass the date in, and write each run so it touches only its own slice.

A backfill reruns a job for past dates: to fill history for a new table, or to repair a bug. With idempotent, date-sliced jobs it's just "run each day from September to now", and the runs can go in parallel because they don't touch each other's data. Without them it's a careful manual cleanup.

Late data needs a plan too: if payments can arrive up to three days late, have each run reprocess the last few days, which is only safe because the job is idempotent.

Practice

What does idempotent mean for a pipeline job?

More info

Quick reference

IdempotentRunning twice = running once.
Delete + insert a partitionReplace the run's slice in one transaction.
MERGE / ON CONFLICTUpsert: insert new keys, update existing ones.
Logical dateThe period a run is for, passed in, not "now".
BackfillRerunning a job for past periods.
Lookback windowReprocessing recent days to catch late data.

The other AI and data engineering courses