PostgreSQL 性能优化实战:一条 Order by 的 SQL 从 5 秒优化到 100ms

在 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 的顺序把结果吐出来,所以只能:

  1. 先扫描满足条件的数据

  2. 再对结果做排序

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 优化中也遇到类似的慢查询,不妨重点检查两件事:

  1. 索引列顺序是否匹配查询

  2. 是否可以用 Partial Index 固化固定业务条件

很多时候,突破点就在这里。

相关推荐
jkyy20143 分钟前
顺应IoT与健康产业融合趋势,补齐中小企业健康数字化短板
大数据·人工智能·信息可视化·健康医疗
云栖梦泽在7 分钟前
AI安全实战:AI模型投毒攻击的检测与修复实战
大数据·人工智能·安全
DevOpenClub10 分钟前
职教高考及高职分类招生控制线 API 接口
java·数据库·高考
funnycoffee12312 分钟前
华为S5736交换机3层ECMP负载方式
linux·服务器·数据库
添砖java‘’12 分钟前
MySQL复合查询
数据库·mysql
星川水月14 分钟前
Access数据库快速入门——外部数据导入和SQL简单查询
数据库·sql·access
随身数智备忘录17 分钟前
安全生产法详解:安全生产法如何规范企业安全管理行为?
大数据·人工智能
狒狒热知识23 分钟前
软文营销媒体发稿效果倍增逻辑内容渠道平台三维协同运营解析
大数据
程序猿追28 分钟前
行业新趋势:Agent 重构,企业大屏从静态展示走向智能交互
大数据·人工智能·microsoft
清平乐的技术专栏31 分钟前
一文读懂Kafka中的“消费”(对标MySQL数据库)
数据库·mysql·kafka