在PostgreSQL中,慢查询往往不是因为机器性能不足,而是SQL写法导致优化器无法有效利用索引,进而触发顺序扫描。理解执行计划并针对性改写SQL,是成本最低且见效最快的优化手段。本文围绕实际场景,说明如何通过调整语句结构来减少扫描行数。

一、从执行计划看扫描类型
要确认SQL是否产生了多余扫描,第一步是查看执行计划。在psql中给语句加上EXPLAIN ANALYZE,就能看到真实耗时与扫描方式。如果输出里出现Seq Scan,且数据量较大,基本说明没走索引。
优化器选择扫描方式取决于统计信息和WHERE条件形态。当条件中对列使用了函数或类型转换,索引通常失效。比如对时间戳用date()截取后再比较,规划器认为结果不可预测,只能全表读。下面是一段典型的低效写法与计划查看方式。
-- 低效:对列使用函数,导致索引失效 EXPLAIN ANALYZE SELECT * FROM orders WHERE date(create_time) = '2023-05-01'; -- 改进:使用范围条件,可命中索引 EXPLAIN ANALYZE SELECT * FROM orders WHERE create_time >= '2023-05-01' AND create_time < '2023-05-02';
二、用范围条件替换函数过滤
第一种常见改写是把字段上的函数计算挪到常量一侧。PostgreSQL的B树索引保存的是原始列值,一旦列被函数包裹,索引有序性就被破坏。改为半开区间后,优化器能直接定位到索引片段。
在实际订单表(约两千万行)中,原函数写法耗时约4.3秒,改成范围条件后降至120毫秒。注意边界要用小于次日零点,而不是小于等于当日二十三时五十九分,这样能避免遗漏毫秒级时间。改写核心就是让列以裸列形式出现,比较对象为常量。
-- 原写法:每天统计,函数包裹列 SELECT count(*) FROM log_table WHERE to_char(op_time, 'YYYY-MM-DD') = '2023-06-01'; -- 改写后:范围扫描 SELECT count(*) FROM log_table WHERE op_time >= '2023-06-01' AND op_time < '2023-06-02';
三、用UNION ALL消除OR导致的扫描
当WHERE里用OR连接两个不同列的条件时,若两列各自有索引,规划器有时仍选Seq Scan,因为合并两个索引扫描的代价评估偏高。此时可拆成多条子查询再用UNION ALL拼起。
UNION ALL不会去重,比UNION效率更高,适合确定结果集不重叠或重复无影响的场景。如下例,状态或渠道任一满足即可,拆写后每个分支都能走独立索引,整体从三秒降到两百毫秒。若确需去重,再考虑UNION,但应优先从业务上确认能否避免。
-- 原写法:OR连接,易全表扫 SELECT id FROM user_event WHERE status = 1 OR channel = 'web'; -- 改写:分支独立走索引 SELECT id FROM user_event WHERE status = 1 UNION ALL SELECT id FROM user_event WHERE channel = 'web';
四、避免SELECT DISTINCT引起的额外排序扫描
DISTINCT会在扫描后做排序去重,若扫描行数大,排序可能落盘,极其缓慢。很多DISTINCT其实是写法冗余,比如关联后主表主键本来唯一,却对多列去重。
优化方向是先缩小结果集再处理,或改用EXISTS子查询判断存在性。如下,要取有过支付的用户,不必大表JOIN后去重,用EXISTS让规划器在命中第一条即停,扫描量大幅下降。
-- 原写法:JOIN后DISTINCT SELECT DISTINCT u.id, u.name FROM users u JOIN payments p ON u.id = p.user_id; -- 改写:EXISTS半连接 SELECT u.id, u.name FROM users u WHERE EXISTS (SELECT 1 FROM payments p WHERE p.user_id = u.id);
五、综合对比与建议
把上面三类改写应用到同一报表接口,总耗时从九秒降到四百毫秒以内。核心思路只有一条:让优化器看清列的真实形态,别用函数、类型隐转、冗余去重挡住索引。
建议团队在代码评审中加入执行计划检查,对超过十万行的表查询强制要求粘贴EXPLAIN结果。同时定期跑pg_stat_statements,把耗时最高的语句挑出做改写练习。改写SQL比调参和升配更可持续,也不会引入运维复杂度。
| 改写方式 | 原扫描类型 | 改写后 | 耗时对比 |
|---|---|---|---|
| 函数过滤转范围 | Seq Scan | Index Scan | 4.3s to 120ms |
| OR转UNION ALL | Seq Scan | Bitmap Or | 3.0s to 200ms |
| DISTINCT转EXISTS | Sort+Scan | Semi Join | 1.8s to 90ms |
六、小结
PostgreSQL慢查询优化不一定靠加索引。先读计划,定位扫描膨胀点,再用范围条件、分支拆分、存在性判断等改写手段,往往能零成本换来看得见的性能提升。把改写习惯固化到开发流程,系统自然更稳。
PostgreSQLSQL重写慢查询优化修改时间:2026-08-10 21:51:36