直接看逻辑读和物理读的页数可判断存储过程是否反复刷磁盘:逻辑读高但物理读低说明缓存利用尚可;物理读高则表明频繁从磁盘读取,性能堪忧;set statistics io on是最直接观测手段,单位为8kb页,关键字段包括逻辑读取(内存读页数)、物理读取(磁盘读页数)和预读(异步预加载页数)。

直接看逻辑读和物理读的页数,就能判断存储过程是否在反复刷磁盘——逻辑读高但物理读低,说明缓存利用还行;物理读高,基本等于在硬盘上拖着跑。
SET STATISTICS IO ON 是最直接的观测手段
在 SSMS 中执行存储过程前加这句,SQL Server 就会在“消息”窗口输出每张表的 I/O 统计,单位全是页(8KB):
SET STATISTICS IO ON; EXEC YourStoredProcedure @param = 'value';
关键字段含义:
-
逻辑读取:从缓冲池(内存)中读取的页数。越低越好,但受查询范围和索引覆盖影响 -
物理读取:真正从磁盘加载进内存的页数。持续高于几十甚至上百,说明缓存未命中严重 -
预读:SQL Server 预判后续需要的数据页,提前发起异步读取。它和逻辑读接近,说明预读效率高;若远低于逻辑读,可能因数据碎片或统计信息过期导致预读失效
用 sys.dm_exec_query_stats 查历史执行的 I/O 累计值
单次测试有偶然性,真实瓶颈藏在长期执行中。以下语句按 total_logical_reads 排序,找最“吃内存”的存储过程:
SELECT TOP 20
qs.total_logical_reads,
qs.total_physical_reads,
qs.execution_count,
qs.total_logical_reads / qs.execution_count AS avg_logical_reads,
qs.total_physical_reads / qs.execution_count AS avg_physical_reads,
SUBSTRING(qt.text, qs.statement_start_offset/2,
(CASE WHEN qs.statement_end_offset = -1
THEN LEN(CONVERT(NVARCHAR(MAX), qt.text)) * 2
ELSE qs.statement_end_offset END - qs.statement_start_offset) / 2) AS query_text
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qt.text LIKE '%CREATE PROC%' OR qt.text LIKE '%YourProcName%'
ORDER BY qs.total_logical_reads DESC;
注意点:
- 结果里
query_text可能只截出存储过程内部某条语句,需结合qt.objectid和DB_NAME(qt.dbid)定位具体对象 -
total_physical_reads是自计划编译后累计值,重启服务会清零;如果该值长期增长快于execution_count,说明每次执行都在大量刷盘 - 避免只看总量,重点盯
avg_physical_reads—— 即使总物理读不高,但单次平均超 50 页,也值得优化
物理读高的常见原因与对应动作
不是所有高物理读都该“修”,得先区分是冷启动还是常态问题:
- 首次执行存储过程时
physical_reads高,logical_reads ≈ pre-read:正常。缓存为空,SQL Server 在预读填充。后续执行应大幅回落 - 反复执行仍保持高位物理读:大概率缺有效索引,或现有索引无法覆盖查询列,触发大量“回表”(即通过非聚集索引查到 RID/键值后,再额外读数据页)
- 执行前后刚运行过
DBCC DROPCLEANBUFFERS:人为清空缓存,测出的是最差场景,不能代表生产表现 - 统计信息陈旧(
STATS_DATE()返回日期早于一周):优化器误判行数,选错执行计划,可能放弃索引走全表扫描 → 直接导致逻辑读、物理读双高
验证索引是否覆盖,可打开执行计划,看是否有 Key Lookup 或 Clustered Index Seek 后紧跟大量 Clustered Index Scan —— 这些都是回表信号。
逻辑读异常高但物理读低,往往暴露设计问题
这种情况容易被忽略,因为“没刷盘”看起来不卡。但它意味着:内存带宽和 CPU 在反复搬运大量无用数据页。
- 典型表现:逻辑读几千页,但只返回几行结果。比如
SELECT *查大表却只用其中 1–2 列,又没建覆盖索引 - 根本原因常是缺失
INCLUDE列的非聚集索引,或 WHERE 条件列选择性极差(如WHERE status IN (0,1,2)占全表 90%),导致索引失效、退化为扫描 - 检查
sys.dm_db_index_usage_stats中对应索引的user_seeks是否远小于user_scans:如果是,说明索引基本没被“用对”
真正难调的不是物理读爆表,而是逻辑读高得离谱却没人报警——它不触发磁盘等待,但悄悄吃光内存带宽和 CPU 时间片,还会挤占其他查询的缓冲池空间。










