You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

PostgreSQL:为何仅索引扫描比索引扫描性能更慢?

问题背景

我们有一张表foo_tbl,DDL如下:

CREATE TABLE public.foo_tbl (
    id int8 NOT NULL,
    foo_id varchar(11) NOT NULL,
    foo_date timestamptz NULL,
    -- ... 其他无关列 ...

    CONSTRAINT pk_footbl PRIMARY KEY (id)
);
CREATE INDEX idx_1_2cols ON public.foo_tbl USING btree (foo_date, foo_id); -- 初始索引
CREATE INDEX idx_2_1col ON public.foo_tbl USING btree (foo_id); -- 查询变慢后新增的索引

有一个关联7张表的查询(示例简化),通过foo_id关联foo_tbl并获取foo_date字段:

select b.bar_code, f.foo_date from bar_tbl b join foo_tbl f on b.bar_id = f.foo_id limit 100;

未关联foo_tbl时查询耗时<2秒,关联后耗时>15秒——此时查询对foo_tbl使用idx_1_2cols执行仅索引扫描(Index Only Scan)(查询仅用到该表的foo_id和foo_date字段),对应的EXPLAIN ANALYZE结果:

{
  "Node Type": "Index Only Scan",
  "Parent Relationship": "Inner",
  "Parallel Aware": false,
  "Scan Direction": "Forward",
  "Index Name": "idx_1_2cols",
  "Relation Name": "foo_tbl",
  "Schema": "public",
  "Alias": "f",
  "Startup Cost": 0.42,
  "Total Cost": 2886.11,
  "Plan Rows": 1,
  "Plan Width": 20,
  "Actual Startup Time": 12.843,
  "Actual Total Time": 13.068,
  "Actual Rows": 1,
  "Actual Loops": 1200,
  "Output": ["f.foo_date", "f.foo_id"],
  "Index Cond": "(f.foo_id = (b.bar_id)::text)",
  "Rows Removed by Index Recheck": 0,
  "Heap Fetches": 0,
  "Shared Hit Blocks": 2284772,
  "Shared Read Blocks": 0,
  "Shared Dirtied Blocks": 0,
  "Shared Written Blocks": 0,
  "Local Hit Blocks": 0,
  "Local Read Blocks": 0,
  "Local Dirtied Blocks": 0,
  "Local Written Blocks": 0,
  "Temp Read Blocks": 0,
  "Temp Written Blocks": 0,
  "I/O Read Time": 0.0,
  "I/O Write Time": 0.0
}

创建单字段索引idx_2_1col后,查询恢复至<3秒,此时规划器选择新索引执行索引扫描(Index Scan),EXPLAIN ANALYZE结果:

{
  "Node Type": "Index Scan",
  "Parent Relationship": "Inner",
  "Parallel Aware": false,
  "Scan Direction": "Forward",
  "Index Name": "idx_2_1col",
  "Relation Name": "foo_tbl",
  "Schema": "public",
  "Alias": "f",
  "Startup Cost": 0.42,
  "Total Cost": 0.46,
  "Plan Rows": 1,
  "Plan Width": 20,
  "Actual Startup Time": 0.007,
  "Actual Total Time": 0.007,
  "Actual Rows": 1,
  "Actual Loops": 1200,
  "Output": ["f.foo_date", "f.foo_id"],
  "Index Cond": "((f.foo_id)::text = (b.bar_id)::text)",
  "Rows Removed by Index Recheck": 0,
  "Shared Hit Blocks": 4800,
  "Shared Read Blocks": 0,
  "Shared Dirtied Blocks": 0,
  "Shared Written Blocks": 0,
  "Local Hit Blocks": 0,
  "Local Read Blocks": 0,
  "Local Dirtied Blocks": 0,
  "Local Written Blocks": 0,
  "Temp Read Blocks": 0,
  "Temp Written Blocks": 0,
  "I/O Read Time": 0.0,
  "I/O Write Time": 0.0
}

核心疑问:为何此场景下索引扫描比仅索引扫描更快?仅索引扫描为何如此缓慢?

备注:

  • 执行EXPLAIN ANALYZE前已执行VACUUM ANALYZE
  • foo_tbl数据量仅数十万,关联的部分表数据量达数百万
  • 数据库为Amazon Aurora PostgreSQL兼容版13.5(非无服务器)
原因分析
  • 索引顺序不匹配查询条件,导致检索效率极低
    idx_1_2cols的索引顺序是(foo_date, foo_id),属于前缀索引——当查询仅以foo_id为条件时,数据库无法利用B树的有序性快速定位条目,只能遍历整个索引树中所有foo_date分组下的foo_id值,本质是全索引扫描+过滤。而idx_2_1col以foo_id作为唯一索引键,查询时可以直接通过B树快速定位匹配条目,检索效率差距巨大。

  • 缓存访问量的数量级差异
    从执行计划的Shared Hit Blocks可以看出:仅索引扫描命中了2284772个缓存块,而索引扫描仅命中4800个缓存块。前者需要访问的索引数据量是后者的数百倍,即使数据都在内存缓存中,大量的块访问也会带来显著的CPU开销和延迟,这是查询变慢的核心直接原因。

  • 仅索引扫描的“优势”未发挥,反而带来额外负担
    虽然查询用到的两个字段都在idx_1_2cols中,满足仅索引扫描的条件,但由于索引顺序不匹配查询逻辑,这个仅索引扫描和全索引扫描的执行逻辑并无区别——不仅没发挥仅索引扫描的优势,还因为索引包含两个字段,体积更大,带来了更多的数据访问量。

  • 循环执行的放大效应
    两个计划的Actual Loops都是1200次,意味着扫描逻辑会被重复执行1200次。仅索引扫描每次耗时约13ms,乘以1200次后总耗时就超过15秒;而索引扫描每次仅0.007ms,总耗时仅约8.4ms,加上其他关联逻辑的耗时,总查询时间就控制在了3秒内。


内容的提问来源于stack exchange,提问作者Tr1et

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 16:53:08