Deduplication: Intermediate
ROW_NUMBER Deduplication
Recognize that deduplication questions require you to define what constitutes a duplicate (exact, fuzzy, time-window).
The question that recurs in interviews past the basics: 'design a pipeline that ingests a CDC stream of orders, where the upstream system emits the same order row multiple times during database failovers, and produces a clean orders fact table the analytics layer reads from.' The candidate who writes a ROW_NUMBER + filter query in a one-shot SELECT and stops there is producing a query that works on a snapshot. The candidate who designs the MERGE pattern with idempotent dedup keys, emits a metric for unexpected duplicate volume, and explains why this approach survives multiple ingestion runs without producing different results is designing the pipeline.
Three design decisions this lesson covers
- MERGE or INSERT ... ON CONFLICT patterns that dedupe as rows land in the target. Idempotency by construction.
- include the upstream LSN in the dedup key. Failover replays share the LSN; legitimate updates have new ones.
- emit duplicate count per run; alert on drift. The metric surfaces upstream issues before consumers see them.
- ▸"the CDC stream sends the same row twice during failovers"
- ▸"how do we make this dedup idempotent across pipeline reruns?"
- ▸"the duplicate count drifted last week; what do we do?"
- ▸"write-time dedup or read-time dedup, which one for this use case?"
- ▸"the consumer sees the same order twice in the dashboard"
Why these decisions matter in production
Choosing Which Duplicate to Keep (ORDER BY)
Write the standard pattern: ROW_NUMBER() OVER (PARTITION BY dedup_key ORDER BY tiebreaker) = 1.
The MERGE pattern for idempotent dedup
Reading the pattern
- ▸Source can have multiple rows per dedup key (CDC duplicates)
- ▸MERGE with duplicate source keys produces engine-specific behavior
- ▸ROW_NUMBER + WHERE rn=1 ensures one source row per target
- ▸Idempotency holds: same source → same MERGE actions → same target state
Why ROW_NUMBER in the USING clause
If the source has multiple rows per order_id (CDC stream emitting duplicates), a MERGE without dedup tries to apply multiple updates to the same target row, which engines treat as an error or as last-write-wins depending on the dialect. The ROW_NUMBER picks one row per key inside the USING clause, so the MERGE sees exactly one source row per target. State this when writing: 'the ROW_NUMBER in USING is mandatory; without it, the MERGE behavior on duplicate source keys is engine-specific and unsafe to rely on.'
INSERT ... ON CONFLICT for Postgres-family
- The engine supports it (Snowflake, BigQuery, SQL Server, Oracle, Postgres 15+)
- The dedup logic needs separate UPDATE and INSERT actions
- The team standardizes on MERGE syntax across pipelines
- Multi-statement transactions are acceptable
- Engine is Postgres or compatible
- The dedup logic is mostly upsert (insert or update; no complex branching)
- The team standardizes on Postgres-family syntax
- The query needs to be portable across CockroachDB or similar
The WHERE clause that protects newer data
DISTINCT vs GROUP BY vs ROW_NUMBER
Know when each deduplication method applies: DISTINCT for exact rows, GROUP BY for aggregation, ROW_NUMBER for keeping specific rows.
The CDC duplicate scenario
- ▸Connector reconnects after failover; replays uncommitted batch
- ▸Same logical change emitted twice with different ingestion timestamps
- ▸Both look legitimate without LSN-aware dedup
- ▸Without dedup, replaying the stream produces different target state
The dedup key in CDC
LSN-aware MERGE
Deletes in CDC dedup
- DELETE the target row immediately
- Loses history; cannot audit what was deleted when
- Simpler downstream queries (no deleted_at filter)
- Right when the source treats deletes as 'never existed'
- UPDATE the target row's deleted_at column
- Preserves history; full audit trail
- Downstream queries filter deleted_at IS NULL
- Right when audit matters; separate cleanup handles physical deletion
Partial-Key and Multi-Column Duplicates
Handle near-duplicates: events within N seconds, case-insensitive matching, phonetic similarity.
The duplicate-count metric
The alert pattern
Per-key duplicate distribution
Dedup metrics as a contract
Deterministic Tiebreaks for Stable Output
Discuss idempotent deduplication in ETL: dedup-on-write vs dedup-on-read, MERGE semantics, and exactly-once guarantees.
Write-time dedup
Read-time dedup
- Many consumers read the same data; one dedup is amortized across them
- Dedup logic is stable; the discipline is established
- Storage cost matters; deduped target is much smaller than raw event stream
- Read latency matters; consumers cannot afford per-query dedup
- Source-of-truth must retain all rows for audit or replay
- Different consumers want different dedup logic (some include duplicates)
- Dedup logic is evolving; pipeline rerun cost outweighs per-query cost
- Storage is cheap; deduped target wouldn't save significant cost
The hybrid pattern
- ▸Raw event stream preserved unchanged (read-time semantics; can be replayed)
- ▸Deduped target maintained by an ingestion job (write-time semantics; fast reads)
- ▸Common-case readers query the deduped target
- ▸Audit and custom-dedup consumers query the raw stream
- ▸Both properties: retention and read speed
Why this conversation matters at scale
> You are in a data engineering interview at a logistics company. The interviewer asks: 'Design a pipeline that ingests a CDC stream of orders, where the upstream system emits the same order row multiple times during database failovers, and produces a clean orders fact table the analytics layer reads from.'
(source_key, source_lsn), not the natural key plus an ingestion timestamp. Connector replays reuse the same LSN, so LSN-keyed dedup catches them while a timestamp-keyed dedup treats every replay as a distinct row and inflates counts.ROW_NUMBER inside the USING clause of a MERGE and filter to rn = 1 on each branch. Without it, multiple source rows can match one target row and the engine raises a nondeterministic-match error or applies the updates in arbitrary order.WHERE EXCLUDED.created_at > target.created_at guard on the update branch is what protects against out-of-order CDC delivery. Without it a delayed message with an older timestamp overwrites newer target state with stale data.Real data has duplicates; the interview tests whether you can define "duplicate"
- Category
- SQL
- Difficulty
- intermediate
- Duration
- 25 minutes
- Challenges
- 0 hands-on challenges
Topics covered: ROW_NUMBER Deduplication, Choosing Which Duplicate to Keep (ORDER BY), DISTINCT vs GROUP BY vs ROW_NUMBER, Partial-Key and Multi-Column Duplicates, Deterministic Tiebreaks for Stable Output
Lesson Sections
- ROW_NUMBER Deduplication (concepts: sqlDistinct)
Three design decisions this lesson covers First: write-time dedup. MERGE or INSERT ... ON CONFLICT patterns that dedupe as rows land in the target, so the target is always in a deduped state. Idempotency is the key property: rerunning the ingestion produces the same target rows, not duplicate target rows. Second: CDC-aware dedup. CDC streams emit the same row multiple times during failovers, retries, and reconnections; the dedup discipline catches the duplicates at the ingestion boundary and emi
- Choosing Which Duplicate to Keep (ORDER BY) (concepts: sqlWindowDedup)
Write-time dedup means the deduplication happens as rows land in the target table, not when consumers query it. The target is always in a deduped state. The pattern is MERGE on engines that support it (Snowflake, BigQuery, SQL Server, Oracle, Postgres 15+) or INSERT ... ON CONFLICT on Postgres-family engines, with the dedup key matching the source's natural key. The MERGE pattern for idempotent dedup Reading the pattern The USING clause includes a ROW_NUMBER that picks the most recent row per or
- DISTINCT vs GROUP BY vs ROW_NUMBER (concepts: sqlWindowDedup)
CDC streams are the canonical case for production dedup. Debezium, AWS DMS, Fivetran, Stitch, and Snowflake Streams all emit row-level changes from a source database to a downstream system. During failovers, retries, and reconnections, the same logical change can be emitted multiple times with different ingestion timestamps. The dedup pattern has to handle this without dropping legitimate updates and without keeping spurious duplicates. The CDC duplicate scenario Imagine the upstream MySQL prima
- Partial-Key and Multi-Column Duplicates (concepts: sqlWindowDedup)
The dedup operation produces a count of removed duplicates per run. That count is itself a metric. A pipeline that silently dedupes is a pipeline whose data quality issues are invisible; a pipeline that emits the duplicate count and alerts on drift catches upstream regressions before consumers do. This section covers the metric, the alert, and the operational pattern. The duplicate-count metric Two CTEs compute the source row count and the deduped row count. The INSERT writes a single row into a
- Deterministic Tiebreaks for Stable Output (concepts: sqlWindowDedup)
The last design decision: write-time dedup vs read-time dedup. Write-time stores the deduped target; every consumer reads the deduped data. Read-time stores all rows including duplicates, and each consumer dedupes in their query. Each has a cost; each is right in a different workload. Picking based on read-write ratio is the design conversation. Write-time dedup Pros: every consumer reads clean data; no per-query dedup logic; storage is bounded by unique rows. Cons: the ingestion job is responsi