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

PostgreSQL仅索引查询中共享缓冲区读取与堆扫描异常排查

PostgreSQL索引仅扫描异常问题

环境信息

  • PostgreSQL版本:13.10(AWS RDS)

仅索引查询语句

explain
  (analyse, verbose, buffers)
SELECT
  block_timestamp
FROM
  archive_tokentransfer
WHERE
  (
    from_addr = 72387
    AND block_timestamp >= 0
  )
order by
  block_timestamp
LIMIT
  10000

查询执行计划

"QUERY PLAN"
"Limit  (cost=0.71..4279.42 rows=10000 width=8) (actual time=0.483..60.586 rows=10000 loops=1)"
"  Output: block_timestamp"
"  Buffers: shared hit=4909 read=1659 written=200"
"  I/O Timings: read=37.730 write=13.824"
"  ->  Index Only Scan Backward using token_transfer_from_addr on public.archive_tokentransfer  (cost=0.71..245647.39 rows=574114 width=8) (actual time=0.482..59.544 rows=10000 loops=1)"
"        Output: block_timestamp"
"        Index Cond: ((archive_tokentransfer.from_addr = 72387) AND (archive_tokentransfer.block_timestamp >= 0))"
"        Heap Fetches: 1145"
"        Buffers: shared hit=4909 read=1659 written=200"
"        I/O Timings: read=37.730 write=13.824"
"Planning:"
"  Buffers: shared hit=235 read=7"
"  I/O Timings: read=0.036"
"Planning Time: 29.771 ms"
"Execution Time: 61.209 ms"

最近VACUUM信息

[
  {
    "schemaname": "public",
    "relname": "archive_tokentransfer",
    "last_vacuum": null,
    "last_autovacuum": "2023-10-19 02:51:55.087541+00",
    "last_analyze": null,
    "last_autoanalyze": "2023-11-06 18:53:41.891353+00"
  }
]

问题描述

针对from_addr = 72387的仅索引查询,获取10000行数据却需要遍历约6000个缓冲区,远超估算值:

  • 索引结构包含3个4字节整数、2个8字节大整数,单行长约28字节加少量开销,按索引总大小计算平均每行约50字节;PostgreSQL每页8KB,半满情况下每页可存约100行,10000行仅需约100页。
  • 表更新操作极少,但堆扫描(Heap Fetches)数约1000,数值偏高。

已尝试操作

  • 执行reindex concurrently重建索引:索引大小从1200GB降至850GB,但缓冲区读取量和堆扫描数无变化。
  • 测试其他from_addr值:即使对应行数超过10000,缓冲区读取仅约100,堆扫描数为0,无异常。
  • 删除并重新插入相关行:缓冲区命中数从6000降至3500,但堆扫描数从975升至10001。

表结构

devapp=> \d+ archive_tokentransfer;
                                                          Table "public.archive_tokentransfer"
      Column       |       Type       | Collation | Nullable |                      Default                      | Storage  | Stats target | Description 
-------------------+------------------+-----------+----------+---------------------------------------------------+----------+--------------+-------------
 id                | bigint           |           | not null | nextval('archive_tokentransfer_id_seq'::regclass) | plain    |              | 
 block_timestamp   | bigint           |           | not null |                                                   | plain    |              | 
 value             | double precision |           | not null |                                                   | plain    |              | 
 value_usd         | double precision |           |          |                                                   | plain    |              | 
 block_number      | integer          |           | not null |                                                   | plain    |              | 
 tx_from_address   | integer          |           | not null |                                                   | plain    |              | 
 tx_to_address     | integer          |           | not null |                                                   | plain    |              | 
 emitting_contract | integer          |           | not null |                                                   | plain    |              | 
 from_addr         | integer          |           | not null |                                                   | plain    |              | 
 to_addr           | integer          |           | not null |                                                   | plain    |              | 
 chain_id          | smallint         |           | not null |                                                   | plain    |              | 
 tx_idx            | smallint         |           | not null |                                                   | plain    |              | 
 type              | smallint         |           | not null |                                                   | plain    |              | 
 pricing_strategy  | smallint         |           | not null |                                                   | plain    |              | 
 token_id          | bytea            |           |          |                                                   | extended |              | 
Indexes:
    "archive_tokentransfer_pkey" PRIMARY KEY, btree (id)
    "block_data_idx" btree (chain_id, block_number, tx_idx)
    "token_transfer_from_addr" btree (from_addr, block_timestamp DESC) INCLUDE (to_addr, emitting_contract)
    "token_transfer_nft_transfers" btree (emitting_contract, token_id, block_timestamp DESC) INCLUDE (from_addr, to_addr, value) WHERE token_id IS NOT NULL
    "token_transfer_participants" btree (emitting_contract, block_timestamp DESC) INCLUDE (from_addr, to_addr)
    "token_transfer_to_addr" btree (to_addr, block_timestamp DESC) INCLUDE (from_addr, emitting_contract)
    "token_transfer_value_usd" btree (block_timestamp, value_usd DESC) INCLUDE (from_addr, to_addr, emitting_contract) WHERE value_usd IS NOT NULL
Check constraints:
    "archive_tokentransfer_chain_id_check" CHECK (chain_id >= 0)
    "archive_tokentransfer_pricing_strategy_check" CHECK (pricing_strategy >= 0)
    "archive_tokentransfer_tx_idx_check" CHECK (tx_idx >= 0)
    "archive_tokentransfer_type_check" CHECK (type >= 0)
    "no_cross_chain_data_bt" CHECK (NOT chain_id = 0)
    "one_fungibletoken_only_bt" CHECK (NOT (token_id IS NOT NULL AND type = 4))
    "only_allow_token_address_types_bt" CHECK (type = ANY (ARRAY[4, 5, 6]))
Access method: heap

问题分析与解决方案

异常原因

  1. 索引数据分布极度分散
    重建索引虽降低了总大小,但from_addr=72387的索引条目可能因初始插入顺序混乱、多次删除/插入操作,导致分散在大量索引页中,远超出正常的页密度,因此需要读取更多缓冲区。

  2. 可见性映射未及时更新
    仅索引扫描依赖visibility map(可见性映射)判断数据是否可见,若映射未标记对应堆页为全可见,PostgreSQL必须去堆中验证数据可见性,从而产生Heap Fetches:

    • 自动VACUUM距今时间过久,该范围堆页的可见性映射未更新;
    • 删除重插操作产生大量新堆元组,旧元组未被清理,导致可见性映射无法标记为全可见,反而加剧堆扫描。

解决步骤

  1. 手动执行VACUUM ANALYZE
    强制更新可见性映射并收集最新统计信息,建议在低峰期操作:

    VACUUM ANALYZE public.archive_tokentransfer;
    
  2. 创建部分索引优化目标查询
    针对from_addr=72387创建专属部分索引,大幅提升查询效率:

    CREATE INDEX token_transfer_from_addr_72387 ON public.archive_tokentransfer (block_timestamp DESC) INCLUDE (to_addr, emitting_contract) WHERE from_addr = 72387;
    
  3. 检查索引分布与空闲空间
    验证索引碎片化情况:

    -- 查看索引扫描统计
    SELECT idx_scan, idx_tup_read, idx_tup_fetch FROM pg_stat_user_indexes WHERE indexname = 'token_transfer_from_addr';
    -- 查看索引页空闲空间
    SELECT * FROM pg_freespace('token_transfer_from_addr') LIMIT 20;
    
  4. 调整自动VACUUM参数
    降低自动VACUUM的触发阈值,确保可见性映射及时更新:

    ALTER TABLE public.archive_tokentransfer SET (autovacuum_vacuum_scale_factor = 0.01);
    
  5. 重新验证执行计划
    完成上述操作后,重新运行EXPLAIN ANALYZE查询,观察缓冲区读取量和Heap Fetches是否下降。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.06 18:20:53