Relix

Problem solving

Things that belong together

Grain: one row per member, labelled with its group · Class: Graph · Signals: connected, linked, same as, belong together, duplicates, rings, households · Operators: ⨝, ∪, CLUSTER

The problem

"Our customer table has duplicates. Two records are the same customer if they share an email address or a phone number. Which records belong together?"

How to recognise it

The question says same as, linked, belong together, duplicates, rings or households, and the rule for "together" is about pairs: two records are together if something connects them. What makes it a graph problem rather than a join is the chain: if A is linked to B and B to C, then A and C are together even though nothing connects them directly. A join finds the links; only a traversal finds the groups.

This is a two-step recipe, and it is the same two steps whatever the domain:

  1. Links. Write the rule for "these two are connected" as a join, and keep one row per connected pair.
  2. Groups. Hand the pairs to CLUSTER, which follows every chain and labels each member with its group.

The data

Customers := [
| id | name       | email       | phone    |
|----|------------|-------------|----------|
| 1  | Ann Lee    | ann@x.com   | 555-0101 |
| 2  | A. Lee     | ann@x.com   | 555-0199 |
| 3  | Annie Lee  | annie@y.com | 555-0199 |
| 4  | Bob Ray    | bob@z.com   | 555-0300 |
| 5  | Robert Ray | rray@z.com  | 555-0300 |
| 6  | Cy Tan     | cy@w.com    | 555-0400 |
];

Records 1 and 2 share an email, 2 and 3 share a phone, 4 and 5 share a phone, and 6 shares nothing.

Join the table to a renamed copy of itself. Customers.id < Other.id keeps each pair once, and stops a record linking to itself.

Query
Other := { ρ Other (Customers) };

Links := {
    π Customers.id → a, Other.id → b (
        Customers ⨝ Customers.id < Other.id
                  ∧ (Customers.email = Other.email ∨ Customers.phone = Other.phone)
        Other)
};
query { Links };
Result
 a  b
 ─  ─
 1  2
 2  3
 4  5
(3 rows)

Step 2: the groups

CLUSTER only sees records that appear in a link, so record 6 would disappear. Linking every record to itself keeps the ones that match nothing as groups of one.

Query
SelfLinks := { π id → a, id → b (Customers) };

Entities := { CLUSTER a, b AS entity_id (Links ∪ SelfLinks) };
query { Entities };
Result
 a  entity_id
 ─  ─────────
 1          1
 2          1
 3          1
 4          2
 5          2
 6          3
(6 rows)

Records 1 and 3 are in the same entity although they share neither an email nor a phone. That is the chain through record 2, and it is the reason to use CLUSTER rather than the join alone.

Using the answer

Put the names back, and count how many records each entity has:

Query
Grouped := {
    τ entity_id, id (
        π entity_id, id, name (Entities ⨝ Entities.a = Customers.id Customers))
};
query { Grouped };
Result
 entity_id  id  name
 ─────────  ──  ──────────
         1   1  Ann Lee
         1   2  A. Lee
         1   3  Annie Lee
         2   4  Bob Ray
         2   5  Robert Ray
         3   6  Cy Tan
(6 rows)
Query
EntitySizes := { γ entity_id, COUNT(a) → records (Entities) };
query { σ records > 1 (EntitySizes) };
Result
 entity_id  records
 ─────────  ───────
         1        3
         2        2
(2 rows)

Pitfalls

Check it