2.4 索引与执行计划:看懂查询怎么跑


文档摘要

2.4 索引与执行计划 本节摘要:同一条查询,建不建索引、写法差一个等号,执行时间可能差三个数量级。本节教你用 PROFILE 打开引擎室:认识 db hits 与行数、区分全标签扫描与索引 Seek、学会建单属性与复合索引,并排查"索引明明建了却不走"的四类常见原因。这是全册调优能力的起点。 漫游技巧已经齐了,但一条查询在生产环境能不能站住,取决于引擎怎么执行它。本节把视角从"写查询"切换到"看查询"。 一、PROFILE:执行计划的第一层读法 在查询前加 ,引擎会真实执行并报告每一步的工作量: 没有索引时的关键输出(示意): 两个指标要盯:estimated rows(每步预估产出行数)与 db hits(访问存储的次数,粗略对应工作量)。

2.4 索引与执行计划

本节摘要:同一条查询,建不建索引、写法差一个等号,执行时间可能差三个数量级。本节教你用 PROFILE 打开引擎室:认识 db hits 与行数、区分全标签扫描与索引 Seek、学会建单属性与复合索引,并排查"索引明明建了却不走"的四类常见原因。这是全册调优能力的起点。

漫游技巧已经齐了,但一条查询在生产环境能不能站住,取决于引擎怎么执行它。本节把视角从"写查询"切换到"看查询"。

一、PROFILE:执行计划的第一层读法

在查询前加 PROFILE,引擎会真实执行并报告每一步的工作量:

PROFILE MATCH (p:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(m:Movie) RETURN m.title

没有索引时的关键输出(示意):

ProduceResults | +-- Projection | +-- Filter | +-- NodeByLabelScan (p:Person) estimated rows: 130 | +-- Expand( (p)-[:ACTED_IN]->(m) ) | +-- ... db hits 合计约 140+

两个指标要盯:estimated rows(每步预估产出行数)与 db hits(访问存储的次数,粗略对应工作量)。行数在某一层突然爆大,就是笛卡尔积或扫描失控的位置。

二、索引出手:NodeByLabelScan 变 NodeIndexSeek

为锚点属性建索引后重跑同一条查询:

// 建索引:Person 的 name 属性 CREATE INDEX person_name IF NOT EXISTS FOR (p:Person) ON (p.name)
CREATE INDEX 消耗 0 ms(后台构建,大图上会持续一段时间)
PROFILE MATCH (p:Person {name: 'Tom Hanks'})-[:ACTED_IN]->(m:Movie) RETURN m.title
ProduceResults | +-- NodeIndexSeek (p:Person, p.name = 'Tom Hanks') estimated rows: 1 | +-- Expand( (p)-[:ACTED_IN]->(m) ) db hits 合计约 12

图:一次锚点匹配的两种执行路径

图:一次锚点匹配的两种执行路径

db hits 从 140+ 掉到 12,而且不再随 Person 总数增长——这就是 1.1 说的"锚点查询规模化"的执行层证据。

三、索引家族:什么时候用哪把刀

索引类型 语法要点 适用场景 不适用
单属性索引 FOR (p:Person) ON (p.name) 等值与范围锚点 以通配开头的字符串匹配
复合索引 ON (p.name, p.born) 查询同时约束两列 只约束第二列(走不到)
全文索引 CREATE FULLTEXT INDEX ... FOR (n:Movie) ON EACH [n.title, n.plot] 模糊搜索、多字段检索 精确等值(杀鸡用牛刀)
存在性约束(企业版) IS NOT NULL 保证属性必填 社区版不可用

建索引前先问一句:这个属性真的会被当锚点吗? 只有出现在模式内等值或范围谓词里的属性才值得建;被 WHERE 后置过滤的属性建了也是白占写入预算。

四、"索引不生效"排查四连

实践中"建了索引却还是慢"几乎都落在这四类:

// 原因一:谓词写成了函数包裹——优化器无法下推 MATCH (p:Person) WHERE toLower(p.name) = 'tom hanks' // 改法:数据规范化入图,查询直接等值匹配 // 原因二:锚点属性不在索引列上(typo 或大小写不一致) MATCH (p:Person {Name: 'Tom Hanks'}) // 属性名是 name 不是 Name // 原因三:走的是复合索引但只给了第二列 // ON (p.name, p.born) 只按 born 匹配 → 索引弃用 // 原因四:OR 连接了两个不同标签的锚点,计划器放弃 Seek // 改用 UNION 或拆两条查询

⚠️ 索引不是免费的:每个索引都在拖慢写入并占用存储。给"读多写少"的锚点属性建,别照着关系库的惯性把所有外键都建一遍——图里没有外键,只有关系。

五、db hits 到底怎么算

调优时你会在 PROFILE 里反复见到 db hits 这个词。它的口径值得说透:一次 db hits 大致对应一次存储记录的访问——读一个节点记录、翻一条关系、取一块属性,各记一次。由此能推演出几条实用的估算直觉:

NodeByLabelScan 10 万节点 → 至少 10 万级 hits NodeIndexSeek 命中 1 节点 → 个位数 hits Expand 一步 → 命中几个邻居就加几次 hits 属性取值 → 每个属性再计入若干 hits

所以"行数少但 hits 高"的计划同样可疑——常见原因是 RETURN * 取回了一堆用不上的属性。按需取列,这个关系库时代的朴素习惯在图上同样省钱。

六、调优的前后对账

把本节方法落成一个固定动作:任何上线查询,保留调优前后的两份 PROFILE。对账时看三个数——总 hits、最大行数节点、是否出现 NodeByLabelScan:

调优前:db hits 2,340,000;NodeByLabelScan 8 万行 调优后:db hits 156;NodeIndexSeek 1 行 结论:建 person_email 索引 + 去掉 RETURN *

这份对账记录既是团队的性能资产,也是回滚时的依据——优化改坏了什么,前一份计划立刻能告诉你。

本节要点回顾

  • PROFILE 看两样东西:行数突变处是问题位置,db hits 是工作量账单;
  • NodeByLabelScanNodeIndexSeek 是慢与快的分水岭,等值锚点是 Seek 的门票;
  • 索引四兄弟各管一段:等值/范围、组合条件、模糊检索、必填校验;
  • 函数包裹、属性名 typo、复合索引只给后列、OR 跨标签——四类高频失效原因;
  • 索引预算留给真正的锚点,写入性能同样值钱。

读侧到本节收官。下一章转入写侧:图怎么长出来、怎么保住正确性。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U