【图数据库内核】遍历与 Expand:从索引起点走到邻居的执行骨架
内容提要
本文介绍图数据库Cypher计划中Expand算子家族:单跳Expand(All)生成邻居、Expand(Into)连接已知两端,变长路径需设上界防爆炸,Pruning优化仅需唯一终点,最短路径用双向BFS而非变长枚举。强调存储耦合、谓词下推及实操要点,如避免超节点扫描、量化路径剪枝等。
延伸解读
Expand 算子与存储布局的耦合
Expand 算子的执行效率与底层存储布局紧密相关。类型已知时,dense 或 block dense 树可减少无关类型的读取;无类型、无方向的遍历会扫描该点全部邻接,在超节点上代价高昂。Expand(Into) 在两端已知时从度数较小的一端发起,可显著降低扫描量。理解这种耦合有助于在实际查询中利用类型和方向过滤,避免不必要的全邻接扫描。
变长路径的爆炸与 Pruning 的局限
变长路径若不设上界,路径数量可能按扇出的幂次爆炸。VarLengthExpand(Pruning) 优化要求有上界且不关心具体路径,只保证终点唯一,但它并非免费的闭包计算:在高度数、弱约束图上仍可能探索大量路径。因此,写查询时应明确上界,并在只需唯一终点时使用 DISTINCT,以触发 Pruning 优化。
最短路径与变长枚举的本质区别
最短路径查询使用双向 BFS 等专用算法,与枚举所有变长路径再取最短有本质区别。StatefulShortestPath(Into) 在两端已知时双向搜索,首次相遇即终止,复杂度远低于变长枚举。写最短路径查询时,应使用 shortestPath 或 SHORTEST 选择器,而非用无界变长匹配冒充,否则可能导致性能灾难。
Q&A
图数据库Cypher计划中,Expand(All)和Expand(Into)有什么区别?
Expand(All)用于从已知起点生成所有邻居,而Expand(Into)用于连接两个已知端点,枚举它们之间的边。Expand(Into)会从度数较小的一端发起,以避免扫描超节点。
变长路径查询为什么容易爆炸?如何避免?
变长路径查询如果不设上界或上界过大,路径排列组合会导致结果数量爆炸。避免方法是必须设置有限上界,并尽量使用量化路径模式将谓词下推到扩张过程中。
VarLengthExpand(Pruning)优化适用于什么场景?它有什么限制?
VarLengthExpand(Pruning)适用于不关心具体路径、只要唯一终点的查询,且关系模式有上界。它通过剪枝避免重复探索,但并非免费闭包,在高度数弱约束图上仍可能探索大量路径。
最短路径查询为什么不用变长路径匹配加LIMIT 1?
最短路径查询使用双向BFS等专用算法,复杂度远低于枚举所有路径再取最短。变长路径匹配的目标是模式枚举,而最短路径的目标是长度最优,两者复杂度阶级不同。
量化路径模式相比旧语法有什么优势?
量化路径模式(如(…){m,n})允许在重复段内写节点/关系谓词,从而更早剪枝,控制扩展成本。旧语法*min..max仍可用但非GQL对齐,且无法在段内声明谓词。
在Cypher计划中,如何判断是否使用了Pruning优化?
通过EXPLAIN查看计划中是否出现VarLengthExpand(Pruning)或VarLengthExpand(Pruning,BFS)算子。通常与RETURN DISTINCT等只要唯一终点的查询一起出现。
为什么说Expand(Into)比从超节点All扫出再过滤更优?
Expand(Into)直接从度数较小的一端发起,避免扫描超节点的全部邻接,从而减少无效扇出。而All扫出再过滤会白付扇出成本。
OptionalExpand与Expand有何不同?
OptionalExpand对应OPTIONAL MATCH,无匹配时仍产出一行,关系与终点为null。它不改变超节点上的读代价,只影响零匹配时是否丢行。