Skip to content

3.8 Existence and intersection across sources (EXISTS / NOT EXISTS / OR)

MATCH (me:AssistantUser)-[:EMAILED]->(p:Person)
WHERE EXISTS { (p)-[:HAS_GITHUB]->(:GitHubIdentity) }
AND EXISTS { (p)-[:HAS_HUBSPOT_CONTACT]->(:HubSpotContact) }
RETURN p.name

Means: “People I email who are both on GitHub and in HubSpot.”

How it executes:

  • The engine sees the two virtual labels inside the EXISTS subqueries and materializes both, anchored on each emailed person p. (The edges inside EXISTS/NOT EXISTS/OPTIONAL/COUNT/ COLLECT are treated as non-required — they do not narrow which anchors get probed, but their targets are materialized so the existence test can be evaluated.)
  • After materialization, EXISTS { … } is a normal Cypher test: keep only people who got both a GitHubIdentity and a HubSpotContact materialized.
  • What it costs. An existence test needs one record per key, so the fetch behind it is capped at one. Whether that is one call per key depends on the source: one that pages is asked one key at a time, because a batched page walk would stop at page one and read the keys not on it as empty; one that does not page answers a batch completely under the cap, so its keys go in batches of maxKeysPerCall (three hundred anchors, six calls of fifty). batchSafe: false keeps one key per call either way (§3.5).

The same mechanism powers NOT EXISTS (“in HubSpot but not on GitHub”), OR (“on GitHub or in HubSpot”), and COUNT { … }. Express existence with EXISTS { }, not an OPTIONAL MATCH … WHERE x IS NOT NULL (which does not filter) and not a self-referential round-trip (p)-[:R]->(x)<-[:R]-(p) (which matches nothing under Cypher’s relationship-uniqueness rule).