PostgreSQL ltree 中间通配符查询与前缀查询的性能有何差异?
结论
Main.*.Level3 仍然可以使用现有的 GiST 索引。中间出现 * 不会直接导致索引失效,但也不能保证它与 Main.Level1.* 一样快。
前缀查询的固定起始路径更长,通常更容易排除无关的索引分支;中间通配符查询也能利用固定标签进行过滤。哪一种更快,取决于标签分布、匹配行数和实际访问的索引页数,不能只看通配符的位置。
还需要纠正一个前提:PostgreSQL 自带的 ltree 扩展没有直接支持这些操作的 GIN operator class。这里讨论的是示例中的 USING GIST (path) 索引;如果使用第三方 GIN 扩展或其他数据表示,需要另行分析。ltree 索引文档
GiST 如何处理这两种查询?
ltree 的 GiST 实现在内部节点使用标签位图签名,并结合路径范围信息判断是否继续搜索。签名有助于排除不包含必要标签的分支,但不能单独证明标签的位置和顺序符合完整模式。
两种模式的区别在于:
Main.Level1.*:固定起始路径为Main.Level1,可以按这一前缀缩小范围。Main.*.Level3:固定起始路径只有Main,不过Level3仍可参与签名过滤,中间的*不会让后面的固定标签失去全部索引价值。
上述说明基于 gist_qe、gist_between 和 ltree_consistent 的实现。单值 ltree 在叶子项上会执行完整匹配。内部签名过滤虽然不精确,但这不意味着每个候选项都需要回表重新匹配。PostgreSQL ltree GiST 源码
由此可以推断:如果 Level3 出现在大量索引分支中,它的过滤价值可能有限,中间通配符查询就可能访问更多索引页;如果它很少出现,过滤效果可能很好。没有适用于所有数据集的固定性能倍数。
另外,这两个查询的结果集不同,也没有必然的包含关系。比较耗时时,需要同时看返回行数,避免把结果集大小造成的差异都归因于通配符。
如何验证实际性能?
在具有代表性的数据上更新统计信息,然后分别查看执行计划:
ANALYZE hierarchy;
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM hierarchy
WHERE path ~ 'Main.Level1.*'::lquery;
EXPLAIN (ANALYZE, BUFFERS)
SELECT *
FROM hierarchy
WHERE path ~ 'Main.*.Level3'::lquery;
重点比较以下信息:
- 是否出现使用
path_gist_idx的Index Scan或Bitmap Index Scan,以及对应的Index Cond。 - 实际返回行数与估算行数是否接近。
Buffers中的命中、读取数量,以及各执行节点的耗时。
在相近负载下交替执行几次,留意首次读取和缓存后的差异。EXPLAIN ANALYZE 会实际执行查询。没有真实执行计划和数据分布,无法确定扫描比例或耗时。即使计划选择 Seq Scan,也可能是匹配行数较多、顺序扫描成本更低,不代表该模式不能使用索引。EXPLAIN 使用说明
有哪些优化和语义注意事项?
如果确认签名过滤效果不足,可以在测试环境尝试增加 siglen,例如:
CREATE INDEX path_gist_sig100_idx
ON hierarchy USING GIST (path gist_ltree_ops(siglen=100));
较长签名能提高过滤精度,但也会增大索引,是否有收益需要实测。100 只是示例值。比较不同签名长度时,应确认执行计划实际使用了哪个索引。siglen 参数说明
还要确认查询范围符合业务要求:* 匹配零个或多个标签,因此 Main.*.Level3 也能匹配 Main.Level3;Level3 是末尾标签名称,并不表示它必须位于第三层。Main.Level1.* 包含该路径本身及任意深度的后代。如果只查询直接子节点,应使用 Main.Level1.*{1}。这些限制应按真实需求添加,不能为了提速改变查询含义。lquery 匹配规则
备注:内容仅供参考。
