接口越用越慢,数据库CPU不高、连接数也正常,就是一条查询从几十毫秒涨到几秒。这种时候猜是哪张表没用没意义,先把它抓出来,再看执行计划为什么没走索引。
先把慢查询抓出来
pg_stat_statements是第一步。它要预加载,改完必须重启实例才生效,光CREATE EXTENSION是不够的:
shared_preload_libraries = 'pg_stat_statements' pg_stat_statements.max = 10000 pg_stat_statements.track = all
重启之后建扩展,然后按总耗时排序:
CREATE EXTENSION IF NOT EXISTS pg_stat_statements; SELECT substring(query, 1, 80) AS q, calls, round(total_exec_time::numeric, 1) AS total_ms, round(mean_exec_time::numeric, 2) AS avg_ms, rows FROM pg_stat_statements ORDER BY total_exec_time DESC LIMIT 10;
看total_ms而不是avg_ms。一条每次只要5毫秒、一天调200万次的查询,比一条偶尔跑3秒的报表查询更值得改。统计是实例级的累积值,重置用pg_stat_statements_reset()。
要看单条SQL到底慢在哪一步,开log_min_duration_statement,比如记录超过1000毫秒的语句:
log_min_duration_statement = 1000 log_line_prefix = '%m [%p] %q%u@%d '
生产环境常年开auto_explain更省事,它把执行计划直接写进日志,不用手工重跑:
shared_preload_libraries = 'pg_stat_statements,auto_explain' auto_explain.log_min_duration = 1000 auto_explain.log_analyze = on auto_explain.log_buffers = on
实测这套配置在一天之内就能把问题SQL捞全,代价是日志量会涨,磁盘不大的机器上把阈值设高一点。
执行计划要这么读
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM orders WHERE phone = '13800138000';
三个地方挨着看。一是节点类型出现Seq Scan且表有几百万行,基本就是没走索引。二是rows估算值和actual rows差出数量级,比如预估120行、实际14万行,说明统计信息过时,先跑一遍ANALYZE orders再看。三是Buffers那行,shared read远大于shared hit意味着数据在反复从磁盘读,加索引和加大内存都能缓解,但根子不在一处。
索引建了却不命中的几种写法
对列做了函数或运算。where lower(name) = 'abc'走不上name上的普通索引,得建表达式索引:
CREATE INDEX idx_orders_phone_lower ON orders (lower(phone));
按天统计写成where date(created_at) = '2026-10-01'也是一个道理,改成范围条件就能用上普通索引:
WHERE created_at >= '2026-10-01' AND created_at < '2026-10-02'
类型不匹配是最隐蔽的一种。phone是varchar,SQL里写成where phone = 13800138000,数据库会把列转成数字再比较,索引直接失效,执行计划里能看到一个cast。遇到过一次线上事故,就是这个原因让一张800万行的表天天全表扫。
前导模糊。like '%1380'用不上B-tree索引,要上pg_trgm加GIN索引:
CREATE EXTENSION pg_trgm; CREATE INDEX idx_orders_phone_trgm ON orders USING gin (phone gin_trgm_ops);
复合索引的前导列没出现在条件里。索引是(status, created_at),查询只给created_at,就用不上。建索引时把等值条件的列放前面、范围条件的列放后面。
表太小。几百行的表全表扫比走索引还快,优化器故意选Seq Scan,这不是问题。
规划器的两个默认值
random_page_cost默认4.0,是按机械盘的随机寻道代价定的。机器上是SSD的话,这个值会让优化器高估随机读成本、偏不爱用索引,改到1.1是常见做法。见过不少机器换了SSD之后这个值一直没动过,索引建齐了照样全表扫:
ALTER SYSTEM SET random_page_cost = 1.1; SELECT pg_reload_conf();
effective_cache_size默认4GB,告诉优化器有多少内存可用于缓存。64GB内存的机器只写4GB,优化器会以为缓存很小、更倾向顺序扫。这两处改完不用重启,reload即可。
统计信息过期的判断办法:pg_stat_user_tables里的last_analyze字段,长期不更新的大表手动跑一次ANALYZE,很多"突然变慢"就是这么来的。autovacuum默认阈值在大表上容易跟不上,可以单独调:
ALTER TABLE orders SET (autovacuum_analyze_scale_factor = 0.02);
顺手清掉没人用的索引
索引不是越多越好,每个索引都在拖慢写入。查一下哪些从没被用过:
SELECT relname, indexrelname, idx_scan FROM pg_stat_user_indexes WHERE idx_scan = 0 ORDER BY pg_relation_size(indexrelid) DESC;
idx_scan为0且体积不小的,基本可以删,删之前确认它不是唯一约束在用的那个。另外查一下哪些表在大量顺序扫:
SELECT relname, seq_scan, seq_tup_read, idx_scan FROM pg_stat_user_tables ORDER BY seq_tup_read DESC LIMIT 10;
seq_scan数值大、idx_scan几乎是0的大表,就是要补索引的对象。
以上写法在PostgreSQL 13.0、14.0、15.0、16.0、17.0、18.0,上均适用;18.0发布于2025年9月25日,引入了异步I/O子系统,顺序扫描本身变快了,但索引该建还得建。数据截至2026年10月。