☰
SQL 查询性能优化:从执行时间到 IO、执行计划与数据访问路径
2026/9/29 21:53:19 网站建设 项目流程

SQL 查询性能优化:从执行时间到 IO、执行计划与数据访问路径

SQL 查询性能优化,不能只看“这条 SQL 执行了多少秒”。

一条 SQL 当前执行只需要 200ms,并不代表它是一条性能良好的 SQL。如果它为了返回 100 行数据,却读取了 100 万行、产生几十万次逻辑读,那么当数据量、并发量和缓存压力增加后,它很可能迅速退化。

因此,SQL 性能优化的核心不是单纯追求“当前执行得快”,而是减少数据库为得到最终结果所付出的资源成本。

可以把 SQL 查询性能抽象为:

[
Performance = f(ExecutionPlan, IO, CPU, Rows, Loops, Memory, Wait)
]

也就是说,SQL 性能主要由以下几个因素决定:

  • 执行计划
  • 逻辑 IO
  • 物理 IO
  • CPU 消耗
  • 实际处理行数
  • 算子执行次数
  • 内存使用
  • 锁和等待

一套好的 SQL 优化方法,应该围绕这些指标展开,而不是只看执行时间。


一、SQL 性能优化首先看什么

建议固定观察以下 7 个核心指标:

  1. Execution Time:执行时间
  2. CPU Time:CPU 时间
  3. Logical Reads:逻辑读取
  4. Physical Reads:物理读取
  5. Rows Read:读取行数
  6. Rows Returned:最终返回行数
  7. Operator Loops:算子执行次数

其中最容易被忽略、但又极其重要的是:

Logical Reads Rows Read Loops

因为这三个指标更能反映 SQL 的真实成本。

例如:

返回数据:100 行 扫描数据:1,000,000 行

那么数据访问效率为:

[
Efficiency =
\frac{RowsReturned}{RowsRead}
]

即:

[
Efficiency =
\frac{100}{1000000}
= 0.01%
]

这意味着数据库做了大量无效工作。

即使当前查询只运行 300ms,这条 SQL 依然值得优化。


二、不要只看执行时间

这是 SQL 性能分析中最重要的原则之一。

例如一条 SQL:

Elapsed Time: 200ms Logical Reads: 900000 Physical Reads: 0

看起来只执行了 200ms。

但是Physical Reads = 0很可能只是因为数据已经存在 Buffer Cache 中。

数据库并没有真的从磁盘读取,而是直接从内存中读取了大量数据。

此时 SQL 的真实问题仍然存在:

Logical Reads = 900000

随着以下条件发生变化:

数据量增加 并发增加 Buffer Cache 竞争增加 数据库内存压力增加

查询性能可能变成:

200ms ↓ 1s ↓ 5s ↓ 10s

所以应该区分两个概念:

执行时间 = 当前环境下 SQL 跑得快不快 IO / Rows / Loops = SQL 本身设计得好不好

SQL 优化更应该关注第二个问题。


三、SQL Server 查询性能分析

SQL Server 提供了一套非常成熟的性能分析工具。

最常用的是:

setstatisticsioon;setstatisticstimeon;select...from...where...;setstatisticsiooff;setstatisticstimeoff;

其中:

setstatisticsioon;

用于观察 IO。

例如:

Table 'LSXHDMX'. Scan count 9, logical reads 928764, physical reads 0

这里最值得关注的是:

logical reads 928764

表示 SQL Server 从 Buffer Pool 中读取了大约 92 万个数据页。

SQL Server 一个 Page 默认 8KB。

所以大致访问的数据量为:

[
928764 \times 8KB
]

约等于:

[
7.1GB
]

这并不代表真的从磁盘读取了 7GB,而是表示 SQL Server 在内存页层面进行了如此大量的数据访问。

这通常说明存在:

大范围扫描 索引不合适 Nested Loop 被大量重复执行 过滤条件没有有效下推 关联顺序不合理 统计信息估算错误

四、SQL Server 的 Statistics IO 怎么看

典型输出:

Table 'OrderItem'. Scan count 20, logical reads 100000, physical reads 0, read-ahead reads 200

几个重要指标分别表示:

1. Logical Reads

logical reads

表示数据库从 Buffer Pool 中读取的数据页数量。

这是 SQL Server 优化中最应该关注的指标之一。

通常来说:

在返回结果基本一致的情况下 Logical Reads 越少越好

例如:

优化前:

logical reads = 500000

优化后:

logical reads = 5000

那么即使执行时间暂时变化不大,这通常也是一次非常成功的优化。


2. Physical Reads

表示数据库真正从存储设备读取的数据页数量。

例如:

physical reads = 0

并不意味着查询成本很低。

它往往只意味着:

数据已经在内存中

所以 SQL 优化不能只看 Physical Reads。


3. Scan Count

表示相关访问路径执行的次数。

例如:

Scan count = 1

通常问题不大。

如果出现:

Scan count = 10000

那么应该重点检查执行计划。

很多情况下是:

Nested Loop

导致内部表被重复访问。


五、一定要看实际执行计划

SQL 优化不能只看 SQL 文本。

真正决定数据库如何执行的是:

Execution Plan

也就是执行计划。

SQL Server 中建议直接查看:

Actual Execution Plan

重点关注几个节点:

Table Scan Index Scan Index Seek Nested Loops Hash Match Merge Join Sort Key Lookup RID Lookup Spool

六、Scan 不一定差,Seek 不一定好

很多开发人员会形成一种简单认知:

Index Seek = 好 Index Scan = 差 Table Scan = 很差

实际上并不完全正确。

例如查询:

select*fromsaleswheresale_date>='2026-01-01';

如果这条 SQL 最终需要返回表中 70% 的数据,那么:

Index Scan

可能比:

Index Seek + 大量 Key Lookup

更高效。

所以判断执行计划不能只看算子名字,而应该同时看:

Rows Cost IO Loops Lookup 次数

优化的目标不是:

强制所有查询都变成 Index Seek

而是:

让数据库用最低成本获取目标数据

七、重点检查 Actual Rows 和 Estimated Rows

执行计划中一个非常关键的指标是:

Estimated Rows Actual Rows

例如:

Estimated Rows = 100 Actual Rows = 1,000,000

说明优化器估算严重错误。

数据库原本认为:

只有 100 行

于是可能选择:

Nested Loop

但实际数据是:

100 万行

最终导致 Nested Loop 内层执行几十万甚至几百万次。

这种情况通常需要检查:

统计信息 参数嗅探 数据倾斜 临时表 表变量 表达式 关联条件

因此执行计划分析中,一个非常重要的问题是:

优化器以为有多少行,实际上有多少行?


八、重点观察 Rows × Loops

很多慢 SQL 的真正问题,不是单次查询慢,而是某个操作执行了太多次。

例如:

Index Seek Actual Rows = 100 Actual Number of Executions = 10000

那么实际处理量大约是:

[
100 \times 10000

1,000,000
]

也就是说,虽然执行计划上看只是一个普通的 Index Seek,但它被执行了一万次。

这是典型的:

Nested Loop 放大效应

因此分析 SQL 时建议建立一个简单意识:

[
TotalWork
\approx
Rows
\times
Loops
]

很多性能问题都可以通过这个公式发现。


九、索引优化的核心不是“加索引”

SQL 性能问题出现后,最常见的处理方式就是:

加索引

但这是一种非常危险的习惯。

正确的索引设计应该从查询模式出发。

例如:

selectproduct_code,quantity,amountfromsale_detailwhereshop_code='001'andsale_date>='2026-09-01'andsale_date<'2026-10-01';

可以考虑索引:

createindexix_sale_detail_shop_dateonsale_detail(shop_code,sale_date)include(product_code,quantity,amount);

这里:

shop_code sale_date

用于定位数据。

而:

product_code quantity amount

通过 INCLUDE 覆盖查询。

这样可以减少:

Key Lookup

十、索引字段顺序非常重要

联合索引:

(a,b,c)

并不等价于:

(c,b,a)

通常可以按照下面的思路设计:

等值条件 ↓ 范围条件 ↓ 排序 / 分组 ↓ Include 字段

例如:

whereshop_code=?andproduct_code=?andsale_datebetween?and?

那么可能考虑:

(shop_code, product_code, sale_date)

而不是简单按照数据库字段原来的顺序创建索引。


十一、避免对索引字段进行函数计算

典型错误:

whereconvert(varchar,rq,23)='2026-09-28'

或者:

whereyear(rq)=2026

这会导致数据库很难直接使用rq上的索引进行 Seek。

更推荐:

whererq>='2026-09-28'andrq<'2026-09-29'

同样:

whereyear(rq)=2026

应该考虑改成:

whererq>='2026-01-01'andrq<'2027-01-01'

优化原则是:

尽量不要加工索引列,而应该加工查询参数。


十二、日期范围推荐使用左闭右开

日期查询非常容易写出问题。

例如:

whererqbetween'2026-09-01'and'2026-09-30 23:59:59'

这种写法容易受到:

datetime datetime2 毫秒精度

影响。

推荐统一使用:

whererq>='2026-09-01'andrq<'2026-10-01'

即:

[
[startDate,endDate)
]

这种方式:

  • 更准确
  • 更容易使用索引
  • 更容易生成动态 SQL
  • 更容易统一报表日期逻辑

十三、减少无效数据读取

SQL 优化中最有效的方法之一不是“让数据库执行得更聪明”,而是:

让数据库少读数据

例如:

select*fromsale_detail;

如果实际上只需要:

selectspdm,sl,jefromsale_detail;

那么应该避免select *。

尤其是在:

大宽表 LOB text json varchar(max)

场景下,读取不需要的字段会明显增加:

IO 内存 网络传输 序列化成本

十四、尽早过滤数据

例如:

select...from(select*fromsale_detail)awherea.shop_code='001';

逻辑上可以工作,但复杂 SQL 中应该尽量让过滤条件尽早进入数据源。

理想结构:

select...fromsale_detailwhereshop_code='001';

在复杂报表中尤其要关注:

Predicate Pushdown

即过滤条件是否真正下推到最底层的数据访问节点。

优化目标应该是:

先减少数据 再关联 再聚合 再排序

而不是:

先把所有数据关联起来 ↓ 最后再过滤

十五、JOIN 是性能优化重点

大型业务 SQL 中,JOIN 往往是主要性能瓶颈。

例如:

ajoinbjoincjoindjoine

最终性能取决于:

各表数据量 过滤条件 JOIN 字段索引 JOIN 顺序 统计信息 选择性 执行计划

尤其要关注:

大表 JOIN 大表

例如:

销售明细:5 亿行 库存流水:3 亿行 采购明细:1 亿行

如果直接 Join:

salejoinstockjoinpurchase

很容易出现数据量爆炸。

更好的办法通常是:

先按业务粒度分别聚合 ↓ 再 JOIN

例如:

sale 先 group by shop + product stock 先 group by shop + product purchase 先 group by shop + product 最后三份结果 JOIN

这样可以大幅降低 Join 数据量。


十六、GROUP BY 前尽量减少数据

例如:

selectshop_code,product_code,sum(quantity)fromsale_detailgroupbyshop_code,product_code;

如果 sale_detail 有 5 亿条数据,那么 Group By 本身成本可能很高。

如果实际只统计最近 7 天:

whererq>=dateadd(day,-6,@endDate)andrq<dateadd(day,1,@endDate)

一定要在 Group By 之前过滤。

即:

5亿 ↓ 先过滤 ↓ 500万 ↓ 再 GROUP BY

而不是:

5亿 ↓ GROUP BY ↓ 最后再过滤

十七、临时表不是坏东西

很多人认为:

一条 SQL 写完

比:

临时表拆分

更高级。

实际上对于复杂业务查询,这并不一定成立。

例如一个 SQL 同时存在:

销售 退货 库存 采购 价格 会员 促销 门店 商品

全部写成一个巨大 CTE:

withaas(...),bas(...),cas(...),das(...)select...

优化器可能很难得到稳定执行计划。

此时可以合理使用:

select...into#salefrom...createindexix_sale...on#sale(...);

然后:

select...from#salejoin...

临时表有几个重要价值:

中间结果物化 降低查询复杂度 重新生成统计信息 建立临时索引 限制数据规模 提高执行计划稳定性

所以:

临时表应该被视为一种查询执行策略,而不是性能优化失败的标志。


十八、CTE 并不等于缓存

例如:

withsaleas(select...fromsale_detail)select...fromsale;

很多开发人员误以为:

sale 已经执行一次并保存起来

实际上 CTE 更多只是:

查询表达式

数据库完全可能把它重新展开到最终执行计划中。

所以复杂查询中,如果同一个中间结果被反复使用,应该观察执行计划。

必要时使用:

临时表 物化视图 中间汇总表

十九、ORDER BY 和 SORT 成本不能忽略

排序:

orderbyamountdesc

看起来很简单。

但如果需要排序 1000 万行,成本可能非常高。

执行计划可能出现:

Sort

并且发生:

Memory Spill

即排序所需内存不足,将数据写入 TempDB 或磁盘。

因此应该关注:

排序行数 排序字段是否有索引 Memory Grant TempDB Spill

很多报表 SQL 的真正瓶颈并不是 Join,而是:

大规模 Sort

二十、分页查询需要特别设计

常见分页:

orderbyidoffset1000000rowsfetchnext20rowsonly;

页数越深,性能越差。

因为数据库仍然可能需要处理前面的:

100 万行

大型系统可以考虑:

Seek Pagination

也叫:

Keyset Pagination

例如:

whereid>@lastIdorderbyidfetchnext20rowsonly;

这样性能更容易保持稳定。


二十一、避免 N+1 查询

SQL 优化不仅发生在数据库端。

ORM 系统经常出现:

查询订单 100 条 ↓ 每个订单查询一次明细 ↓ 101 次数据库访问

也就是:

N + 1

即使每条 SQL 只执行 5ms:

[
101 \times 5ms
]

也已经超过 500ms。

所以应用侧同样应该观察:

一次请求执行了多少 SQL

而不仅仅是:

某一条 SQL 执行了多久

二十二、MySQL 怎么分析 SQL

MySQL 8 推荐:

explainanalyzeselect...

重点看:

actual time rows loops Table scan Index lookup Nested loop

例如:

actual rows=10000 loops=1000

说明实际处理规模约为:

[
10000
\times
1000

1000万
]

因此 MySQL 同样应该重点观察:

Rows Loops

而不是只看执行时间。


二十三、PostgreSQL 怎么分析 SQL

PostgreSQL 推荐:

explain(analyze,buffers)select...

其中:

Buffers: shared hit shared read

非常重要。

可以近似理解:

shared hit ≈ SQL Server logical reads shared read ≈ 实际读取

例如:

shared hit=900000 shared read=0

和 SQL Server:

logical reads=900000 physical reads=0

反映的是类似的问题。


二十四、Oracle 怎么分析 SQL

Oracle 可以通过:

setautotraceonstatistics;

查看:

consistent gets physical reads rows processed

其中:

consistent gets

与 SQL Server:

logical reads

非常类似。

更加专业的分析可以结合:

DBMS_XPLAN

查看:

E-Rows A-Rows

即:

Estimated Rows Actual Rows

其核心思想与 SQL Server 完全一致。


二十五、建立统一的跨数据库性能模型

实际上不同数据库虽然命令不同,但底层问题高度一致。

可以统一成下面的分析模型:

SQL │ ▼ Execution Plan │ ┌──────────┼──────────┐ ▼ ▼ ▼ IO CPU Memory │ ▼ Rows × Loops │ ▼ Actual Work │ ▼ Execution Time

其中:

Execution Time

更像最终表现。

而:

IO Rows Loops Execution Plan

才是根因。

因此可以把 SQL 优化过程抽象成:

[
SQL
\rightarrow
Plan
\rightarrow
IO
\rightarrow
Rows
\rightarrow
Loops
\rightarrow
Time
]


二十六、推荐的 SQL 性能优化流程

实际工作中,可以固定按照下面的顺序分析。

第一步:确认 SQL

首先保存原始 SQL。

不要一开始就修改 SQL,否则后面无法比较优化效果。


第二步:记录基准数据

记录:

执行时间 CPU 时间 Logical Reads Physical Reads 返回行数

例如:

Elapsed Time = 3200ms CPU Time = 2800ms Logical Reads = 928764 Rows Returned = 2300

第三步:查看实际执行计划

检查:

Table Scan Index Scan Index Seek Nested Loop Hash Match Sort Key Lookup Spool

第四步:寻找最大成本节点

不要平均用力。

通常应该寻找:

读取数据最多的节点 执行次数最多的节点 行数估算偏差最大的节点 IO 最大的节点

也就是寻找:

[
MaxCostOperator
]


第五步:检查 Rows × Loops

例如:

Rows = 5000 Loops = 10000

那么这个节点很可能就是问题所在。


第六步:检查索引

确认:

过滤字段 Join 字段 排序字段 Group By 字段

是否存在适合的索引。


第七步:检查数据是否可以提前减少

考虑:

提前 WHERE 提前 GROUP BY 拆临时表 减少 JOIN 减少字段

第八步:重新执行 SQL

再次运行:

setstatisticsioon;setstatisticstimeon;

获得新的指标。


第九步:对比优化前后

例如:

指标优化前优化后
Logical Reads928,76421,430
CPU2,800ms320ms
Elapsed3,200ms410ms
Rows2,3002,300

这样的优化才是可以量化和验证的。


二十七、一个非常重要的优化原则

SQL 性能优化的核心目标可以浓缩成一句话:

用尽可能少的数据访问,得到相同的业务结果。

因此:

[
Performance
\propto
\frac{UsefulData}{TotalWork}
]

如果:

读取 100 万行 返回 100 行

通常意味着查询效率较低。

如果:

读取 120 行 返回 100 行

则查询的数据访问路径通常非常理想。


二十八、SQL 性能优化的三个层次

从架构角度,可以把 SQL 优化分成三个层次。

第一层:SQL 语句优化

例如:

WHERE JOIN GROUP BY ORDER BY 子查询 CTE 窗口函数

第二层:数据库访问结构优化

例如:

索引 分区 统计信息 物化视图 临时表 汇总表

第三层:系统架构优化

例如:

缓存 读写分离 报表库 OLTP / OLAP 分离 预计算 数据仓库 ClickHouse 异步统计

如果一条报表每次查询都需要:

扫描 10 亿销售明细 + 扫描 5 亿库存流水 + 扫描 3 亿采购记录

那么继续优化 SQL 本身的收益已经有限。

此时真正需要考虑的是:

数据模型和系统架构

二十九、最后的判断标准

判断 SQL 是否优秀,不应该只问:

执行了多少秒?

而应该连续问几个问题:

为了返回这些数据, 到底读取了多少数据? 实际处理了多少行? 某个算子执行了多少次? 执行计划是否稳定? 数据增长 10 倍后还能不能运行?

真正优秀的 SQL 应该同时具备:

低 IO 低 CPU 低 Rows Read 低 Loops 执行计划稳定 数据量增长后性能可预测

最终可以把 SQL 性能优化浓缩成一个模型:

[
SQLPerformance

f(
DataAccess,
ExecutionPlan,
IO,
Rows,
Loops,
CPU
)
]

其中最值得关注的是:

[
DataAccess
]

因为绝大多数 SQL 性能问题,本质上都可以归结为一句话:

数据库读取和处理了太多本来不应该处理的数据。

因此,SQL 查询优化真正的目标不是让数据库“跑得更快”,而是:

让数据库少做无效工作。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询