OPTIONAL MATCH后紧跟WHERE子句时,仅查询3结果异常的原因解析
测试数据构建
首先执行以下Cypher语句创建测试数据:
// create data WITH RANGE(1,5) AS ages UNWIND ages AS age CREATE(k:Kid{age:age}) RETURN k; MATCH(ks:Kid) WHERE ks.age > 2 WITH COLLECT(ks) AS ks UNWIND ks AS k FOREACH (n IN RANGE(1, k.age) | CREATE(k)-[:HAS]->(t:Toy{size:n})); // show all data MATCH(k:Kid) OPTIONAL MATCH(k:Kid)-[:HAS]->(t:Toy) RETURN *; // delete all data MATCH(k:Kid) OPTIONAL MATCH(k:Kid)-[:HAS]->(t:Toy) DETACH DELETE k, t RETURN *;
数据逻辑:创建5个年龄为1-5的Kid节点,其中年龄>2的Kid(3、4、5岁)会生成对应数量的Toy节点,Toy的size从1到对应Kid的年龄;1、2岁的Kid没有关联的Toy节点。
执行的3条查询及结果
查询1
MATCH(k:Kid) // query No.1 WITH k OPTIONAL MATCH(k:Kid)-[:HAS]->(t:Toy) WHERE t.size = 4 WITH k WHERE t.size = 4 RETURN COUNT(DISTINCT k);
返回结果:2(符合预期)
查询2
MATCH(k:Kid) // query No.2 WITH k OPTIONAL MATCH(k:Kid)-[:HAS]->(t:Toy) WHERE t.size IS NOT NULL WITH k WHERE t.size IS NOT NULL RETURN COUNT(DISTINCT k);
返回结果:3(符合预期)
查询3
MATCH(k:Kid) // query No.3 WITH k OPTIONAL MATCH(k:Kid)-[:HAS]->(t:Toy) WHERE t.size IS NULL WITH k WHERE t.size IS NULL RETURN COUNT(DISTINCT k);
返回结果:5(不符合预期,预期应为2)
核心疑问:当移除OPTIONAL MATCH后紧跟的WHERE子句时,查询1、2的结果不变,仅查询3的结果发生变化,为何会出现这种差异?
原因分析
这本质是**OPTIONAL MATCH中WHERE子句的过滤时机和属性为NULL的两种场景**共同导致的:
1. 明确两个关键场景
- 场景A:节点本身不存在(比如1、2岁的
Kid没有Toy,OPTIONAL MATCH匹配不到t,此时t的所有属性均为NULL) - 场景B:节点存在,但属性值确实为NULL(本次测试数据中无此类
Toy,所有Toy的size都是非NULL数值)
2. 拆解三个查询的逻辑
查询1的逻辑
OPTIONAL MATCH ... WHERE t.size =4:仅当Toy存在且size=4时,t才会被匹配到;其他情况(无Toy,或Toy存在但size≠4)的t均为NULL。
后续WHERE t.size=4只会保留t非NULL且size=4的Kid(即4、5岁的Kid),结果为2,符合预期。
即使移除OPTIONAL MATCH后的WHERE,逻辑仍一致——无Toy的t是NULL,size=4的判断会自动排除这些情况,结果不变。
查询2的逻辑
OPTIONAL MATCH ... WHERE t.size IS NOT NULL:仅当Toy存在且size非NULL时,t才会被匹配到;无Toy的Kid对应的t为NULL。
后续WHERE t.size IS NOT NULL保留的是有Toy的Kid(3、4、5岁),结果为3,符合预期。
移除OPTIONAL MATCH后的WHERE后,t要么是存在的Toy(size非NULL),要么是NULL,过滤后仍保留有Toy的Kid,结果不变。
查询3的逻辑
OPTIONAL MATCH ... WHERE t.size IS NULL:该过滤是在匹配阶段执行的——试图查找存在Toy节点且size属性为NULL的情况,但测试数据中根本没有此类Toy,因此所有Kid的OPTIONAL MATCH都匹配不到t,t全为NULL。
后续WHERE t.size IS NULL会保留所有Kid(包括1、2、3、4、5岁),因为t均为NULL,所以返回5。
而如果移除OPTIONAL MATCH后的WHERE子句,查询变为:
MATCH(k:Kid) WITH k OPTIONAL MATCH(k)-[:HAS]->(t:Toy) WITH k WHERE t.size IS NULL RETURN COUNT(DISTINCT k);
此时逻辑是:先匹配所有Kid的Toy(有Toy的t是Toy节点,无Toy的t是NULL),再过滤t.size IS NULL——这里的t.size NULL包含两种情况:t本身是NULL(1、2岁Kid),或t存在但size是NULL(测试数据中无此类情况)。最终保留的是1、2岁的Kid,结果为2,符合预期。
总结差异核心
查询1、2的WHERE条件在OPTIONAL MATCH阶段过滤的是存在且满足条件的节点,和后续过滤逻辑重叠;而查询3的WHERE t.size IS NULL在OPTIONAL MATCH阶段试图找存在但属性为NULL的节点,但测试数据中无此类节点,导致OPTIONAL MATCH全匹配失败,t全为NULL,后续过滤把所有Kid都纳入统计。移除OPTIONAL MATCH的WHERE后,才会将“没有Toy导致t为NULL”的场景纳入判断,得到正确结果。
内容的提问来源于stack exchange,提问作者norihide.shimatani

