在 PostgreSQL 优化里,很多慢查询并不是"没加索引",而是索引设计和查询模式不匹配 。
这次分享一个真实案例:一条列表查询从 接近 5 秒 优化到 100ms 左右。
本文从 选择索引列 + 减少order by排序 入手进行优化
优化后减少排序的步骤,以及条件列和查询列都在索引中
一、问题 SQL
原始查询如下:
SELECT col_account_id, col_object_id
FROM tbl_xxx
WHERE col_status = 0
AND col_user_id = 850056
AND col_last_update > 0
ORDER BY col_created_time ASC;
这个 SQL 的特征很明显:
-
col_status = 0是固定值 -
col_user_id = ?是等值条件 -
col_last_update > 0是范围条件 -
ORDER BY col_created_time需要按创建时间排序
二、优化前执行计划
优化前执行计划如下:
Sort (cost=16385.91..16584.96 rows=79620 width=16)
(actual time=4970.599..4974.849 rows=72976 loops=1)
Sort Key: tbl_xxx.col_created_time
Sort Method: quicksort Memory: 6493kB
-> Index Only Scan using idx_old_xxx on tbl_xxx
(cost=0.56..9904.51 rows=79620 width=16)
(actual time=1.215..4949.754 rows=72976 loops=1)
Index Cond: ((col_status = 0)
AND (col_user_id = 850056)
AND (col_last_update > 0))
Heap Fetches: 8987
Planning Time: 1.700 ms
Execution Time: 4979.983 ms
执行时间接近:
5 秒
三、问题分析
从执行计划看,有两个关键问题。
1)虽然走了索引,但还是要排序
计划里有这一段:
Sort
说明 PostgreSQL 不能直接从索引中按 col_created_time 的顺序把结果吐出来,所以只能:
-
先扫描满足条件的数据
-
再对结果做排序
2)原有索引顺序不适合这个查询
原来的索引设计类似这样:
(col_status, col_user_id, col_last_update, col_created_time, col_object_id)
看起来字段很多,但问题在于:
-
col_last_update > 0是范围条件 -
一旦范围列出现在
col_created_time前面 -
PostgreSQL 就没法继续利用索引顺序完成
ORDER BY col_created_time
这就是为什么即使有索引,还是要额外排序。
四、优化思路
重新看这条 SQL,本质是:
在固定状态、固定更新时间条件下,查某个用户的数据,并按创建时间排序。
也就是说,真正适合索引的是:
-
先固定业务过滤条件
-
再按
col_user_id + col_created_time组织顺序
这种场景非常适合使用 Partial Index(条件索引)。
五、优化方案
创建新的条件索引:
CREATE INDEX CONCURRENTLY idx_xxx_user_created_partial
ON tbl_xxx (col_user_id, col_created_time)
INCLUDE (col_object_id)
WHERE col_status = 0
AND col_last_update > 0;
六、为什么这个索引有效
这个索引设计有几个关键点。
1)把固定条件放进 partial index 谓词里
WHERE col_status = 0
AND col_last_update > 0
这样索引里只保留真正会被这类查询用到的数据,索引更小、更聚焦。
2)索引键改成 (col_user_id, col_created_time)
这样对于:
WHERE col_user_id = 850056
ORDER BY col_created_time
PostgreSQL 可以直接按索引顺序扫描,不再需要额外排序。
3)INCLUDE (col_object_id) 做覆盖
查询只返回:
col_account_id, col_object_id
把需要返回的列覆盖进索引后,更容易走 Index Only Scan。
七、优化后执行计划
优化后的执行计划如下:
Index Only Scan using idx_xxx_user_created_partial on tbl_xxx
(cost=0.56..16726.95 rows=67877 width=16)
(actual time=0.041..101.490 rows=73216 loops=1)
Index Cond: (col_user_id = 850056)
Heap Fetches: 16058
Planning Time: 6.562 ms
Execution Time: 105.375 ms
执行时间变成:
105 ms
八、效果对比
| 指标 | 优化前 | 优化后 |
|---|---|---|
| 执行时间 | 4979ms | 105ms |
| 是否额外排序 | 是 | 否 |
| 扫描方式 | Index Only Scan + Sort | Index Only Scan |
| 性能提升 | - | 约 47 倍 |
九、这个案例的核心经验
这次优化最关键的不是"加索引"这三个字,而是:
1)索引列顺序必须匹配查询模式
如果 SQL 有:
WHERE 等值条件
ORDER BY 排序字段
那么索引往往就应该设计成:
(等值条件列, 排序列)
2)范围列不要随便挡在排序列前面
像:
col_last_update > 0
这种范围条件,一旦放在排序列前面,就很容易让排序失效。
3)固定业务条件特别适合 Partial Index
如果某类查询总是带:
col_status = 0
AND col_last_update > 0
那就非常适合把它们直接固化进索引条件里。
十、什么时候适合用这种优化方式
这种方式特别适合下面几类场景:
-
用户数据列表
-
订单列表
-
消息/任务列表
-
后台审核列表
只要查询模式类似:
WHERE 固定状态 + 用户条件
ORDER BY 时间字段
通常都可以从这个思路里获益。
十一、额外建议
优化后仍然看到:
Heap Fetches
说明虽然已经是 Index Only Scan,但还不是完全纯索引扫描。
可以再配合执行:
VACUUM (ANALYZE) tbl_xxx;
进一步改善可见性映射,减少回表次数。
十二、总结
这次优化带来的启发很直接:
PostgreSQL 查询优化,很多时候真正关键的不是"有没有索引",而是"索引是否和 WHERE + ORDER BY 的访问路径一致"。
最终效果:
5 秒 → 100ms
性能提升非常明显。
如果你在 PostgreSQL 优化中也遇到类似的慢查询,不妨重点检查两件事:
-
索引列顺序是否匹配查询
-
是否可以用 Partial Index 固化固定业务条件
很多时候,突破点就在这里。