lesson 85 Warehouse & reporting projects · advanced
Idempotent incremental UPSERT & stale updates
Repeat incremental updates safely and reject stale changes.
the idea
An incremental load inserts new keys and updates existing ones in one statement: INSERT … ON CONFLICT(customer_id) DO UPDATE SET … . Guard the update with WHERE excluded.updated_at > current_people.updated_at so stale records cannot overwrite newer data, and the same batch can be applied twice with the same result.
Dataset: Retail
The tables the challenges use, with their first 6 rows. Under each column name: the kind of value it holds, and which table it links to.
current_people4 columns · 40 rows
| customer_keynumber · unique id | customer_idtext | contact_nametext | updated_atdate & time |
|---|---|---|---|
| 1 | 12347 | Arthur Turner | 2011-11-30 18:00:00 |
| 2 | 12353 | Harry Turner | 2011-11-30 18:00:00 |
| 3 | 12359 | Theo Turner | 2011-11-30 18:00:00 |
| 4 | 12373 | Harry Hill | 2011-11-30 18:00:00 |
| 5 | 12374 | Emily Hill | 2011-11-30 18:00:00 |
| 6 | 12384 | Ava Moore | 2011-11-30 18:00:00 |
- customer_keynumber · unique id
- 1
- customer_idtext
- 12347
- contact_nametext
- Arthur Turner
- updated_atdate & time
- 2011-11-30 18:00:00
- customer_keynumber · unique id
- 2
- customer_idtext
- 12353
- contact_nametext
- Harry Turner
- updated_atdate & time
- 2011-11-30 18:00:00
- customer_keynumber · unique id
- 3
- customer_idtext
- 12359
- contact_nametext
- Theo Turner
- updated_atdate & time
- 2011-11-30 18:00:00
- customer_keynumber · unique id
- 4
- customer_idtext
- 12373
- contact_nametext
- Harry Hill
- updated_atdate & time
- 2011-11-30 18:00:00
- customer_keynumber · unique id
- 5
- customer_idtext
- 12374
- contact_nametext
- Emily Hill
- updated_atdate & time
- 2011-11-30 18:00:00
- customer_keynumber · unique id
- 6
- customer_idtext
- 12384
- contact_nametext
- Ava Moore
- updated_atdate & time
- 2011-11-30 18:00:00
first 6 of 40 rows · your queries use all of them
incoming_people3 columns · 8 rows
| customer_idtext | contact_nametext | updated_atdate & time |
|---|---|---|
| 12359 | Theo Turner (updated) | 2011-12-05 08:00:00 |
| 12392 | Lily Moore (updated) | 2011-12-05 08:00:00 |
| 12447 | Arthur Taylor (updated) | 2011-12-05 08:00:00 |
| 12507 | Arthur Wilson (updated) | 2011-12-05 08:00:00 |
| 12374 | Emily Hill (stale) | 2011-11-15 08:00:00 |
| 12566 | Mia Patel (stale) | 2011-11-15 08:00:00 |
- customer_idtext
- 12359
- contact_nametext
- Theo Turner (updated)
- updated_atdate & time
- 2011-12-05 08:00:00
- customer_idtext
- 12392
- contact_nametext
- Lily Moore (updated)
- updated_atdate & time
- 2011-12-05 08:00:00
- customer_idtext
- 12447
- contact_nametext
- Arthur Taylor (updated)
- updated_atdate & time
- 2011-12-05 08:00:00
- customer_idtext
- 12507
- contact_nametext
- Arthur Wilson (updated)
- updated_atdate & time
- 2011-12-05 08:00:00
- customer_idtext
- 12374
- contact_nametext
- Emily Hill (stale)
- updated_atdate & time
- 2011-11-15 08:00:00
- customer_idtext
- 12566
- contact_nametext
- Mia Patel (stale)
- updated_atdate & time
- 2011-11-15 08:00:00
first 6 of 8 rows · your queries use all of them
worked example
Run these steps in order to see the idea before you write your own query. The step marked “the pattern” is what the challenges ask for. Running the example does not count toward progress.
Compare incoming with current timestamps. Side-by-side updated_at values show new keys, stale replays, and rows that should win on conflict.the pattern
example.sqlSELECT i.customer_id, i.updated_at AS incoming, c.updated_at AS current FROM incoming_people i LEFT JOIN current_people c ON c.customer_id = i.customer_id;
Count keys that would conflict. These incoming customer_ids already exist—rerunning a plain INSERT would fail without ON CONFLICT.
step-2.sqlSELECT COUNT(*) AS would_conflict FROM incoming_people i JOIN current_people c ON c.customer_id = i.customer_id;
See which incoming rows are newer. The newer flag is the UPSERT guard: only when incoming updated_at beats current should contact_name and updated_at change.
step-3.sqlSELECT i.customer_id, i.updated_at AS incoming, c.updated_at AS current, i.updated_at > c.updated_at AS newer FROM incoming_people i JOIN current_people c ON c.customer_id = i.customer_id;
common mistake
Without the timestamp guard, replaying an old record overwrites a newer value; without ON CONFLICT, rerunning the batch fails on duplicate keys.