You wrote MERGE precisely so you wouldn't get duplicates:
MERGE (p:Person {email: $email})-[:WORKS_AT]->(c:Company {name: $company})
A week later there are four Acme Inc nodes and three copies of some people. MERGE isn't broken. It does something different from what most people assume, and without a constraint it can't stop concurrent duplicates.
MERGE matches the whole pattern
MERGE means "match this entire pattern; if it doesn't exist as a whole, create all of it". For the query above:
- Alice already exists, and Acme already exists, but there's no
WORKS_ATrelationship between them. - The full pattern doesn't match, so Cypher creates the full pattern: a new Alice node, a new Acme node and the relationship.
That's the most common source of duplicates in Neo4j.
Fix: merge each node on its key, then merge the relationship between the bound nodes.
MERGE (p:Person {email: $email})
MERGE (c:Company {name: $company})
MERGE (p)-[:WORKS_AT]->(c)
Each node is matched or created on its own, and the last MERGE only has to create the relationship.
Merge on the key only, and set the rest
MERGE matches on every property you put in the pattern:
// Duplicates whenever the name changes or differs in any way:
MERGE (p:Person {email: $email, name: $name, updatedAt: $now})
A different name means no match, so another Alice gets created. Merge on the identifying key and set everything else:
MERGE (p:Person {email: $email})
ON CREATE SET p.name = $name, p.createdAt = datetime()
ON MATCH SET p.name = $name, p.updatedAt = datetime()
Also normalize the key before merging (toLower(trim($email))). Alice@Example.com and alice@example.com are different values to Cypher.
