Finding Near-Duplicate Rows in Your Data
You pull a customer report and notice something off. The total customer count is 4,200, but your billing system says 3,800. You start scrolling and there they are: "Sarah Chen" and "sarah chen" and " Sarah Chen". Same email on all three rows, same signup date, same lifetime value. One account, counted three times.
Exact duplicates are easy - run Remove Duplicates and you're done. Near-duplicates are
trickier. The rows aren't identical. They just look the same to a human but not to a computer.
A basic DISTINCT won't catch them.
The usual suspects
Near-duplicates show up in predictable ways:
- Case differences: "TechWave Solutions" vs "Techwave Solutions" vs "TECHWAVE SOLUTIONS"
- Extra whitespace: " John Smith " vs "John Smith" vs "John Smith"
- Trailing characters: "Acme Corp." vs "Acme Corp" vs "Acme Corp, Inc."
- Abbreviations: "St." vs "Street", "NY" vs "New York"
The first two categories - case and whitespace - account for probably 80% of near-duplicate issues. And they're the easiest to fix.
One customer record, entered four times. Same email, same signup date, same lifetime value on every row:
| customer_name (raw) | signup_date | lifetime_value | |
|---|---|---|---|
| Sarah Chen | s.chen@northwind.co | 2024-03-11 | $12,400 |
| sarah chen | s.chen@northwind.co | 2024-03-11 | $12,400 |
| SARAH CHEN | s.chen@northwind.co | 2024-03-11 | $12,400 |
| Sarah Chen | s.chen@northwind.co | 2024-03-11 | $12,400 |
A GROUP BY on customer_name reports four customers worth $49,600. There is one customer worth $12,400. The repeated email and date are what make this safe to collapse.
Start with the built-in near-duplicate finder
Before you build anything by hand, look at what the app already found. Click the green + in the Pipeline panel and select Bulk Replace from the Transform group. Pick the column, then switch from All Values to the Similar Suggestions tab.
That tab runs DuckDB's editdist3
(Levenshtein distance) over the distinct values in the column, lowercased, and groups any two values
within one character edit of each other. "Techwave" and "TechWave" pair up because lowercasing makes
them identical. "Nothwind" and "Northwind" pair up because they differ by one insertion. Each group
comes with an + Accept button that drops it straight into the
Groups list, and applying the step rewrites every member to the group's label.
Two limits worth knowing before you rely on it. It samples up to 500 distinct values, so on a high-cardinality column it sees a slice, not everything. And one edit is a tight threshold on purpose: " Sarah Chen " is four edits away from "Sarah Chen", so padding and double spaces slip past it, and so does "Acme Corp." vs "Acme Corporation". Normalize whitespace first and the suggestions get noticeably better.
One more detail: the label on an accepted group is one of the group's own values, whichever the grouping picked, and there is no rename box. If you want a specific canonical spelling, delete the accepted group, go back to the All Values tab, tick the variants yourself, type the name you want and hit + Create.
The strategy: normalize, then deduplicate
You can't deduplicate on the original column because the values aren't equal. So create a normalized copy of the column, clean it up, and deduplicate on that instead.
Here's the approach in ExploreMyData:
Step 1: Copy the column
Use Copy Columns from the Columns group to duplicate the column you want
to match on. If your column is customer_name,
name the copy match_key.
That copy is what we'll flatten. The original stays untouched so you keep the real name.
Step 2: Trim, then lowercase
Text Transform applies exactly one transform per step, so this is two
steps, not one. Add a Text Transform on match_key
with "trim" to strip leading and trailing whitespace, then add a second one on the same column with
"lowercase".
Each step rewrites the column in place, so what you see in the pipeline is
TRIM(match_key) followed by
LOWER(match_key). Written as a single
expression that composes to
LOWER(TRIM(match_key)), with TRIM on the
inside because it ran first. For these two the order happens not to matter, but get in the habit of
reading the composition inside out, because for most pairs it does.
After both steps, all of these become the same value:
- "Sarah Chen" → "sarah chen"
- "SARAH CHEN" → "sarah chen"
- " Sarah Chen " → "sarah chen"
Step 3: Collapse the internal spaces
Trim only handles the ends. "Sarah Chen" with two spaces in the middle is still a different
string from "Sarah Chen" with one. Use Find & Replace on
match_key. Find
(two spaces) and replace with
(one space). Leave "Entire cell"
unticked, since you want a substring match here.
Under the hood: REPLACE(match_key, ' ', ' ')
If your data might have three or more consecutive spaces, apply this step twice. The first pass turns triple spaces into doubles, the second pass collapses those to singles.
Both columns side by side, original preserved and the flattened copy used for matching:
| customer_name (original) | match_key (normalized) |
|---|---|
| Sarah Chen | sarah chen |
| sarah chen | sarah chen |
| SARAH CHEN | sarah chen |
| Sarah Chen | sarah chen |
| Acme Corp. | acme corp. |
| ACME CORP | acme corp |
The four Sarah Chen rows now share one key. The two Acme rows still don't, because of that trailing period. That's the next section.
Step 4: Deduplicate on the normalized column
Now use Remove Duplicates from the Filter & Sort group. Select
match_key as the
column to check for duplicates.
ExploreMyData generates:
DISTINCT ON (match_key)
This keeps one row per unique normalized value and drops the rest. All four Sarah Chen spellings share a key, so one survives and the account stops being counted four times.
Step 5: Clean up
Once you're satisfied with the deduplication, drop the helper column. Use
Delete Columns to remove
match_key.
Your data now has one row per customer with the original formatting intact.
Going further with abbreviations
Case and whitespace normalization handles the majority of near-duplicates. But what about "Acme Corp." vs "Acme Corporation"? These need domain-specific replacements.
Before deduplicating, add Find & Replace steps on
match_key:
- Replace "corp." with "corporation"
- Replace "inc." with "incorporated"
- Replace "ltd." with "limited"
- Replace "st." with "street" (for address data)
Each replacement adds a pipeline step. The SQL stacks up:
REPLACE(REPLACE(col, 'corp.', 'corporation'), 'inc.', 'incorporated')
Because the column is already lowercased, you don't have to fight the "Case sensitive" checkbox, which is on by default. Do watch the substring behaviour though: replacing "st." also rewrites the middle of "west." If you want whole values swapped rather than fragments, that's a job for Bulk Replace, which matches the entire cell.
It's not fancy fuzzy matching, but it catches the patterns that actually appear in your data. Look at the duplicates you have, identify the specific variation patterns, and add targeted replacements. Practical beats clever.
Full pipeline for near-duplicate removal, seven steps, each targeting one cleanup concern:
| Step | Operation | Purpose |
|---|---|---|
| 1 | Copy Columns | Preserve the original; create match_key |
| 2 | Text Transform: trim | Remove leading and trailing whitespace |
| 3 | Text Transform: lowercase | Collapse case differences |
| 4 | Find & Replace: two spaces → one | Collapse internal whitespace |
| 5 | Bulk Replace (Similar Suggestions) | Fold in one-character spelling variants |
| 6 | Remove Duplicates (on match_key) | Keep one row per normalized name |
| 7 | Delete Columns: match_key | Remove the helper column |
Check before you delete
This is the part people skip. Matching names are not evidence of a duplicate. Four rows reading "TechWave Solutions" with four different contact emails are four contacts at one company, and Remove Duplicates would throw three of them away. Four rows for Sarah Chen with the same email, the same signup date and the same lifetime value are one record entered four times. Only the second case is safe.
The cheap check: run Group & Aggregate on
match_key with COUNT plus
COUNT DISTINCT on whatever identifies the entity, an email or an account id. If a group has four rows
and one distinct email, collapse it. Four rows and four distinct emails means keep them all, and what
you actually wanted was a cleaner company label, not fewer rows. In that case run the normalization
steps and stop before Remove Duplicates.