Skip to content
About
Main content

Deduplication: three different problems in one word

Why does fuzzy matching have a recall ceiling, and why does loosening a threshold collapse clusters?

First, a boundary. Transport-level duplicates, the same message delivered twice, are solved exactly with sequence numbers or idempotency keys, because the duplicate carries proof of its own duplication. Everything in this section is the case where no such proof exists: two records that represent the same real-world entity and were created independently.

Exact dedup on a business key

Exact dedup on a business key (say, (phone, name)) is uniqueness enforcement in different clothes, and it inherits the same constraint from The free tier: candidate matches must meet, so you either co-partition by the key or you data.

The interesting variant is , which bounds memory by bounding time: "treat two records as duplicates only if they arrive within 24 hours of each other". The bound is an assumption: that duplicates arrive close together. It cannot be verified from inside the pipeline. When it is wrong, you get no error. You get a duplicate that passed. Whoever shrank the window to save memory has silently bought a with it, and the purchase is invisible in every metric the pipeline reports.

Fuzzy dedup: a two-stage filter

Fuzzy dedup (names with typos, different spellings, different formats) is entity resolution, and its mechanics are taught there. In short: comparing every pair is quadratic (about 5 × 10¹³ pairs for 10⁷ records), so every real system splits the work in two:

  1. A cheap step that produces candidate pairs (a shared postcode, a , MinHash bands).
  2. An expensive that scores each candidate pair.

For a check, what matters is how that design goes wrong.

The consequence: recall is capped by the cheap stage

The consequence is sharp, and applies far beyond customer matching.

Teams tune scoring thresholds for months, while their ceiling was fixed the day someone chose to block on postcode. A customer who moved house, or whose postcode has a typo, is never compared with their other record.

The same holds for every two-stage filter: blocking before matching, before , candidate generation before ranking, partition pruning before scanning. The discarded set is unrecoverable, so the cheap stage decides what can be found at all.

The standard escape is multi-pass blocking with several independent keys (postcode, phone prefix, phonetic surname). The price is a candidate set that grows with every pass.

Failure mode 1: transitivity and the percolation collapse

Pairwise matching is not transitive (A matches B, B matches C, A does not match C), yet any clustering step forces : it puts A, B and C into one group.

Suppose you build clusters as (everything linked, directly or through others, becomes one group). Then a single false link merges two clusters permanently. Now loosen the threshold slightly. The density of links crosses a , the way a few more open paths suddenly connect a whole grid: a giant component appears and swallows thousands of entities at once. That is Ravi's cluster of 2,300 people.

Quality does not degrade gradually as you loosen the threshold. So production systems cap cluster size, require several independent supporting links, or cluster with a method that does not take transitive closure.

Failure mode 2: the two errors are not equal

A missed merge leaves two records, which is easy to repair later. A wrong merge, if it is a destructive rewrite, destroys the information needed to undo it. So the converged design keeps resolution as an : a mapping from source records to entity IDs, with the source records kept untouched, instead of rewriting the source.

-- Overlay: sources stay as they are; the mapping can be revised or reversed.
CREATE TABLE customer_entity_map (
  source_system  VARCHAR,
  source_id      VARCHAR,
  entity_id      BIGINT,      -- the resolved real-world person
  matched_by     VARCHAR,     -- which rule or model version decided this
  matched_at     TIMESTAMP
);
-- To undo a bad merge: update the mapping rows. No source data was destroyed.

This is immutability buying reversibility, and also another case of "costs are conserved": the price is storage plus an extra join on every read, paid to keep a bad decision recoverable.

Compression: fuzzy matching is a two-stage filter. The cheap stage fixes recall forever, and the expensive stage only argues about .