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布局和工作负载下并不可靠。真正需要回答的是三个问题:

  1. SQL Server当前把内存用在了哪里?
  2. 当前压力来自操作系统、SQL Server内部组件,还是查询执行内存不足?
  3. 采取扩容、调整配置、优化查询还是治理工作负载,哪一种动作最可能解决根因?

本文沿用前两篇的诊断路径,先确认现象,再定位内存使用者,最后把处理动作分为即时、配置、查询和架构四个层次。文中脚本按 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_idpages_kblocked_page_allocations_kb判读
018,420,00017,900,000节点大部分内存由SQL Server锁定,需结合操作系统余量判断
117,980,00017,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_typeallocations_mbtarget_allocations_mballocations_kb_per_sec判读
MEMORYBROKER_FOR_RESERVE6,1444,096-18,000当前分配高于目标且速率为负,需结合内存授予等待确认是否正在收缩
MEMORYBROKER_FOR_CACHE23,50028,00035,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资源池上限、并发授予、查询估算
802Buffer 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 识别排序和哈希溢出

在可控的测试环境中,可以通过扩展事件捕获排序或哈希警告,也可以从执行计划查看相关警告。若发现大量溢出,应优先检查:

  1. 统计信息是否过期;
  2. 数据类型和谓词是否导致估算失真;
  3. 查询是否读取了不必要的列和行;
  4. 索引是否可以减少排序或哈希输入;
  5. 内存授予反馈是否适用于当前版本和兼容级别。

直接把 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与查询授予压力,最后进入优化和同窗口复测。SQL Server内存压力诊断与优化流程
图:内存压力诊断与优化示例流程;阈值、权限和变更动作以目标实例证据为准。

八、分层优化:先缩小需求,再调整容量

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_SQLBUFFERPOOL24 GBBuffer Pool占用较大主机余量、工作集、读延迟
CACHESTORE_SQLCP增长+4 GB / 10分钟可能有计划复用问题即席SQL比例、参数化和Clock Hands

这组样例只用于展示判读顺序,不代表某个生产实例的实测结果。

十、内存诊断检查清单

  1. 是否有正常期和故障期基线?
  2. 操作系统可用内存是否同步下降?
  3. SQL Server进程是否报告物理或虚拟内存压力?
  4. 哪些Memory Clerk在增长?增长速度如何?
  5. Buffer Pool压力是否伴随物理读、Lazy Writes和Free List Stalls?
  6. 是否存在 RESOURCE_SEMAPHORE?等待者申请了多少内存?
  7. 是否有排序、哈希溢出或错误估算?
  8. max server memory是否给操作系统和其他进程留出空间?
  9. 版本、补丁和兼容级别是否支持计划采用的自动调优能力?
  10. 每项变更是否有复测指标和回退条件?

十一、总结

内存诊断的关键不是找到一个“神奇阈值”,而是把压力拆成可验证的链路:

  • 用操作系统和进程 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环境下测试通过,但在生产环境使用前请先在测试环境验证。

版权: 本文为原创文章,转载请注明出处。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐