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 固化固定业务条件

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

相关推荐
cd_949217214 小时前
中金金融认证中心(CFCA)获元宇宙行业标准首批验证单位授牌
大数据·人工智能·金融
captain3764 小时前
数据库设计
数据库
XDHCOM1 天前
ORA-32484重复列名错误,ORACLE数据库CYCLE子句故障修复与远程处理方案
数据库·oracle
翻斗包菜1 天前
PostgreSQL 日常维护完全指南:从基础操作到高级运维
运维·数据库·postgresql
呆瑜nuage1 天前
MySQL表约束详解:8大核心约束实战指南
数据库·mysql
财迅通Ai1 天前
6000万吨产能承压 卫星化学迎来战略窗口期
大数据·人工智能·物联网·卫星化学
liliangcsdn1 天前
Agent Memory智能体记忆系统的示例分析
数据库·人工智能·全文检索
那个失眠的夜1 天前
Mybatis延迟加载策略
xml·java·数据库·maven·mybatis
Rick19931 天前
SQL 执行流程
数据库·sql