Situation clarification: first confirm scope — which nightly job, which tables/joins, and the exact downstream metric that doubled. Reproduce on a copy of the pipeline with yesterday's and a known-good day's outputs.Diagnostics — quick high-level checks- Compare row counts by table and pipeline stage between good and bad runs:sql
SELECT 'customers' AS t, COUNT(*) FROM customers WHERE batch_date = '2025-12-05';
SELECT 'addresses' AS t, COUNT(*) FROM addresses WHERE batch_date = '2025-12-05';
SELECT 'orders' AS t, COUNT(*) FROM orders WHERE batch_date = '2025-12-05';
- Compare pipeline outputs (pre-join vs post-join):sql
SELECT stage, COUNT(*) FROM staging_table WHERE batch_date = '2025-12-05' GROUP BY stage;
Find where duplication appears (join cardinality checks)- For each join, compute counts before and after and group-by key multiplicity:sql
-- multiplicity in addresses per customer
SELECT customer_id, COUNT(*) AS addr_cnt
FROM addresses WHERE batch_date = '2025-12-05'
GROUP BY customer_id HAVING COUNT(*) > 1 LIMIT 10;
-- multiplicity in orders per address
SELECT address_id, COUNT(*) AS ord_cnt
FROM orders WHERE batch_date = '2025-12-05'
GROUP BY address_id HAVING COUNT(*) > 1 LIMIT 10;
- Run join cardinality matrix to detect many-to-many surprises:sql
SELECT COUNT(*) FROM customers c
JOIN addresses a ON c.customer_id = a.customer_id
WHERE c.batch_date = '2025-12-05';
-- compare to COUNT(distinct c.customer_id) and COUNT(distinct a.address_id)
Validate key uniqueness and identity- Check primary-key uniqueness and nulls:sql
SELECT COUNT(*) AS total, COUNT(DISTINCT customer_id) AS distinct_ids FROM customers WHERE batch_date = '2025-12-05';
SELECT COUNT(*) - COUNT(DISTINCT <pk>) AS duplicate_rows FROM <table> WHERE batch_date = '2025-12-05';
- Use window functions to find duplicate rows (identical payloads):sql
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY customer_id, name, email ORDER BY ingest_ts DESC) rn
FROM customers_staging WHERE batch_date = '2025-12-05'
) t WHERE rn > 1 LIMIT 50;
- Compute checksums to detect unintended re-ingest:sql
SELECT customer_id, MD5(CONCAT_WS('|', col1,col2,...)) AS row_hash, COUNT(*) FROM customers_staging GROUP BY customer_id,row_hash HAVING COUNT(*) > 1;
Compare diffs between last-good and bad runs- Use EXCEPT/LEFT JOIN to find newly duplicated keys or new rows:sql
SELECT customer_id FROM customers_bad EXCEPT SELECT customer_id FROM customers_good;
Root-cause hypotheses & tests- Was a JOIN key changed (e.g., address.customer_id nulls replaced)? Test for NULLs or whitespace in join keys.- Was upstream dedupe failing (staging dedupe removed/buggy)? Re-run dedupe logic on staging data to see if it removes duplicates.- Was incremental logic (CDC) applied twice? Check offsets/timestamps and ingestion markers.Fixes (short-term remediation)- Fix the offending join by deduping input relation with deterministic rule before join:sql
WITH dedup_addresses AS (
SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY address_id ORDER BY updated_ts DESC) rn FROM addresses WHERE batch_date = '2025-12-05'
) x WHERE rn = 1
)
-- use dedup_addresses in downstream joins
- If duplication came from many-to-many semantics, change join to aggregate or distinct keys prior to join (e.g., join on primary keys only, or aggregate orders counts before join).Fixes (long-term & prevention)- Enforce uniqueness constraints where possible (DB primary keys, unique indexes) or add pipeline assertions that fail the job if duplicates appear (e.g., count != distinct_count).- Add idempotent ingestion patterns: use MERGE/upsert with natural keys and last-write-wins logic instead of blind inserts.- Add end-to-end tests: nightly checks that compare row counts, distinct counts, key null rates, and row-hash diffs against baseline.- Add monitoring/alerting: data quality metrics (duplicate rate, cardinality ratio) with thresholds and automated alerts.- Add lineage and lineage-aware sampling so you can trace a duplicated downstream row back to source row(s).Communication & postmortem- Triage: communicate impact, rollback or reprocess if needed, and run reprocessing with patched dedupe logic.- Postmortem: document root cause, timeline, code diff that introduced bug, and the permanent controls (assertions, constraints, monitoring) deployed.This approach systematically narrows the problem from pipeline stage to specific join or upstream ingestion, validates uniqueness with COUNT vs COUNT(DISTINCT) and window functions, and prescribes both immediate remediation and long-term controls to prevent recurrence.