Find connected groups of duplicate records in a CSV file using a tested Union-Find (Disjoint Set Union) implementation.
Two records belong to the same group when they share a non-empty value in the
same matching column. Connections are transitive: if record A matches B and B
matches C, all three records receive the same PARENT_ID.
- The default matching columns are
EMAIL,CARD, andPHONE. - Values only match within the same column.
- Leading and trailing whitespace is ignored.
- Empty values and the case-insensitive string
NULLare ignored. - Non-empty values are compared exactly and case-sensitively.
- The first record from each connected group becomes its representative.
- A representative points to itself.
- Input order and original ID values are preserved.
- IDs must be non-empty and unique.
For example:
1 matches 2 by CARD
2 matches 5 by PHONE
The connected group is {1, 2, 5}, and every record in it receives
PARENT_ID=1.
- PHP 8.3 or newer
- Composer
git clone https://github.com/MelnixDev/csv-duplicate-finder-php.git
cd csv-duplicate-finder-php
composer installWrite the result to the terminal:
php bin/find-duplicates.php examples/input.csvWrite the result to a file:
php bin/find-duplicates.php examples/input.csv output.csvShow help:
php bin/find-duplicates.php --helpindex.php remains available as a compatible entry point:
php index.php examples/input.csvThe CSV must have a header row containing ID, EMAIL, CARD, and PHONE.
Additional columns are allowed and ignored. Column order does not matter.
ID,PARENT_ID,EMAIL,CARD,PHONE,TMP
1,NULL,email1,card1,phone1,
2,NULL,email2,card1,phone2,
3,NULL,email3,card3,phone3,
4,NULL,email1,card2,phone4,
5,NULL,email5,card5,phone2,The parser supports standard quoted CSV values, including commas inside quoted fields, and accepts a UTF-8 BOM before the first header.
The output contains the original ID and the representative PARENT_ID:
ID,PARENT_ID
1,1
2,1
3,3
4,1
5,1See examples/input.csv and examples/expected-output.csv for the complete example.
Each record starts in its own disjoint set. For every matching column, the
finder remembers the first record seen for each value and performs union()
when that value occurs again. find() uses path compression, while union()
uses rank to keep the trees shallow.
The representative ID is tracked separately from the internal tree root, so union-by-rank does not change the rule that the first input record represents the group.
With a fixed number of matching columns, runtime is near-linear:
O(n α(n)), where α is the inverse Ackermann function. Memory usage is
O(n + u), where u is the number of unique matching values.
Run the test suite:
composer testRun static analysis:
composer analyseRun all checks:
composer checkThe regression suite covers transitive chains, independent group merges, non-sequential IDs, blank values, cross-column collisions, quoted CSV values, invalid headers, and terminal execution.
- Matching is case-sensitive after trimming whitespace.
- Only comma-delimited CSV is supported.
- The complete input is currently held in memory.
- The output intentionally contains only
IDandPARENT_ID.
This project is licensed under the GNU General Public License v3.0.