Symptoms
- Totals jump on a dashboard right after an incident or a manual re-run.
- Primary-key-like columns have duplicates; downstream joins fan out.
1. Measure the damage
SELECT order_id, COUNT(*) AS copies
FROM silver.orders
WHERE load_date = '2026-10-24'
GROUP BY order_id HAVING COUNT(*) > 1
ORDER BY copies DESC LIMIT 20;
Confirm they're true duplicates (all columns equal) or different versions of the same key (need "latest wins").
2. Pause downstream
Stop gold-layer jobs and extracts that would spread the duplicates further.
3. Remove duplicates
Delta Lake — simplest if the table was clean before the bad run:
DESCRIBE HISTORY silver.orders LIMIT 10;
RESTORE TABLE silver.orders TO VERSION AS OF 1287; -- version before the duplicate append
Then re-run the load once, correctly.
Delta Lake — dedupe in place, keeping the latest per key:
CREATE OR REPLACE TEMP VIEW orders_dedup AS
SELECT * EXCEPT (rn) FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY ingest_ts DESC) AS rn
FROM silver.orders WHERE load_date = '2026-10-24') WHERE rn = 1;
INSERT OVERWRITE silver.orders REPLACE WHERE load_date = '2026-10-24'
SELECT * FROM orders_dedup;
Snowflake:
CREATE OR REPLACE TABLE orders_fix AS
SELECT * FROM orders QUALIFY ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY ingest_ts DESC) = 1;
ALTER TABLE orders SWAP WITH orders_fix;
SQL Server / Azure SQL:
WITH d AS (SELECT *, ROW_NUMBER() OVER (PARTITION BY OrderID ORDER BY IngestTs DESC) rn FROM dbo.Orders WHERE LoadDate = '2026-10-24')
DELETE FROM d WHERE rn > 1;
Warning
Take a copy before deleting (Delta/Snowflake history covers you; on SQL,
SELECT INTOa backup table). Verify row counts against the source system before and after.
4. Rebuild downstream
Recompute gold tables for affected dates, refresh semantic models, and tell report owners which numbers were wrong and for how long.
5. Make the load idempotent
| Pattern | Use for |
|---|---|
MERGE on a business key | Upserts, CDC feeds |
Partition overwrite (REPLACE WHERE, dynamic partition overwrite) | Daily/hourly batch loads |
| Insert-only with dedupe key + unique constraint (SQL) | Event logs |
| Write-audit-publish (stage, validate, then swap) | Critical financial tables |
MERGE INTO silver.orders t
USING staged_orders s ON t.order_id = s.order_id
WHEN MATCHED AND s.ingest_ts > t.ingest_ts THEN UPDATE SET *
WHEN NOT MATCHED THEN INSERT *;
Make sure the source batch itself is deduplicated before MERGE — MERGE fails when several source rows match one target row.
Prevent it next time
- A uniqueness test on key columns after each load (dbt
unique, DLT expectation, a simple SQL check) that blocks publishing. - Re-run procedures in the runbook: "always re-run the whole partition", never "append the missing part".
- Record batch IDs on every row so a bad batch can be deleted precisely.