第 09 篇把 Expand 算子钉住。本篇回答:规划器如何挑算子树,以及为何每个算子「看起来合理」时整查询仍会爆。 核心量不是毫秒广告,而是 行(Rows)在算子之间如何增殖,以及 Estimated Rows 在幂律图上如何骗人。
本文是「图数据库内核」系列第 10 篇(共 16 篇)。→ 系列目录
篇目 核心内容 第 9 篇 · Expand 遍历算子语义 第 10 篇 · Cypher 计划 EXPLAIN/PROFILE、基数、计划事故 第 11 篇 · 事务与锁 并发下的另一类「跑不动」
版本锚定:Cypher Manual current / 5 Understanding query plans、Query tuning(Planner COST/IDP、
inferSchemaParts等)。基数管理实践交叉参考 Neo4j KB Tuning Cypher queries by understanding cardinality(B 级;适用版本标注偏 3.5–4.4,机制与 current 计划列一致处采用)。不粘贴本机未跑 PROFILE;手册示例数字仅作列形态说明。
一、查询生命周期:声明式 → 逻辑计划 → 物理执行
手册把一条 Cypher 分成:
- 解析声明式字符串;
- 规划器(optimizer)产出逻辑计划——在当前库状态(索引、约束、统计)下估计哪条执行路径更便宜;
- Runtime 把逻辑计划变成物理执行(如
PIPELINED)。
规划器默认 COST(与
planner=idp
同义):在受限搜索空间内找估计代价最低的计划。planner=dp
可穷尽搜索,规划时间可能显著变长——只在调计划本身时偶用。
统计与 schema 变了,缓存计划可能过时;可用
CYPHER replan=force EXPLAIN …
在低峰强制重规划(只规划不执行)。5.21+ 的
inferSchemaParts
控制是否从关系类型等推断标签以改进行估计——有助于「带依赖标签」(如每个
:Actor 也是
:Person)时的估计,细节见 Query
tuning。
二、怎么读计划表
2.1 EXPLAIN vs PROFILE
| EXPLAIN | PROFILE | |
|---|---|---|
| 是否执行 | 否 | 是(含写查询——会改库) |
| Estimated Rows | 有 | 有 |
| Rows / DB Hits / Memory 等 | 无 | 有 |
| 用途 | 看算子形状与估计 | 用真实行数对照估计、定位爆炸层 |
| 成本 | 低 | 更高;手册:非主动调优时不要随便 PROFILE |
2.2 自下而上
计划是算子的二叉树。表从上到下打印时,数据从叶子向上流:最底行是扫描/Seek,最顶是
ProduceResults。父算子吃子算子产出的行流。
2.3 必看列
| 列 | 含义(手册) | 调优用法 |
|---|---|---|
| Estimated Rows | 规划器估计本算子产出行数;用于选计划 | 与 Rows 比;来自标签/索引统计与选择性模型,无运行时反馈,可错——当提示而非测量 |
| Rows | 实际产出行数(PROFILE) | Support/KB:多数算子按行执行;Rows 尖峰强烈指示下游工作量 |
| DB Hits | 存储层低级访问计数(实体/属性/索引项等) | 一行可对应多次 hits;≠ Rows;反映存贮层干活量(接第 03–05 篇) |
| Page Cache Hits/Misses | EE;缓存命中 | 冷热缓存不可比(第 05 篇) |
| Memory | 算子峰值堆;页脚 total 是查询峰值 | 勿把各算子 Memory 简单相加 |
| Indexes Used | 用了哪些索引、访问次数 | 核对是否真走了你以为的索引 |
ProduceResults ← 读:结果
↑
Filter / Expand / …
↑
NodeIndexSeek / Scan ← 读:起点
三、基数:行流才是第一公民
KB 把 cardinality 定义为算子之间流动的行数。两条不变量:
- 算子对输入的每一行执行;
- 算子产出新的行流给父算子。
因此:
- 同一节点出现在 5 行上,再从该节点 Expand,可能把同一扩张做 5 遍;
- 两个独立匹配做成笛卡尔式行积后,
CREATE/MERGE会按行数重复(写错数据或白做功); WITH DISTINCT/ 聚合 / 早LIMIT(在合适位置)是重置基数的工具,不是 SQL 装饰。
与第 09 篇衔接:变长路径「按路径出行」时,Rows 可以远大于「唯一终点数」;Pruning + DISTINCT 同时改算子与基数叙事。
四、代价模型在赌什么——以及幂律如何作弊
规划器近似:
[ () _{} c()() ]
\(\widehat{|Rows|}\)
来自标签计数、索引选择性等统计,并把谓词选择性组合起来。工程实现里常见独立性假设(谓词/边彼此独立)——DeepWiki
等对源码的转述指向 AssumeIndependence…
一类模型;本篇把它标为机制方向,不以第三方转述当
A 级字面。
幂律图上的系统性偏差(本系列开放问题,第 01–02 篇已立靶):
| 估计用的「平均」 | 真实 |
|---|---|
| 标签上平均度数 \(\bar{d}\) | 少数点 \(d_{\max}\gg\bar{d}\) |
| 属性等值选择性 | 热点值极度倾斜 |
| 多跳 \(f^k\) | 路径上撞上超节点后一层炸穿 |
于是出现教科书事故:
- Estimated Rows 很小 → 规划器选 Nested Expand / 某 Join 顺序;
- 实际 Rows 在某一 Expand 飙升 → CPU、堆、DB Hits、page fault 一起爆;
- 每个叶子仍显示 IndexSeek——「用了索引」与「查询健康」无关。
这就是标题里的「看起来对但跑爆」:计划树局部都是合法算子,乘积被错估的基数撕开。
五、经典计划事故(对照前几篇)
5.1 高召回入口 × 深 Expand
第 07 篇:IndexSeek 产出巨大 \(|S|\)。
第 09 篇:每行再乘 \(f\)。
计划:Seek → Expand → Expand… 每步 Estimated
可能仍「乐观」。
对策:收紧谓词;早聚合/DISTINCT;降跳数;建模拆超节点——不是再加一个更宽的索引。
5.2 行积后的第二次 MATCH
MATCH (m)<-[:ACTED_IN]-(a) -- |rows| = #actors
MATCH (m)<-[:DIRECTED]-(d) -- 对每一行再匹配导演 → 行积
KB Movies 示例:同一 m 出现在多行时,从
m 再匹配会冗余执行。应用 collect /
pattern comprehension / WITH DISTINCT m
先把基数压回去。
5.3 Estimated ≪ Rows 的变长
无上界或上界过大的
*..k:估计按平均连通性,实际路径枚举指数涨。对照第
09 篇:加界、DISTINCT→Pruning、量化路径内谓词。
5.4 强迫错索引(USING)
USING INDEX
可覆盖规划器选择。若强迫低基数属性上的 Seek,入口 Rows
本身就大——提示成功、执行失败。以 PROFILE 的 Rows
为准,不以「用了索引」为准。
5.5 缓存与 PROFILE 误读
Page Cache Misses 高可能只是冷缓存(第 05 篇);重启后第一次 PROFILE 与稳态不可比。先看 Rows 爆炸层,再看 Misses。
5.6 EXPLAIN 好看、PROFILE 写爆堆
Estimated 全程个位数,实际中间路径物化把事务内存打满(第
05 篇「伪 I/O」)——扩 pagecache.size
无效,要砍行流或跳数。
六、调优检查单(计划层)
- EXPLAIN 看形状:起点是否 IndexSeek/合理 Scan?Expand 是 All 还是 Into?有无可怕的笛卡尔 Apply?
- PROFILE(只在调优时)找 Rows 第一处数量级跳变的算子——多半是事故点。
- 对比 Estimated Rows vs Rows:差几个数量级 → 不信任该计划的代价排序,用查询重写/提示/统计新鲜度干预。
- 用 聚合 / DISTINCT / 早 LIMIT(注意 LIMIT 在写操作之后的惰性语义,KB 有警告)管理基数。
- 变长:上界 + 是否只要终点(第 09 篇)。
- 统计与索引变更后 replan;大变更后不要死抱旧计划缓存。
- DB Hits 高但 Rows 不高 → 回第 03–05 篇(指针追逐、属性链、冷页);Rows 高 → 先留在本篇砍基数。
七、和系列坐标系闭合
| 篇 | 计划层对应 |
|---|---|
| 02 代价模型 | \(\widehat{f}\) vs 真 \(f\)、\(d_{\max}\) |
| 07 索引 | 叶子算子与 \(|S|\) |
| 08 语义索引 | 过程/SEARCH 结果再进 MATCH 时的行流 |
| 09 Expand | 行增殖的主要机器 |
| 05 page cache | PROFILE 的 Hits/Misses;次于 Rows |
| 11 锁 | 计划「快」但仍等待——另一诊断轴 |
八、争论与开放问题
- 代价规划 vs 提示/重写:A 侧信任统计 + COST;B 侧在图负载上默认不信任平均度数。本系列立场:用 Estimated↔︎Rows 落差决定信任度,而不是站队。
- 独立性假设:谓词相关(标签蕴含、类型与度数相关)时估计偏乐观或悲观都常见;
inferSchemaParts是产品侧补丁之一,不是银弹。 - 开放问题:面向幂律的度数直方图是否进入官方基数模型;并行/流水线 runtime 下 Rows 与 Time 列的解读(手册:融合管道共享 Time);与 GDS 分析计划的边界。
谱系:代价优化来自数据库查询处理传统(System R 以降);图模式匹配把「连接」换成拓扑扩张后,同一套基数语言、更歪的数据分布——争论从「会不会用索引」移到「敢不敢信 \(f^k\)」。
九、来源与实验台账(本篇)
| 结论 | 等级 | 来源 |
|---|---|---|
| 查询生命周期;EXPLAIN/PROFILE;列语义;自下而上读法;估计无运行时反馈 | A | Cypher Manual Understanding query plans |
| COST/IDP/DP;replan;inferSchemaParts(5.21+) | A | Query tuning |
| 按行执行、行积、DISTINCT/聚合管理基数、变长与 Pruning 条件实践 | B | KB understanding-cypher-cardinality(与第 09 篇手册 Pruning 交叉) |
| 幂律下估计失真 | 工程判断 / 开放问题 | 标明非官方定理 |
| 本机 PROFILE 案例 | 未跑 | 不伪造 Estimated/Rows 表 |
十、小结
- 计划为估计代价而选;执行按真实行流付钱。 调优看 Rows,不看「有没有 IndexSeek」奖杯。
- Estimated Rows 可错——尤其在超节点与深路径上;与 Rows 的落差就是诊断信号。
- 基数管理(DISTINCT、聚合、早限流、查询重排)与第 09 篇算子选择是同一件事的两面。
- 下一篇:当计划与行流都健康,仍慢或卡住时——锁、隔离与关系写入的图特有点。
→ 系列目录 · 上一篇:Expand · 下一篇:事务与锁
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【图数据库内核】图引擎全景:属性图作为一等公民的存储与遍历
定位属性图相对行存/LSM/向量/GraphRAG 的生态位;钉住邻接代价、Neo4j store format(record→block)、page cache、索引与 Expand、Cypher 计划五条坐标系,并以 Angles & Gutierrez 谱系与原生图争论收束系列路线。
【图数据库内核】标签、类型与属性索引:起点过滤器,不是遍历引擎
钉住 Neo4j 5 搜索性能索引(LOOKUP / RANGE / TEXT / POINT)与约束(唯一、存在、类型、KEY)如何决定「从哪里开始走」;对照 token lookup、过索引写放大,以及索引起点 × 深度 expand 的经典事故。
【图数据库内核】遍历与 Expand:从索引起点走到邻居的执行骨架
拆解 Cypher 遍历算子 Expand(All/Into)、OptionalExpand、VarLengthExpand(All/Into/Pruning)与最短路径双向 BFS;钉住路径爆炸、DISTINCT 剪枝、量化路径谓词,以及 expand 如何吃第 03–05 篇的 store 与 page cache。
【图数据库内核】属性图 · Store Format · Expand · Cypher 计划边界
补齐站内缺失的图拓扑存储与遍历内核:属性图模型、Neo4j record/aligned/block 布局、page cache 与 dense 节点、标签索引、Expand 与 Cypher 计划边界,以及 TinkerPop/JanusGraph 对照与 GraphRAG 接口。