Deduplication
Deduplication is the process of identifying and removing duplicate records from a dataset, keeping only one representative row per logical entity.
Overview
Duplicate rows creep into datasets for many reasons: a source system emits the same event twice, an ingestion job is re-run and re-appends data, or two records legitimately represent the same customer with slightly different spellings. Deduplication is the step that resolves this down to one row per real-world entity, which matters because duplicates silently inflate counts, sums, and averages in any downstream aggregation.
There are two related but distinct cases: exact duplicates, where entire rows are identical, and logical duplicates, where rows share a key (like an order ID or email) but differ in other columns — often because you want to keep only the most recent version.
Removing exact duplicates
For fully identical rows, DISTINCT is enough:
Copy code
SELECT DISTINCT * FROM events;
Removing duplicates by key
When duplicates share a key but you want to keep one row per key (e.g. the latest), DuckDB's QUALIFY clause combined with a window function is the cleanest approach:
Copy code
SELECT *
FROM orders
QUALIFY ROW_NUMBER() OVER (
PARTITION BY order_id
ORDER BY updated_at DESC
) = 1;
QUALIFY filters on a window function result without needing a wrapping subquery, so you can compute ROW_NUMBER() per order_id and keep only the top-ranked row (here, the most recently updated one) in a single statement. The same pattern works for any "latest record wins" or "first record wins" scenario by adjusting the ORDER BY.
An alternative for the common "one row per key" case is DuckDB's DISTINCT ON, e.g. SELECT DISTINCT ON (order_id) * FROM orders ORDER BY order_id, updated_at DESC.
Deduplicating during ingestion
Deduplication is often applied as a transformation step immediately after landing raw data, before it's used elsewhere — for example, as a dbt staging model that wraps the QUALIFY pattern above, so every downstream model can assume one row per key without re-deduplicating itself.
Why it matters
Undetected duplicates are one of the most common causes of inflated metrics: doubled revenue, doubled user counts, or broken joins that fan out unexpectedly. Deduplication logic should be applied consistently and as early in the pipeline as possible, ideally paired with a downstream test (e.g. asserting a key column is unique) to catch regressions if a source starts emitting duplicates again.
Related terms
ROW_NUMBER() is a SQL window function that assigns a unique, sequential integer to each row within a partition based on a specified order, with no ties.
QUALIFY →QUALIFY filters rows based on the result of a window function, letting you write conditions like 'keep only the top-ranked row per group' without wrapping the query in a subquery.
DISTINCT →DISTINCT removes duplicate rows from a query's result set, returning only unique combinations of the selected columns.
OVER clause →The OVER clause turns an aggregate or ranking function into a window function by defining the partition, order, and frame it operates on, without collapsing the result set.
Natural key →A natural key is an identifier that comes from real-world business data — like an email address, SSN, or product SKU — as opposed to a surrogate key generated purely for database use.
primary key →A primary key uniquely identifies each database row. Learn SQL definitions, examples, UUID vs auto-increment best practices, and how to fix constraint errors.
FAQS
DISTINCT removes rows that are fully identical across all selected columns. QUALIFY with ROW_NUMBER() lets you deduplicate by a subset of columns (a key) while choosing which duplicate to keep — for example the most recently updated row — based on an ORDER BY.
Common causes include re-running an ingestion job without checkpointing, at-least-once delivery from message queues or APIs, upstream systems emitting update and correction events with the same ID, and merging data from multiple sources that overlap.
Use idempotent load patterns (like MERGE / upsert on a primary key instead of plain INSERT), and add automated uniqueness tests on key columns so a pipeline run fails loudly if duplicates reappear rather than silently accumulating.
