索引是否真膨胀需综合dead_tuple_count与free_space判断,而非仅看bloat_ratio;pgstattuple2更准更快但需手动安装且不兼容原生接口;重建索引应依据pg_stat_all_indexes中idx_scan、idx_tup_read/fetch等运行时指标决策。

怎么看索引是否真膨胀了?别只盯 bloat_ratio
PostgreSQL 的索引膨胀不是靠肉眼判断的,pgstattuple 返回的 bloat_ratio 常被误读——它只是估算值,且对 B-tree 索引不直接反映“无效页”占比。真正关键的是 dead_tuple_count 和 free_space 的组合:如果一个索引长期写多读少(比如日志表的 created_at 索引),dead_tuple_count 持续上升而 free_space 却很低,说明旧元组没被 vacuum 清理干净,物理空间没回收。
-
pgstattuple对索引只支持 B-tree,GiST/GIN 不适用;想查 GIN 膨胀得用pgstatginindex - 执行
pgstattuple会拿AccessShareLock,不影响 DML,但大索引上可能卡住并发 vacuum,线上慎在高峰跑 - 别用
bloat_ratio > 0.3当硬阈值——小索引(1GB)0.1 就该介入
pgstattuple2 是什么?和原生 pgstattuple 差在哪
pgstattuple2 是社区扩展(非官方内置),核心区别是它绕过标准 tuple 扫描,直接读取底层页头和行指针,所以快、准、锁粒度更细。但它要求 PostgreSQL ≥ 12,且必须手动编译安装(CREATE EXTENSION pgstattuple2 后才能用)。
- 原生
pgstattuple查 10GB 索引常耗时 20+ 秒,pgstattuple2通常压到 3 秒内 -
pgstattuple2多返回page_skipped字段——表示跳过多少页(因 concurrent update 冲突),若该值 > 0,结果可信度下降 - 它不兼容
pgstattuple的函数签名:pgstattuple2('idx_name')不接受 OID 参数,也不支持 schema-qualified 名字带引号
写监控 SQL 时,怎么避开常见陷阱
直接 SELECT pgstattuple 结果进告警系统,很容易误报。根本原因是函数执行时机不可控,且不同索引的膨胀成因差异极大。
- 别在单次查询里 JOIN
pg_class+pg_index+pgstattuple—— 函数会被每行调用一次,1000 个索引就扫 1000 遍,CPU 爆掉 - 用物化视图或临时表缓存结果:先
SELECT indexrelid, (pgstattuple(indexrelid)).* INTO TEMP bloat_snapshot FROM pg_index WHERE indislive,再分析 - 排除系统索引:
WHERE NOT nspname = 'pg_catalog' AND NOT nspname = 'information_schema',否则pg_type_typname_nsp_index这种自带高 bloat_ratio 的索引天天触发告警 - 对写入频繁的分区表,按
relnameLIKE 'events_2024%' 过滤,别扫全库
什么时候该重建索引?看 pg_stat_all_indexes 的信号
pgstattuple 告诉你“有多肿”,但要不要动刀,得结合运行时行为。重点盯三个字段:idx_scan(多久没被用了)、idx_tup_read(每次扫描读多少行)、idx_tup_fetch(实际命中多少)。三者比值异常,才是重建的强信号。
-
idx_scan = 0且dead_tuple_count > 10000→ 索引已成僵尸,DROP 更划算 -
idx_tup_read / idx_tup_fetch > 5且bloat_ratio > 0.2→ 大量扫描落空,说明索引选择性崩了,VACUUM FULL或REINDEX可能有用 - 如果
pg_stat_all_indexes.last_scan超过 7 天没更新,别急着重建——先确认应用逻辑是否真弃用了这个索引
重建本身有代价:REINDEX INDEX 会阻塞写,CREATE INDEX CONCURRENTLY 不锁表但耗时翻倍,且失败后残留无效索引。真要动手,优先选低峰期 + 配合 pg_stat_progress_create_index 观察进度。










