PostgreSQL慢查询优化中,索引缺失是最常见的问题之一,但人工从执行计划里判断该建什么索引非常依赖经验。pgMustard能够把执行计划里的扫描节点、成本估算和统计信息转成具体索引建议,帮助定位全表扫描、排序溢出和过滤条件选择度不理想等问题。下面围绕如何获取慢查询、解读索引建议以及验证优化效果展开。

一、慢查询定位与真实执行计划获取
要优化慢查询,第一步不是急着建索引,而是先把慢查询稳定地记录下来。PostgreSQL自带的pg_stat_statements扩展可以统计每条SQL的总执行时间、平均执行时间和返回行数,适合发现消耗资源最多的语句。而auto_explain可以在语句执行时间超过阈值时自动把执行计划写入日志,避免手工逐一抓取。下面是一个常见的配置示例:
-- postgresql.conf 中启用慢查询记录 shared_preload_libraries = 'pg_stat_statements,auto_explain' pg_stat_statements.track = all auto_explain.log_min_duration = 1000 auto_explain.log_analyze = on auto_explain.log_buffers = on
启用之后,可以通过pg_stat_statements视图找出总执行时间最高的SQL。拿到具体语句后,不要只使用普通的EXPLAIN,因为普通EXPLAIN只包含估计值,不会真正执行语句,无法反映真实行数和实际I/O情况。应当使用EXPLAIN (ANALYZE, BUFFERS)来获取包含实际执行时间、实际行数以及共享缓冲区读取情况的计划。
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT) SELECT * FROM orders WHERE created_at >= now() - interval '30 days' AND status = 'paid';
带ANALYZE的计划会真正执行SQL,因此在生产环境中要谨慎使用。对于写操作,可以放在事务中并在执行后回滚,或者改用EXPLAIN (ANALYZE, BUFFERS, TIMING OFF)进一步降低负载。拿到包含真实代价的执行计划后,才能让pgMustard基于准确数据给出索引建议。
二、pgMustard索引建议的解读逻辑
pgMustard解析的是PostgreSQL执行计划的JSON或TEXT格式。它会识别计划树中的顺序扫描节点,也就是Seq Scan。如果一个表很大,而查询只返回少量行,却触发了顺序扫描,pgMustard就会把该节点标记为索引缺失的高危区域。它还会参考过滤条件中的列、连接条件、排序字段以及行数估计,判断哪些列适合建立B-tree索引、复合索引或者部分索引。
例如下面的执行计划片段来自一个订单表范围查询:
Seq Scan on orders (cost=0.00..15123.45 rows=12000 width=180) (actual time=0.123..187.421 rows=11876 loops=1) Filter: ((created_at >= (now() - '30 days'::interval)) AND (status = 'paid'::text)) Rows Removed by Filter: 238873 Buffers: shared hit=3421 read=8971 Planning Time: 1.234 ms Execution Time: 192.876 ms
这里最关键的是Rows Removed by Filter: 238873。它说明PostgreSQL扫描了大约25万行数据,但最终只返回了1.1万多行,其余全部被过滤条件丢弃。pgMustard会基于这种大比例过滤推断出created_at和status这两个列的组合选择性较好,从而给出创建复合索引的建议。它的建议一般不是简单的“加索引”,而是会说明候选列、建议的列顺序以及预估的成本降低幅度。
除了缺失索引,pgMustard还能识别冗余索引和低效索引。比如某个表已经有(a, b)的复合索引,又单独创建了(a)索引,此时单独索引可能只在少数场景下有意义。它会结合pg_stats中的列直方图和空值比例,评估每个过滤条件的区分度,避免把低选择性列错误地放在索引前导位置。
三、订单查询优化实例:从全表扫描到索引扫描
假设订单表结构如下,并且已经积累了几十万行数据:
CREATE TABLE orders (
id bigserial PRIMARY KEY,
user_id bigint NOT NULL,
status text NOT NULL,
amount numeric(12,2),
created_at timestamptz NOT NULL DEFAULT now()
);
业务上经常需要查询最近30天内已支付状态的订单,语句与第一节中的示例一致。原始执行计划显示全表扫描,执行时间约192毫秒,缓冲区读取约1.2万个数据页。将计划上传到pgMustard后,它根据过滤条件中的等值列status和范围列created_at,建议创建(status, created_at)顺序的复合索引。注意这里不建议把created_at放在前面,因为等值条件放在前导列可以让索引扫描保持连续,而范围条件放在后导列仍然能够利用B-tree的有序性。
按照建议创建索引时,推荐使用CONCURRENTLY选项,避免长时间锁表影响写入:
CREATE INDEX CONCURRENTLY idx_orders_status_created_at
ON orders (status, created_at);
创建完成后再次执行EXPLAIN (ANALYZE, BUFFERS),计划通常会变成索引扫描:
Index Scan using idx_orders_status_created_at on orders (cost=0.29..823.45 rows=12000 width=180) (actual time=0.051..12.873 rows=11876 loops=1) Index Cond: ((status = 'paid'::text) AND (created_at >= (now() - '30 days'::interval))) Buffers: shared hit=189 read=312 Planning Time: 0.456 ms Execution Time: 14.321 ms
对比可以看到,执行时间从192毫秒下降到14毫秒左右,缓冲区读取从约1.2万个页面下降到约500个页面。更重要的是,优化后不再把大量时间浪费在读取和过滤无关行上。这个例子说明pgMustard的价值不是凭空猜测索引,而是把执行计划中的成本异常转化为可执行的列级别建议,降低了对DBA个人经验的依赖。
四、使用索引建议时容易忽略的代价
虽然索引能大幅提升查询性能,但每个索引都会带来写入放大和存储开销。对于频繁插入、更新或删除的表,每新增一个索引都会让这些写操作变慢。pgMustard的建议更适合读多写少的查询场景,如果某张表每天有大量写入,就需要权衡查询收益与写入代价。
此外,很多数据库里长期积累了一些从不使用的索引。可以通过pg_stat_user_indexes统计索引扫描次数,找出长期空闲的索引对象:
SELECT indexrelid::regclass AS index_name,
idx_scan,
pg_size_pretty(pg_relation_size(indexrelid)) AS index_size
FROM pg_stat_user_indexes
JOIN pg_index ON pg_index.indexrelid = pg_stat_user_indexes.indexrelid
WHERE pg_stat_user_indexes.idx_scan = 0
AND pg_index.indisunique IS FALSE;
这些未被使用的索引不仅占用磁盘,还会拖慢写操作。在应用pgMustard的索引建议之前,应当先清理已有的冗余索引,避免新索引与旧索引功能重叠。对于选择性很低的列,例如只有几个固定取值且分布均匀的字段,即使查询计划显示过滤,建立索引也可能无法显著减少扫描范围,反而增加维护成本。
pgMustard更像是执行计划分析助手,而不是自动决策系统。它给出的建议需要结合实际业务访问模式、表增长趋势和维护窗口来综合判断。合理的做法是:先定位慢查询,用真实执行计划找到代价最高的节点,再根据索引建议在小范围验证,最后持续监控pg_stat_user_indexes和pg_stat_statements,确认索引确实被用到且查询性能稳定提升。
PostgreSQL慢查询优化pgMustard索引建议修改时间:2026-09-20 15:12:35