由 John Doe 十一月 18, 2025
PostgreSQL 的 EXPLAIN ANALYZE 输出新增了 Index Searches 行,可显示索引搜索次数的信息。

特性提交日志
在 EXPLAIN ANALYZE 中显示索引搜索次数。
在索引扫描/仅索引扫描/位图索引扫描节点的 EXPLAIN ANALYZE 输出中,暴露索引搜索/索引遍历的次数。该信息对于使用 ScalarArrayOp 条件的扫描尤为有用,由于与物理索引特性相互作用的实现细节,这类扫描的索引搜索次数可能难以预测。当 EXPLAIN ANALYZE 执行的计划中,索引扫描节点成功应用了跳过扫描优化时,所显示的信息也能提供有用的上下文。
该埋点实现的方式是:让所有索引访问方法(AM)在每次新索引搜索开始时,递增一个新的 nsearches 计数器。计数器的递增时机与索引访问方法当前递增pg_stat_*_indexes.idx_scan计数器的时机完全一致(我们统计的是同一事件,但粒度为扫描级而非关系级)。并行查询中,当索引扫描节点结束时,工作进程会将其本地计数器结构复制到共享内存中,即便该节点并非并行感知型扫描节点。
我们的方案与跟踪其他索引扫描相关开销(如 “Rows Removed by Filter:”)所采用的方案不同,但与类似场景(涉及仅能在访问方法内部而非执行器主体中轻松获取的开销)的方案具有可比性。例如,位图堆扫描的 “Heap Blocks:” 输出。索引访问方法必须直接负责维护这个新计数器,因为每个 amgettuple 调用(或 amgetbitmap 调用)可能需要多次递增计数器;同时,执行器主体也必须管理用于将每个工作进程的计数器结构传输到主进程的共享内存。
讨论:https://postgr.es/m/CAH2-WzkRqvaqR2CTNqTZP0z6FuL4-3ED6eQB0yx38XBNj1v-4Q@mail.gmail.com
示例
一般来说,最高效的扫描是对优化后的索引进行单次遍历,这样能最大限度减少缓冲区读取次数。
但为每个查询都创建优化的索引并不具备良好的扩展性,因为每个额外的索引都会带来代价,包括但不限于写入放大、HOT 更新失效(针对之前未索引的列)以及共享缓冲区(shared_buffers)空间竞争加剧。
因此,如果你正在优化一条重要查询,并且愿意为其创建和维护索引,那么 “Index Searches > 1” 通常意味着存在更优的解决方案。
我们来构造一个用例进行测试:
CREATE TABLE example (
integer_field bigint NOT NULL,
boolean_field bool NOT NULL);
INSERT INTO example (integer_field, boolean_field)
SELECT random () * 10_000,
random () < 0.5
FROM generate_series(1, 100_000);
CREATE INDEX bool_int_idx
ON example (boolean_field, integer_field);
VACUUM ANALYZE example;
我们创建了一个包含两列的表,插入 10 万行数据:其中一列基数极低(布尔值均匀分布),另一列基数高得多(0 到 10000 之间的随机整数)。
我们为这两列创建了索引,且布尔列在前(列顺序极其重要)。最后执行 VACUUM ANALYZE 更新可见性映射表,并收集统计信息。
现在,若运行仅过滤索引中第二列的查询,PostgreSQL 18 应该会通过“跳过扫描”生成效率高得多的查询计划。
首先在 PostgreSQL 18 中运行该查询,得到的查询计划如下:
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS)
SELECT boolean_field FROM example WHERE integer_field = 5432;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------
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
Planning Time: 0.240 ms
Execution Time: 0.323 ms
为什么索引搜索次数是 4 次?预期应该会是 2 次(分别对应 TRUE 和 FALSE 各一次)。根据性能邮件列表中提问,原来在一般情况下,必须考虑边界条件和空值(NULL),因此当这些情况无法排除时,可能会多 1 到 2 次索引搜索。
由于这些优化的灵活性很强,因此我们来尝试通过显式过滤仅保留 TRUE 或 FALSE 值,看看能否将索引搜索次数降至 2 次:
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS)
SELECT boolean_field FROM example WHERE integer_field = 5432
AND boolean_field IN (true, false);
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------
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 (''::boolean[])) AND (example.integer_field = 5432))
Heap Fetches: 0
Index Searches: 2
Buffers: shared hit=5
Planning Time: 0.265 ms
Execution Time: 0.115 ms
现在索引搜索次数正是我们期望的 2 次,缓冲区读取次数进一步减少到 5,执行时间也随之加快。
但是修改查询并不总是可行的。如果原始查询对业务负载至关重要,我们可能愿意为其创建优化索引以进一步提升性能。
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;
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------
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
Planning Time: 0.179 ms
Execution Time: 0.078 ms
通过调整新索引的列顺序,相关元组现在集中存放,因此扫描只需一次索引遍历(Index Searches: 1),最终实现了最少的缓冲区读取(3 次)和最快的执行速度。
参考
提交日志:https://git.postgresql.org/pg/commitdiff/0fbceae841cb5a31b13d3f284ac8fdd19822eceb