找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1627

积分

0

好友

213

主题
发表于 14 小时前 | 查看: 2| 回复: 0

引用说明
原文作者 Michael Christofides
原文地址:https://www.pgmustard.com/blog/what-do-index-searches-in-explain-mean


你是否在升级到 PostgreSQL 18 后,发现 EXPLAIN ANALYZE 的输出中多了一个神秘的 Index Searches 字段?这个新增的统计项究竟揭示了什么秘密?是优化生效的信号,还是索引设计存在潜在问题的提示?本文将深入剖析这一新字段,结合具体示例,带你读懂其背后的执行逻辑与性能含义。

一、背景与概念:Index Searches 是什么?

从 PostgreSQL 18 开始,EXPLAIN ANALYZE 的输出中新增了 Index Searches 字段。简单来说,它记录了查询执行时,从索引的根节点向下进行的“下降(descent)”次数。每一次下降,都对应着引擎对索引树的一次定位操作。

最常见的情况:Index Searches: 1

Index Searches 值为 1 时,意味着优化器判断只需对索引进行一次下降,就能定位到所有满足条件的条目。这是最理想的情况吗?未必。如果所需的数据在索引中恰好紧密排列,那么一次下降确实高效。但如果符合条件的行分散在索引各处,那么即使只做一次下降,也可能需要在索引页中跳过大量无关数据,导致实际读取的缓冲区(Buffers)很多,效率低下。

Index Searches > 1 是怎么来的?

多个索引下降通常意味着查询涉及多个离散的值,而新的执行计划认为,为每个值单独做一次精准定位,比做一次“大扫荡”式的扫描更划算。

这源于 PostgreSQL 17 对 btree 索引的一项重要优化:对于 IN 子句这类多值查找,允许对同一索引进行多次独立的下降,每次精确定位一个值。这避免了在一次扫描中处理大量不满足条件的中间条目,显著提升了性能。PostgreSQL 18 更进一步,将这一机制扩展并固化为 skip scan(跳跃扫描) 功能,同时将执行情况通过 Index Searches 字段透明化。

二、官方示例解析:何时会触发多次下降?

让我们通过两个来自 PostgreSQL 官方文档的示例,直观感受 Index Searches 的作用。

示例 1:IN 子句触发的多值查找

考虑一个包含 IN 子句的查询:

EXPLAIN ANALYZE
SELECT * FROM tenk1 WHERE thousand IN (1, 500, 700, 999);

在 PostgreSQL 18 下,其执行计划可能如下:

                                                                 QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=9.45..73.44 rows=40 width=244) (actual time=0.012..0.028 rows=40.00 loops=1)
   Recheck Cond: (thousand = ANY (‘{1,500,700,999}’::integer[]))
   Heap Blocks: exact=39
   Buffers: shared hit=47
   ->  Bitmap Index Scan on tenk1_thous_tenthous  (cost=0.00..9.44 rows=40 width=0) (actual time=0.009..0.009 rows=40.00 loops=1)
         Index Cond: (thousand = ANY (‘{1,500,700,999}’::integer[]))
         Index Searches: 4        ← 4 个值,4 次索引下降
         Buffers: shared hit=8
 Planning Time: 0.029 ms
 Execution Time: 0.034 ms

说明IN (1, 500, 700, 999) 包含了 4 个离散值。因此执行器对索引进行了 4 次独立的下降,每次定位一个值。如果不这么做,而是进行一次大范围的扫描(从1扫到999),虽然 Index Searches 显示为 1,但需要读取的缓冲区会多得多。这个字段让原本“黑盒”的优化过程变得可见。

补充:你还可以通过系统视图 pg_stat_user_indexesidx_scan 列,观察到这些索引下降次数的累积统计。

示例 2:范围条件与 Skip Scan

这个例子展示了跳跃扫描在复合索引上的应用:

EXPLAIN ANALYZE
SELECT four, unique1 FROM tenk1
WHERE four BETWEEN 1 AND 3 AND unique1 = 42;

假设存在一个索引 (four, unique1),执行计划可能为:

                                                                  QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------
 Index Only Scan using tenk1_four_unique1_idx on tenk1  (cost=0.29..6.90 rows=1 width=8) (actual time=0.006..0.007 rows=1.00 loops=1)
   Index Cond: ((four >= 1) AND (four <= 3) AND (unique1 = 42))
   Heap Fetches: 0
   Index Searches: 3        ← four 的值 1、2、3,各下降一次
   Buffers: shared hit=7
 Planning Time: 0.029 ms
 Execution Time: 0.012 ms

说明:条件 four BETWEEN 1 AND 3 实际对应了三个值:1,2,3。由于 unique1=42 的行很可能分散在这三个 four 值对应的不同索引区域中,PostgreSQL 18 的 skip scan 机制会选择为每个 four 值执行一次独立的下降(共3次),直接定位 (four, unique1) = (1, 42)(2, 42)(3, 42) 可能的位置。这比从 four=1 扫到 four=3 并过滤掉所有 unique1≠42 的行要高效得多。

三、动手实验:从实践看 Index Searches 的影响

理论需要数据验证。下面我们通过一个完整的例子,演示不同索引设计和查询写法如何影响 Index Searches 及最终性能。

建表与准备数据

首先创建一个测试表并插入数据:

CREATE TABLE example (
    integer_field bigint NOT NULL,
    boolean_field bool NOT NULL
);

INSERT INTO example (integer_field, boolean_field)
SELECT
    random() * 10000,
    random() < 0.5
FROM generate_series(1, 100000);

-- 创建一个“非理想”的索引:将低基数的布尔列放在前面
CREATE INDEX bool_int_idx ON example (boolean_field, integer_field);

VACUUM ANALYZE example;

表结构说明:

  • 10万行数据。
  • boolean_field 基数很低,只有 true/false 两个值,各约5万行。
  • integer_field 基数高,是0-10000的随机整数。
  • 索引 bool_int_idx 将低基数列放在首位,这通常不利于仅按高基数列过滤的查询。

场景 1:PostgreSQL 17 下的“笨”扫描

在 PostgreSQL 17(不支持 skip scan)上运行一个仅按高基数列过滤的查询:

EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS)
SELECT boolean_field FROM example WHERE integer_field = 5432;

输出如下:

 Index Only Scan using bool_int_idx on public.example
   (cost=0.29..1422.39 rows=10 width=1) (actual time=0.579..1.931 rows=18 loops=1)
   Output: boolean_field
   Index Cond: (example.integer_field = 5432)
   Heap Fetches: 0
   Buffers: shared hit=168      ← 读了整个索引(168 × 8KB = 1344KB)
 Planning Time: 0.197 ms
 Execution Time: 1.976 ms

结果只返回了18行,却因为索引设计问题,被迫扫描了整个索引(读取168个缓冲区,约1.3MB),效率极低。此时还没有 Index Searches 字段。

场景 2:PostgreSQL 18 的 Skip Scan 优化

同样的查询,在 PostgreSQL 18 中运行:

EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS)
SELECT boolean_field FROM example WHERE integer_field = 5432;

输出发生了显著变化:

 Index Only Scan using bool_int_idx on public.example
   (cost=0.29..13.04 rows=10 width=1) (actual time=0.230..0.274 rows=5.00 loops=1)
   Output: boolean_field
   Index Cond: (example.integer_field = 5432)
   Heap Fetches: 0
   Index Searches: 4            ← 新出现的字段
   Buffers: shared hit=9        ← 从 168 降至 9
 Planning Time: 0.240 ms
 Execution Time: 0.323 ms       ← 执行时间大幅缩短

性能提升立竿见影:

  1. 缓冲区读取:从 168 个骤降至 9 个。
  2. 执行时间:从 1.976ms 减少到 0.323ms。
  3. 新增字段:出现了 Index Searches: 4

为什么是4次? 这涉及到边界条件和 NULL 值的处理。在执行 skip scan 时,为了确保逻辑的完备性,引擎可能需要为 true, false 以及可能的边界情况(如 -infinity, +infinity)分别执行下降。根据 PostgreSQL 核心贡献者 Peter Geoghegan 的解释,这会导致次数可能比预期的值(2个)多1到2次。

场景 3:显式枚举,引导优化器

如果我们明确告诉数据库 boolean_field 只可能取 truefalse,可以帮它做出更优决策:

EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS)
SELECT boolean_field FROM example
WHERE integer_field = 5432
AND boolean_field IN (true, false);

输出:

 Index Only Scan using bool_int_idx on public.example
   (cost=0.29..8.79 rows=10 width=1) (actual time=0.060..0.077 rows=12.00 loops=1)
   Output: boolean_field
   Index Cond: ((example.boolean_field = ANY (‘{t,f}’::boolean[])) AND (example.integer_field = 5432))
   Heap Fetches: 0
   Index Searches: 2            ← 精确枚举后降为 2 次
   Buffers: shared hit=5        ← 进一步减少
 Planning Time: 0.265 ms
 Execution Time: 0.115 ms

通过 IN (true, false) 显式枚举所有可能值,PostgreSQL 知道无需考虑其他边界,直接对这两个值各做一次下降即可。Index Searches 减少到 2,缓冲区和执行时间进一步优化。这个优化在 PostgreSQL 17 中就已引入。

场景 4:终极方案:设计最优索引

如果你的应用场景允许,为这个关键查询创建一个专用的、列顺序最优的索引,永远是性能提升的最佳途径。在 数据库 性能调优中,合理的索引设计是基础。

CREATE INDEX int_bool_idx ON example (integer_field, boolean_field);

EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS)
SELECT boolean_field FROM example WHERE integer_field = 5432;

输出:

 Index Only Scan using int_bool_idx on public.example
   (cost=0.29..4.47 rows=10 width=1) (actual time=0.042..0.047 rows=12.00 loops=1)
   Output: boolean_field
   Index Cond: (example.integer_field = 5432)
   Heap Fetches: 0
   Index Searches: 1            ← 单次下降,最优
   Buffers: shared hit=3        ← 只需 3 个缓冲区
 Planning Time: 0.179 ms
 Execution Time: 0.078 ms       ← 最快

将高基数的 integer_field 放在索引首位后,所有 integer_field = 5432 的行在索引中物理上紧密相邻。查询只需一次索引下降,读取最少的缓冲区(3个),获得了最快的执行速度。

四个场景性能对比总结:

场景 PostgreSQL 版本 Index Searches Buffers 执行时间
bool_int_idx,无显式条件 17 —(无此字段) 168 1.976ms
bool_int_idx,无显式条件 18(skip scan) 4 9 0.323ms
bool_int_idx,显式枚举 IN(true,false) 18 2 5 0.115ms
int_bool_idx(列顺序调换) 18 1 3 0.078ms

四、Index Searches 的实践意义与建议

看到 Index Searches 大于 1,我们该如何解读?

它本身不是绝对的“好”或“坏”指标。

  • 对于追求极致性能的关键查询:如果 Index Searches > 1,它提示你当前的索引可能不是最优设计。数据没有在索引中“共处一室”(colocated),迫使数据库执行多次查找。此时,考虑调整索引列顺序,让查询条件列前置,是实现 Index Searches: 1 和最佳性能的终极手段。
  • 对于日常查询或刚升级到 PG18Index Searches > 1 通常是 好事!它表明 PostgreSQL 正在利用 skip scan 或多值下降优化,智能地避免全索引扫描,用多次精准定位换取总体 I/O 的下降。这正是数据库自我优化的体现。

在实际的 索引优化 工作中,我们应综合看待这个新字段:

  1. 优化重要查询时:将 Index Searches 作为一个新的观察维度。结合 Buffers(缓冲区读取量)和 Rows Removed by Filter(过滤掉的行数)等传统指标,更全面地评估索引效率。
  2. 评估 PG18 升级收益:如果你的业务中有大量类似“场景1”的查询(用非首列条件过滤),可以在测试环境中验证 PG18 的 skip scan 能带来多大的性能提升。
  3. 审视索引策略:在排查和清理冗余索引时,可以将“列相同但顺序不同”的索引也纳入评估范围。思考在 PG18 skip scan 的加持下,是否有可能合并或删除某些索引,同时保证读性能不受影响。

Index Searches 字段的引入,是 PostgreSQL 朝着执行计划更透明、诊断信息更丰富方向迈出的又一步。它为我们深入理解查询如何与索引交互打开了一扇新的窗口。希望本文能帮助你更好地利用这个工具,优化你的数据库应用。如果你想与更多开发者交流数据库使用心得,欢迎来到 云栈社区 分享你的见解。

测试说明:本文所有示例均来自原文,实际结果会因 PostgreSQL 版本(Index Searches 字段仅在 PostgreSQL 18 及以上EXPLAIN ANALYZE 输出中出现,skip scan 功能也需要 PG18)、数据分布、统计信息更新情况、VACUUM 执行情况及硬件环境等因素而有所不同。建议在测试环境中使用 EXPLAIN (ANALYZE, BUFFERS, VERBOSE) 实际验证,不要将示例数值直接用于生产环境的性能判断。




上一篇:2026年手机市场迎来全行业涨价潮,存储成本暴涨是主因
下一篇:2025年美国电商市场分析:集中度加剧,亚马逊、Shopify双雄格局稳固
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-1 20:57 , Processed in 0.579169 second(s), 42 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表