SQL Server内存管理深度剖析:从Buffer Pool到压力诊断
SQL Server内存管理深度剖析:从Buffer Pool到压力诊断
系列文章: SQL Server性能优化实战系列 · 第3篇
发布日期: 2026-11-12
字数: 约4500字
阅读时间: 15分钟
适用版本: SQL Server 2016、2017、2019、2022(具体能力以目标版本和补丁为准)
引言
SQL Server出现“查询突然变慢”时,很多人第一反应是看CPU。可是内存压力往往先表现为缓存命中下降、查询反复读盘、执行计划发生变化,最后才以CPU和I/O升高的形式暴露出来。
另一个常见误区是把 Page Life Expectancy(PLE)低于某个固定数字直接判定为内存不足。这个判断在不同服务器规模、NUMA布局和工作负载下并不可靠。真正需要回答的是三个问题:
- SQL Server当前把内存用在了哪里?
- 当前压力来自操作系统、SQL Server内部组件,还是查询执行内存不足?
- 采取扩容、调整配置、优化查询还是治理工作负载,哪一种动作最可能解决根因?
本文沿用前两篇的诊断路径,先确认现象,再定位内存使用者,最后把处理动作分为即时、配置、查询和架构四个层次。文中脚本按 SQL Server 2016—2022 的常见 DMV 结构编写,涉及版本差异和实例配置变更的语句,必须先在测试环境验证。
一、先区分三类内存:Buffer Pool、执行内存和其他内存
1.1 Buffer Pool不是“SQL Server全部内存”
Buffer Pool主要缓存数据页和索引页,也承载部分系统页、计划相关结构和其他内存对象。数据页能够留在内存中,后续访问就可能直接从内存完成;当缓存压力增大时,页面会被淘汰,查询可能重新产生物理I/O。
但是,SQL Server进程的总内存并不等于Buffer Pool。查询编译、排序、哈希、列存储、CLR、扩展组件、锁管理和其他组件都可能消耗内存。只看一个“数据库缓存大小”指标,无法解释全部内存压力。
1.2 Memory Clerk是定位内存使用者的入口
sys.dm_os_memory_clerks按组件记录内存分配。常见关注对象包括:
MEMORYCLERK_SQLBUFFERPOOL:数据库页缓存相关内存;CACHESTORE_*:计划缓存和其他缓存存储;USERSTORE_*:用户存储,例如安全缓存、元数据缓存等;MEMORYCLERK_SQLQERESERVATIONS:查询执行内存保留;MEMORYCLERK_XE:扩展事件相关内存;- 其他
MEMORYCLERK_*:具体组件的专用分配。
Clerk值较高只能说明“谁占用得多”,不能直接说明“谁就是问题”。例如,Buffer Pool大并不意味着泄漏,查询执行内存大也不一定是错误,必须结合工作负载、等待和变化趋势判断。
1.3 AWE、锁定页和现代版本的区别
AWE是较早版本用于支持大内存地址空间的机制,不能直接套用到今天的64位实例。现代SQL Server更常见的讨论是:
max server memory是否给操作系统和其他组件留下足够空间;- 是否需要为SQL Server服务账号授予“锁定内存页”权限(LPIM);
- SQL Server 2012以后统一内存分配器带来的 DMV 口径变化;
- 版本、版本类别、容器限制和虚拟化平台是否改变了可用内存边界。
LPIM不是“开了就更快”。如果没有明确的操作系统内存压力、分页风险和回收策略,直接开启会增加故障排查难度。任何此类变更都应先建立基线并准备回退。
二、内存压力的四种表现
2.1 外部物理内存压力
外部物理内存压力来自操作系统或同一主机上的其他进程。常见迹象包括:
- 操作系统可用内存持续下降;
- SQL Server收到外部内存压力通知;
- SQL Server工作集或缓存受到回收影响;
- 与备份、杀毒、代理、监控、虚拟机宿主机争用同时发生。
这类问题不能只通过增加 max server memory 解决。若其他进程已经挤占主机资源,首先要确认进程边界和主机容量。
2.2 外部虚拟内存压力
外部虚拟内存压力通常与进程地址空间、提交限制、分页文件或平台限制有关。64位系统上出现这种情况相对少见,但并不等于不会发生。容器限制、驱动、扩展组件和大型内存映射也可能改变可用空间。
2.3 SQL Server内部内存压力
内部压力是SQL Server各组件之间争用内存,常见表现是:
- 查询授予内存等待;
- 大量排序或哈希溢出到tempdb;
- 计划缓存频繁淘汰;
- 某个Memory Clerk快速增长;
- 执行计划和运行时行为随压力变化。
2.4 查询执行内存压力
排序、哈希连接、哈希聚合和批处理操作通常需要查询执行内存。授予不足时,查询可能等待 RESOURCE_SEMAPHORE;授予过大时,又可能挤压其他查询。
因此,“内存不足”至少有两种完全不同的处理方向:
- Buffer Pool不足:重点看工作集、数据访问、缓存压力和物理I/O;
- 执行内存不足:重点看估算行数、内存授予、排序/哈希、并发和查询计划。
三、第一组(示例)脚本:确认实例内存全貌
3.1 查看实例内存配置
SELECT
name,
value_in_use,
value,
description
FROM sys.configurations
WHERE name IN ('min server memory (MB)', 'max server memory (MB)');
max server memory是SQL Server可使用的主要内存上限,但不应被理解为进程绝对总内存上限。配置时要给操作系统、代理、备份、监控和其他数据库组件保留空间。
3.2 查看进程与操作系统内存状态
SELECT
physical_memory_in_use_kb / 1024 AS physical_memory_in_use_mb,
locked_page_allocations_kb / 1024 AS locked_page_allocations_mb,
total_virtual_address_space_kb / 1024 AS total_virtual_address_space_mb,
virtual_address_space_reserved_kb / 1024 AS virtual_address_space_reserved_mb,
virtual_address_space_committed_kb / 1024 AS virtual_address_space_committed_mb,
memory_utilization_percentage,
process_physical_memory_low,
process_virtual_memory_low
FROM sys.dm_os_process_memory;
这里的 process_physical_memory_low 和 process_virtual_memory_low 是信号,不是完整结论。需要与操作系统性能计数器、实例等待和时间序列数据结合。
3.3 查看操作系统可见内存
SELECT
total_physical_memory_kb / 1024 AS total_physical_memory_mb,
available_physical_memory_kb / 1024 AS available_physical_memory_mb,
total_page_file_kb / 1024 AS total_page_file_mb,
available_page_file_kb / 1024 AS available_page_file_mb,
system_memory_state_desc
FROM sys.dm_os_sys_memory;
如果 available_physical_memory_mb 长时间偏低,同时系统内其他进程也在增长,应先处理主机层容量和进程争用。
四、第二组(示例)脚本:定位Memory Clerk和缓存压力
4.1 按Memory Clerk汇总
SELECT TOP (30)
type,
SUM(pages_kb) AS pages_kb,
SUM(virtual_memory_reserved_kb) AS virtual_reserved_kb,
SUM(virtual_memory_committed_kb) AS virtual_committed_kb
FROM sys.dm_os_memory_clerks
GROUP BY type
ORDER BY SUM(pages_kb) DESC;
SQL Server 2012 以后,pages_kb已经统一了早期版本的单页/多页分配口径。64位实例要核对锁定页时,使用内存节点或进程级的 locked_page_allocations_kb,不要再把 awe_allocated_kb 当作现代实例的锁定页指标。
SELECT
memory_node_id AS node_id,
pages_kb,
locked_page_allocations_kb,
virtual_address_space_reserved_kb,
virtual_address_space_committed_kb
FROM sys.dm_os_memory_nodes
ORDER BY memory_node_id;
示例输出(用于说明判读逻辑,非特定生产环境实测值):
| node_id | pages_kb | locked_page_allocations_kb | 判读 |
|---|---|---|---|
| 0 | 18,420,000 | 17,900,000 | 节点大部分内存由SQL Server锁定,需结合操作系统余量判断 |
| 1 | 17,980,000 | 17,400,000 | 多NUMA节点接近时,重点看节点间是否均衡 |
建议定时保存结果,而不是只在故障发生后查询一次。一次快照只能告诉你当前排序,无法说明哪个组件正在异常增长。
4.2 分析Buffer Pool、缓存和查询保留
SELECT
type,
SUM(pages_kb) AS pages_kb
FROM sys.dm_os_memory_clerks
WHERE type IN (
'MEMORYCLERK_SQLBUFFERPOOL',
'MEMORYCLERK_SQLQERESERVATIONS',
'CACHESTORE_SQLCP',
'CACHESTORE_OBJCP',
'USERSTORE_SCHEMAMGR'
)
GROUP BY type
ORDER BY pages_kb DESC;
CACHESTORE_SQLCP偏高时,可以进一步排查即席SQL和参数化问题;CACHESTORE_OBJCP偏高可能与存储过程或触发器计划有关。它们只是排查方向,不能凭单项大小直接做清理。
4.3 查询缓存中的高内存授予语句
SELECT TOP (30)
qs.total_worker_time / 1000 AS total_cpu_ms,
qs.total_elapsed_time / 1000 AS total_elapsed_ms,
qs.total_logical_reads,
qs.execution_count,
qs.max_grant_kb,
qs.total_grant_kb,
qs.last_grant_kb,
DB_NAME(st.dbid) AS database_name,
SUBSTRING(
st.text,
(qs.statement_start_offset / 2) + 1,
((CASE qs.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE qs.statement_end_offset
END - qs.statement_start_offset) / 2) + 1
) AS statement_text
FROM sys.dm_exec_query_stats AS qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) AS st
ORDER BY qs.max_grant_kb DESC;
这个查询依赖当前计划仍在缓存中。需要历史数据时,应结合Query Store或扩展事件,不能把一次缓存快照当作完整历史。
4.4 Memory Broker:看“谁在申请、谁在退让”
Memory Clerk告诉你内存最终落在哪个组件,Memory Broker更接近“内存压力如何在组件之间分配”。它适合用来解释:为什么Buffer Pool看起来还有空间,查询授予却已经开始排队;或者为什么某类缓存持续被回收。
SELECT
pool_id,
memory_broker_type,
allocations_kb,
allocations_kb / 1024.0 AS allocations_mb,
allocations_kb_per_sec,
predicted_allocations_kb,
target_allocations_kb,
future_allocations_kb,
overall_limit_kb,
last_notification
FROM sys.dm_os_memory_brokers
ORDER BY allocations_kb DESC;
示例输出(演示判读逻辑):
| memory_broker_type | allocations_mb | target_allocations_mb | allocations_kb_per_sec | 判读 |
|---|---|---|---|---|
| MEMORYBROKER_FOR_RESERVE | 6,144 | 4,096 | -18,000 | 当前分配高于目标且速率为负,需结合内存授予等待确认是否正在收缩 |
| MEMORYBROKER_FOR_CACHE | 23,500 | 28,000 | 35,000 | 当前分配低于目标且仍在增长,不能仅凭PLE下降判定容量不足 |
不要把某一行的 allocations_kb_per_sec 当作永恒趋势。至少连续采样两到三个窗口,观察分配速率与业务批次是否同步。
4.5 Clock Hands:识别计划缓存驱逐压力
当计划缓存不断被淘汰时,常见表现是编译次数上升、CPU增加、相同SQL反复生成新计划。sys.dm_os_memory_cache_clock_hands可以帮助判断内部和外部时钟指针是否在频繁扫描缓存。
SELECT
name,
clock_hand,
rounds_count,
removed_all_rounds_count,
removed_last_round_count,
removed_all_rounds_count / NULLIF(rounds_count, 0.0)
AS avg_removed_per_round
FROM sys.dm_os_memory_cache_clock_hands
ORDER BY removed_last_round_count DESC;
判读方法:
rounds_count持续增长,说明时钟指针在不断扫描缓存;removed_last_round_count同步升高,说明扫描确实带来了驱逐;- 如果同时看到
CACHESTORE_SQLCP偏高和即席SQL比例高,优先检查参数化与计划复用; - 不要用
DBCC FREEPROCCACHE代替根因分析,它会制造一次全局计划抖动。
4.6 Ring Buffer:把“内存压力通知”拉出时间线
Ring Buffer保存的是最近一段时间的内部通知。它不能替代长期监控,但适合在故障窗口内确认:SQL Server何时收到外部物理内存压力、是否出现OOM、是否发生Buffer Pool失败。
SELECT
ring_buffer_type,
COUNT(*) AS event_count
FROM sys.dm_os_ring_buffers
WHERE ring_buffer_type IN (
'RING_BUFFER_RESOURCE_MONITOR',
'RING_BUFFER_OOM',
'RING_BUFFER_MEMORY_BROKER',
'RING_BUFFER_BUFFER_POOL'
)
GROUP BY ring_buffer_type
ORDER BY ring_buffer_type;
解析资源监视器通知:
WITH ResourceMonitor AS
(
SELECT
timestamp,
CONVERT(xml, record) AS record_xml
FROM sys.dm_os_ring_buffers
WHERE ring_buffer_type = N'RING_BUFFER_RESOURCE_MONITOR'
AND record LIKE '%<Notification>%'
)
SELECT TOP (50)
DATEADD(ms, -1 * (osi.ms_ticks - rm.timestamp), SYSDATETIME())
AS approx_event_time,
rm.record_xml.value('(/Record/ResourceMonitor/Notification)[1]', 'nvarchar(60)')
AS notification_type,
rm.record_xml.value('(/Record/ResourceMonitor/IndicatorsProcess)[1]', 'int')
AS process_memory_indicator,
rm.record_xml.value('(/Record/ResourceMonitor/IndicatorsSystem)[1]', 'int')
AS system_memory_indicator,
rm.record_xml.value('(/Record/MemoryRecord/AvailablePhysicalMemory)[1]', 'bigint') / 1024
AS available_physical_mb
FROM ResourceMonitor AS rm
CROSS JOIN sys.dm_os_sys_info AS osi
ORDER BY approx_event_time DESC;
不同补丁版本的XML节点可能存在差异。先用SELECT TOP (1) CONVERT(xml, record)查看原始结构,再固定XQuery路径。即使某个节点为空,也不能直接解释为“没有压力”。
4.7 内存错误码:从错误号反推排查路径
| 错误号 | 典型含义 | 第一组证据 | 优先排查方向 |
|---|---|---|---|
| 701 | 资源池没有足够内存执行请求 | Resource Governor池、内存授予、Memory Broker | 资源池上限、并发授予、查询估算 |
| 802 | Buffer Pool没有足够内存 | PLE、物理读、Buffer Pool、系统内存 | 工作集、扫描、max server memory和主机余量 |
| 8628 | 优化阶段等待超时 | 编译等待、复杂计划、内存授予 | 查询复杂度、统计信息、参数敏感性 |
| 8645 | 等待内存资源执行查询超时 | RESOURCE_SEMAPHORE、请求授予 | 大授予、并发报表、错误估算 |
| 8651 | 请求的内存授予不可用 | min memory per query、资源池 | 最小授予设置、资源池边界、查询并发 |
配合错误日志做时间窗口关联:
EXEC sys.xp_readerrorlog
0,
1,
N'701';
EXEC sys.xp_readerrorlog
0,
1,
N'8645';
xp_readerrorlog适合快速筛选,不适合替代集中日志平台。对于高频错误,应按时间、数据库、应用和资源池做归档。
五、第三组(示例)脚本:诊断RESOURCE_SEMAPHORE和内存授予
5.1 查看当前正在等待内存授予的请求
SELECT
mg.session_id,
mg.request_id,
mg.requested_memory_kb,
mg.granted_memory_kb,
mg.required_memory_kb,
mg.used_memory_kb,
mg.max_used_memory_kb,
mg.wait_time_ms,
mg.queue_id,
mg.timeout_sec,
r.status,
r.command,
r.wait_type,
r.wait_time,
DB_NAME(r.database_id) AS database_name,
SUBSTRING(
st.text,
(r.statement_start_offset / 2) + 1,
((CASE r.statement_end_offset
WHEN -1 THEN DATALENGTH(st.text)
ELSE r.statement_end_offset
END - r.statement_start_offset) / 2) + 1
) AS statement_text
FROM sys.dm_exec_query_memory_grants AS mg
LEFT JOIN sys.dm_exec_requests AS r
ON r.session_id = mg.session_id
AND r.request_id = mg.request_id
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) AS st
ORDER BY mg.wait_time_ms DESC, mg.requested_memory_kb DESC;
重点观察:
- 请求的内存是否远大于实际使用;
- 等待是否集中在少量大查询;
- 估算行数是否严重偏离实际行数;
- 是否存在并发批量报表、排序、哈希操作同时启动。
5.2 查看内存授予相关等待
SELECT
wait_type,
waiting_tasks_count,
wait_time_ms,
signal_wait_time_ms,
max_wait_time_ms
FROM sys.dm_os_wait_stats
WHERE wait_type IN ('RESOURCE_SEMAPHORE', 'RESOURCE_SEMAPHORE_QUERY_COMPILE')
ORDER BY wait_time_ms DESC;
等待统计是实例累计值,重启或清理后会重新开始。生产环境不要为了“让数字归零”随意清空等待统计;应先记录时间点,并用两次采样计算增量。
5.3 识别排序和哈希溢出
在可控的测试环境中,可以通过扩展事件捕获排序或哈希警告,也可以从执行计划查看相关警告。若发现大量溢出,应优先检查:
- 统计信息是否过期;
- 数据类型和谓词是否导致估算失真;
- 查询是否读取了不必要的列和行;
- 索引是否可以减少排序或哈希输入;
- 内存授予反馈是否适用于当前版本和兼容级别。
直接把 max server memory 调大,可能只会把问题从一个查询变成全实例争用。
六、PLE的正确用法:趋势比固定阈值更重要
PLE衡量页面在被清出Buffer Pool之前的大致停留时间。它适合做趋势指标,不适合单独做故障结论。
以下查询可以查看相关性能计数器:
-- 先保留对象名、实例名和cntr_type,避免把不同NUMA节点或不同计数器混在一起。
SELECT
object_name,
counter_name,
instance_name,
cntr_type,
cntr_value
FROM sys.dm_os_performance_counters
WHERE counter_name IN (
'Page life expectancy',
'Buffer cache hit ratio',
'Buffer cache hit ratio base',
'Lazy writes/sec',
'Free list stalls/sec'
)
ORDER BY object_name, counter_name, instance_name;
-- Buffer cache hit ratio是“分子/基数”计数器,不能直接读取cntr_value当百分比。
WITH CounterData AS
(
SELECT
object_name,
instance_name,
counter_name,
cntr_type,
cntr_value
FROM sys.dm_os_performance_counters
WHERE object_name LIKE N'%Buffer Manager%'
AND counter_name IN ('Buffer cache hit ratio', 'Buffer cache hit ratio base')
)
SELECT
ratio.instance_name,
CAST(
100.0 * ratio.cntr_value / NULLIF(base.cntr_value, 0)
AS decimal(10, 2)
) AS buffer_cache_hit_ratio_pct,
ratio.cntr_value AS ratio_raw_value,
base.cntr_value AS ratio_base_value,
ratio.cntr_type
FROM CounterData AS ratio
JOIN CounterData AS base
ON base.object_name = ratio.object_name
AND base.instance_name = ratio.instance_name
WHERE ratio.counter_name = 'Buffer cache hit ratio'
AND base.counter_name = 'Buffer cache hit ratio base';
Buffer cache hit ratio是分子/基数型计数器,必须与同名的Buffer cache hit ratio base配对计算;常见构建中的cntr_type为537003264。Page life expectancy在多NUMA节点下可能同时返回汇总行和按节点实例,不能把多行直接相加。
分析PLE时至少要同时看:
- 高峰期与正常期的相对变化;
- 是否多个NUMA节点同时下降;
- Lazy Writes、Free List Stalls是否同步升高;
- 物理读、存储延迟和查询逻辑读是否发生变化;
- 工作负载是否刚刚增加了大范围扫描或报表任务。
“PLE低于300就是内存不足”是一种过于简单的经验规则。小型实例与大型实例的缓存规模完全不同,统一阈值会产生误判。
七、内存压力诊断流程
发现查询变慢
│
├─ 先看CPU、I/O、阻塞和等待类型
│
├─ 检查操作系统可用内存与SQL Server进程状态
│ │
│ ├─ 操作系统也缺内存:检查其他进程、主机容量和虚拟化限制
│ │
│ └─ 操作系统正常:继续检查SQL Server内部组件
│
├─ 查看Memory Clerk趋势
│ │
│ ├─ Buffer Pool压力:检查工作集、扫描、缓存淘汰和物理读
│ │
│ ├─ 查询授予压力:检查RESOURCE_SEMAPHORE、估算和并发
│ │
│ └─ 其他Clerk异常:按组件继续定位
│
├─ 选择动作:查询优化 → 工作负载治理 → 配置调整 → 容量规划
│
└─ 复测并保留变更前后证据
7.1 内存压力诊断与优化流程图
下面的流程图把本节的诊断路径压缩为一条可执行闭环:先确认主机层压力,再定位Memory Clerk,区分Buffer Pool与查询授予压力,最后进入优化和同窗口复测。
图:内存压力诊断与优化示例流程;阈值、权限和变更动作以目标实例证据为准。
八、分层优化:先缩小需求,再调整容量
8.1 即时动作
- 暂停非必要的大型报表、批量导入和全表扫描;
- 对正在等待内存的请求确认业务影响,避免直接批量KILL;
- 保存当前DMV、等待和操作系统证据;
- 对单个查询使用临时的资源治理措施,并记录回退时间。
8.2 查询与索引层优化
- 修正严重失真的统计信息;
- 缩小投影列和扫描范围;
- 优化排序、哈希连接和聚合输入;
- 避免把大结果集一次性返回给应用;
- 检查隐式转换、函数包列和参数敏感性;
- 结合Query Store确认优化前后是否真正改善。
8.3 实例配置层优化
-- 查看当前值;变更前先记录原值
EXEC sys.sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sys.sp_configure 'max server memory (MB)';
EXEC sys.sp_configure 'min server memory (MB)';
调整 max server memory 时,应按主机总内存、操作系统需求、SQL Server版本、其他实例和扩展组件估算,而不是套用一个固定比例。变更后要观察操作系统可用内存、SQL Server进程内存、PLE、物理读和应用延迟。
8.4 版本能力层优化
SQL Server 2017、2019和2022对内存授予反馈、批处理模式、自适应处理和参数敏感计划等能力不同。
内存授予反馈的版本演进:
- SQL Server 2017:批处理模式内存授予反馈,数据库兼容级别140;
- SQL Server 2019:扩展到行模式,数据库兼容级别150;
- SQL Server 2022:在兼容级别160下提供百分位数反馈和持久化反馈能力。
先确认数据库级配置,再用Query Store观察反馈前后的最大使用内存和执行时间(注意 sys.query_store_runtime_stats 中内存列的单位为 8 KB 页数,乘以 8 折算为 KB):
SELECT
q.query_id,
p.plan_id,
rs.runtime_stats_interval_id,
rs.avg_query_max_used_memory * 8 AS avg_query_max_used_memory_kb,
rs.min_query_max_used_memory * 8 AS min_query_max_used_memory_kb,
rs.max_query_max_used_memory * 8 AS max_query_max_used_memory_kb,
rs.avg_duration,
rs.avg_logical_io_reads
FROM sys.query_store_query AS q
JOIN sys.query_store_plan AS p
ON p.query_id = q.query_id
JOIN sys.query_store_runtime_stats AS rs
ON rs.plan_id = p.plan_id
WHERE rs.avg_query_max_used_memory IS NOT NULL
ORDER BY rs.max_query_max_used_memory DESC;
如何解释样例: 如果同一query_id的avg_query_max_used_memory_kb从12,800 KB降到3,200 KB,同时逻辑读和持续时间没有恶化,说明反馈可能改善了授予;如果授予下降但溢出事件增加,则不能把“授予变小”直接视为成功。
启用相关能力前要确认:
- 数据库兼容级别;
- 目标版本和补丁;
- Query Store是否可用并已配置容量;
- 是否存在计划回归和异常内存授予;
- 是否准备好监控和回退。
九、实战测算:从“内存不足”回到具体证据
案例1:PLE下降,但根因是批量扫描
现象: 高峰期PLE快速下降,物理读和存储请求升高。
排查: Memory Clerk显示Buffer Pool占比没有异常增长;Query Store发现某报表在高峰期开始大范围扫描,逻辑读显著增加。
处理: 先优化报表谓词和索引,调整执行窗口,再评估是否需要增加内存。
结论: PLE下降是缓存被工作负载快速置换的表现,不能直接等价于内存容量不足。
案例2:RESOURCE_SEMAPHORE来自错误估算
现象: 少量查询等待内存,其他查询响应时间一起升高。
排查: sys.dm_exec_query_memory_grants显示单个查询申请的内存远高于实际使用;执行计划中估算行数与实际行数差异很大。
处理: 修正统计信息和谓词,减少不必要的列,复测内存授予和溢出;只有在查询本身合理且工作负载规模真实增长时,才评估实例容量。
案例3:内存配置过满导致操作系统压力
现象: SQL Server看起来拥有足够内存,但系统出现分页和其他服务不稳定。
排查: max server memory配置接近主机容量,备份代理和监控组件没有保留空间。
处理: 先下调上限并观察系统可用内存,再为其他进程建立容量预算。
9.4 示例输出:如何避免把一个数字当成结论
| 证据 | 示例值 | 初步结论 | 还需要补充 |
|---|---|---|---|
| PLE汇总 | 420 → 95 | 缓存页被快速置换 | NUMA节点、物理读、扫描查询 |
RESOURCE_SEMAPHORE增量 | 18次 / 60秒 | 查询授予出现排队 | 请求大小、估算与实际行数、并发 |
MEMORYCLERK_SQLBUFFERPOOL | 24 GB | Buffer Pool占用较大 | 主机余量、工作集、读延迟 |
CACHESTORE_SQLCP增长 | +4 GB / 10分钟 | 可能有计划复用问题 | 即席SQL比例、参数化和Clock Hands |
这组样例只用于展示判读顺序,不代表某个生产实例的实测结果。
十、内存诊断检查清单
- 是否有正常期和故障期基线?
- 操作系统可用内存是否同步下降?
- SQL Server进程是否报告物理或虚拟内存压力?
- 哪些Memory Clerk在增长?增长速度如何?
- Buffer Pool压力是否伴随物理读、Lazy Writes和Free List Stalls?
- 是否存在
RESOURCE_SEMAPHORE?等待者申请了多少内存? - 是否有排序、哈希溢出或错误估算?
max server memory是否给操作系统和其他进程留出空间?- 版本、补丁和兼容级别是否支持计划采用的自动调优能力?
- 每项变更是否有复测指标和回退条件?
十一、总结
内存诊断的关键不是找到一个“神奇阈值”,而是把压力拆成可验证的链路:
- 用操作系统和进程 DMV确认外部压力;
- 用Memory Clerk确认SQL Server内部内存去向;
- 用内存授予DMV和等待类型识别查询执行压力;
- 用PLE、物理读和工作负载变化判断Buffer Pool是否真的承压;
- 用Query Store、执行计划和扩展事件完成优化前后对照。
当内存问题被拆成这些证据后,扩容才是容量规划的一部分,而不是默认答案。下一篇将把诊断视线从内存转向存储:SQL Server I/O等待类型、文件级延迟以及数据文件、日志文件和tempdb的配置边界。
系列文章预告
本文是“SQL Server性能优化实战系列”的第3篇,后续文章:
- 第4篇: SQL Server I/O性能优化:从等待类型到存储配置
- 第5篇: SQL Server扩展事件(Extended Events)完全指南
- 第6篇: tempdb性能优化完全指南:从空间管理到并发争用
关于作者
Master Xu,20+ 年IT、通信、电信、医疗、制造行业经验,企业级数据库架构师。
擅长企业IT架构规划、数据库性能优化、大型复杂项目交付。
欢迎交流技术话题:
- Email: xcoolwinds@gmail.com
- 知乎: https://www.zhihu.com/people/da-feng-lue-guo-ping-yuan
- 掘金: https://juejin.cn/user/3735496826305865
- CSDN: https://blog.csdn.net/xcoolwinds
开放合作: 技术咨询 | 架构顾问 | 企业内训 | 技术分享
标签
#SQLServer #SQLServer性能优化 #内存管理 #BufferPool #DBA #数据库调优
声明: 本文所有脚本均在SQL Server 2016/2019/2022环境下测试通过,但在生产环境使用前请先在测试环境验证。
版权: 本文为原创文章,转载请注明出处。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)