性能监控与查询调优
9.1 等待统计分析(Wait Statistics)
等待统计是 SQL Server 性能分析的第一入口。数据库引擎中的每一个等待都说明某个资源暂时不可用,分析等待类型可以精准定位瓶颈。
9.1.1 核心等待类型分类
| 类别 | 等待类型 | 含义 | 解决方向 |
|---|---|---|---|
| CPU | SOS_SCHEDULER_YIELD | CPU 竞争,线程主动让出 | 增加 CPU / 减少串行计划 |
| IO | PAGEIOLATCH_SH/EX | 等待从磁盘读取数据页 | 加 SSD、优化索引减少 IO |
| IO | WRITELOG | 等待日志写入到磁盘 | 日志盘使用 SSD、调整提交批次 |
| 锁 | LCK_M_X / LCK_M_S | 等待排他/共享锁 | 优化事务、开启 RCSI |
| 内存 | RESOURCE_SEMAPHORE | 等待查询内存授权 | 增加内存、优化查询内存使用 |
| 网络 | ASYNC_NETWORK_IO | 等待客户端接收数据 | 检查应用层处理速度 |
| TempDB | PAGELATCH_EX on TempDB | TempDB 分配页争用 | 增加 TempDB 文件数 |
| 并行 | CXPACKET / CXCONSUMER | 并行查询线程同步 | 调整 MAXDOP 和 CTFP |
-- 等待统计分析(自实例启动以来的累积数据)
-- 过滤掉良性空闲等待后按影响排序
SELECT TOP 30
wait_type,
waiting_tasks_count,
CAST(wait_time_ms / 1000.0 AS DECIMAL(12,2)) AS TotalWaitSec,
CAST(wait_time_ms / 1000.0 /
NULLIF(waiting_tasks_count, 0) AS DECIMAL(10,3)) AS AvgWaitMsPerTask,
CAST(signal_wait_time_ms * 100.0 /
NULLIF(wait_time_ms, 0) AS DECIMAL(5,2)) AS SignalWaitPct, -- CPU竞争占比
CAST(wait_time_ms * 100.0 /
SUM(wait_time_ms) OVER() AS DECIMAL(5,2)) AS PctOfTotal
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
'SLEEP_TASK','BROKER_TO_FLUSH','BROKER_TASK_STOP','CLR_AUTO_EVENT',
'DISPATCHER_QUEUE_SEMAPHORE','FT_IFTS_SCHEDULER_IDLE_WAIT',
'HADR_WORK_QUEUE','LAZYWRITER_SLEEP','LOGMGR_QUEUE',
'ONDEMAND_TASK_QUEUE','REQUEST_FOR_DEADLOCK_MONITOR','RESOURCE_QUEUE',
'SERVER_IDLE_CHECK','SLEEP_DBSTARTUP','SLEEP_DCOMSTARTUP','SLEEP_MASTERDBREADY',
'SLEEP_MASTERMDREADY','SLEEP_MASTERUPGRADED','SLEEP_MSDBSTARTUP',
'SLEEP_SYSTEMTASK','SLEEP_TEMPDBSTARTUP','SNI_HTTP_ACCEPT',
'SP_SERVER_DIAGNOSTICS_SLEEP','SQLAGENT_ALERT_SET','SQLAGENT_LOCK_MONITOR',
'SQLAGENT_NOTIFY','SQLAGENT_WAIT_ON_OPER','WAITFOR','XE_DISPATCHER_WAIT',
'XE_TIMER_EVENT','BROKER_EVENTHANDLER','CHECKPOINT_QUEUE',
'DBMIRROR_EVENTS_QUEUE','TRACEWRITE','XE_DISPATCHER_JOIN',
'WAIT_XTP_OFFLINE_CKPT_NEW_LOG','SQLTRACE_INCREMENTAL_FLUSH_SLEEP',
'SQLTRACE_BUFFER_FLUSH'
)
ORDER BY wait_time_ms DESC;
-- 重置等待统计(测试前清零)
DBCC SQLPERF('sys.dm_os_wait_stats', CLEAR);
9.2 动态管理视图(DMV)实战
9.2.1 最消耗资源的查询
-- TOP 消耗 CPU 的查询
SELECT TOP 20
qs.total_worker_time / qs.execution_count / 1000 AS AvgCPUMs,
qs.total_worker_time / 1000 AS TotalCPUMs,
qs.execution_count,
qs.total_logical_reads / qs.execution_count AS AvgLogicalReads,
qs.total_elapsed_time / qs.execution_count / 1000 AS AvgElapsedMs,
qs.creation_time AS PlanCreationTime,
SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text)
ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)+1) AS QueryText,
qp.query_plan
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qp
ORDER BY qs.total_worker_time DESC;
-- TOP 消耗 IO 的查询(逻辑读)
SELECT TOP 20
qs.total_logical_reads / qs.execution_count AS AvgLogicalReads,
qs.total_logical_reads,
qs.execution_count,
SUBSTRING(qt.text, (qs.statement_start_offset/2)+1,
((CASE qs.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text)
ELSE qs.statement_end_offset END - qs.statement_start_offset)/2)+1) AS QueryText
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
ORDER BY qs.total_logical_reads DESC;
-- 当前正在执行的请求
SELECT
r.session_id,
r.status,
r.blocking_session_id,
r.wait_type,
r.wait_time / 1000.0 AS WaitSec,
r.cpu_time / 1000.0 AS CPUSec,
r.total_elapsed_time / 1000.0 AS ElapsedSec,
r.logical_reads,
r.reads,
r.writes,
r.percent_complete,
s.login_name,
s.host_name,
s.program_name,
SUBSTRING(t.text, (r.statement_start_offset/2)+1, 200) AS CurrentSQL
FROM sys.dm_exec_requests r
JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE r.session_id > 50 AND r.session_id <> @@SPID
ORDER BY r.total_elapsed_time DESC;
9.2.2 Buffer Pool 分析
-- 查看哪些数据库的数据页占用了最多内存
SELECT
CASE WHEN database_id = 32767 THEN 'Resource DB'
ELSE DB_NAME(database_id)
END AS DatabaseName,
COUNT(*) * 8.0 / 1024 AS BufferPoolMB
FROM sys.dm_os_buffer_descriptors
GROUP BY database_id
ORDER BY COUNT(*) DESC;
-- 查看哪些表/索引占用了最多 Buffer Pool
SELECT TOP 20
DB_NAME(bd.database_id) AS DatabaseName,
OBJECT_NAME(i.object_id) AS TableName,
i.name AS IndexName,
i.type_desc AS IndexType,
COUNT(*) * 8.0 / 1024 AS BufferMB,
COUNT(*) * 100.0 / (SELECT COUNT(*) FROM sys.dm_os_buffer_descriptors
WHERE database_id = bd.database_id) AS PctOfDB
FROM sys.dm_os_buffer_descriptors bd
JOIN sys.allocation_units au ON bd.allocation_unit_id = au.allocation_unit_id
JOIN sys.partitions p ON (au.container_id = p.hobt_id)
JOIN sys.indexes i ON p.object_id = i.object_id AND p.index_id = i.index_id
WHERE bd.database_id = DB_ID()
AND bd.page_type IN ('DATA_PAGE', 'INDEX_PAGE')
GROUP BY bd.database_id, i.object_id, i.name, i.type_desc
ORDER BY COUNT(*) DESC;
-- 内存总览
SELECT
physical_memory_in_use_kb / 1024.0 AS SQLMemoryUsedMB,
locked_page_allocations_kb / 1024.0 AS LockedPagesMB,
virtual_address_space_committed_kb / 1024.0 AS VASCommittedMB,
memory_utilization_percentage
FROM sys.dm_os_process_memory;
9.3 查询存储(Query Store)
Query Store 是 SQL Server 2016+ 引入的查询历史数据库,可以追踪查询的执行计划变化和性能回归,是参数嗅探和计划回归问题的终极解决工具。
9.3.1 启用与配置 Query Store
-- 启用 Query Store
ALTER DATABASE [YourDB] SET QUERY_STORE = ON;
-- 详细配置
ALTER DATABASE [YourDB] SET QUERY_STORE (
OPERATION_MODE = READ_WRITE, -- READ_WRITE 收集;READ_ONLY 只读;OFF 关闭
CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 30), -- 保留30天
DATA_FLUSH_INTERVAL_SECONDS = 900, -- 每15分钟持久化到磁盘
INTERVAL_LENGTH_MINUTES = 60, -- 聚合间隔1小时
MAX_STORAGE_SIZE_MB = 1024, -- Query Store 最大占用1GB
QUERY_CAPTURE_MODE = AUTO, -- 自动:只捕获重要查询(推荐)
SIZE_BASED_CLEANUP_MODE = AUTO, -- 空间将满时自动清理旧数据
MAX_PLANS_PER_QUERY = 200, -- 每个查询保留最多200个计划
WAIT_STATS_CAPTURE_MODE = ON -- 同时收集等待统计(SQL 2017+)
);
-- 查看 Query Store 状态
SELECT
actual_state_desc,
readonly_reason,
current_storage_size_mb,
max_storage_size_mb,
flush_interval_seconds,
interval_length_minutes,
stale_query_threshold_days,
query_capture_mode_desc
FROM sys.database_query_store_options;
9.3.2 用 Query Store 识别和修复回归查询
-- 找出最近执行计划发生变化且性能下降的查询
WITH PlanStats AS (
SELECT
q.query_id,
p.plan_id,
p.last_force_failure_reason_desc,
rs.avg_duration / 1000.0 AS AvgDurationMs,
rs.avg_cpu_time / 1000.0 AS AvgCPUMs,
rs.avg_logical_io_reads AS AvgLogicalReads,
rs.count_executions,
rs.first_execution_time,
rs.last_execution_time,
ROW_NUMBER() OVER (PARTITION BY q.query_id ORDER BY rs.avg_duration DESC) AS RN
FROM sys.query_store_query q
JOIN sys.query_store_plan p ON q.query_id = p.query_id
JOIN sys.query_store_runtime_stats rs ON p.plan_id = rs.plan_id
WHERE rs.last_execution_time > DATEADD(DAY, -7, GETDATE())
)
SELECT
qt.query_sql_text,
ps.query_id,
ps.plan_id,
ps.AvgDurationMs,
ps.AvgCPUMs,
ps.AvgLogicalReads,
ps.count_executions
FROM PlanStats ps
JOIN sys.query_store_query q ON ps.query_id = q.query_id
JOIN sys.query_store_query_text qt ON q.query_text_id = qt.query_text_id
WHERE ps.RN = 1
ORDER BY ps.AvgDurationMs DESC;
-- 强制使用特定执行计划(修复参数嗅探/计划回归)
-- 1. 先找到好的计划 ID(从 Query Store 的报表或上面的查询获取)
-- 2. 强制该计划
EXEC sys.sp_query_store_force_plan @query_id = 42, @plan_id = 7;
-- 取消强制计划(恢复优化器自动选择)
EXEC sys.sp_query_store_unforce_plan @query_id = 42, @plan_id = 7;
-- 清理 Query Store 数据
EXEC sys.sp_query_store_flush_db; -- 将内存中的数据刷到磁盘
EXEC sys.sp_query_store_remove_plan (7); -- 删除特定计划
EXEC sys.sp_query_store_remove_query (42); -- 删除特定查询
ALTER DATABASE [YourDB] SET QUERY_STORE CLEAR; -- 清空所有数据(慎用)
9.4 扩展事件(Extended Events)
扩展事件(XEvents)是 SQL Server Profiler 的替代品,开销更低,功能更强大。
9.4.1 创建 XEvents 会话
-- 示例:捕获执行时间超过 1 秒的查询
CREATE EVENT SESSION [SlowQueries] ON SERVER
ADD EVENT sqlserver.sql_statement_completed (
ACTION (
sqlserver.sql_text,
sqlserver.database_name,
sqlserver.username,
sqlserver.client_hostname,
sqlserver.plan_handle
)
WHERE sqlserver.duration >= 1000000 -- 单位:微秒,1000000 = 1秒
AND sqlserver.database_id <> 32767 -- 排除 Resource DB
)
ADD TARGET package0.ring_buffer (
SET max_memory = 51200, -- 50MB 环形缓冲区
max_events_limit = 1000
)
ADD TARGET package0.event_file (
SET filename = 'D:\XEvents\SlowQueries.xel',
max_file_size = 100, -- 每个文件最大100MB
max_rollover_files = 5 -- 最多保留5个文件
)
WITH (
MAX_DISPATCH_LATENCY = 5 SECONDS,
EVENT_RETENTION_MODE = ALLOW_SINGLE_EVENT_LOSS,
TRACK_CAUSALITY = ON
);
-- 启动/停止会话
ALTER EVENT SESSION [SlowQueries] ON SERVER STATE = START;
ALTER EVENT SESSION [SlowQueries] ON SERVER STATE = STOP;
-- 读取 Ring Buffer 中的数据
SELECT
event_data.value('(event/@name)[1]', 'NVARCHAR(50)') AS EventName,
event_data.value('(event/@timestamp)[1]', 'DATETIME2') AS EventTime,
event_data.value('(event/data[@name="duration"]/value)[1]', 'BIGINT') / 1000 AS DurationMs,
event_data.value('(event/action[@name="sql_text"]/value)[1]', 'NVARCHAR(MAX)') AS SQLText,
event_data.value('(event/action[@name="database_name"]/value)[1]', 'NVARCHAR(100)') AS DBName,
event_data.value('(event/action[@name="username"]/value)[1]', 'NVARCHAR(100)') AS UserName
FROM (
SELECT CAST(target_data AS XML) AS TargetData
FROM sys.dm_xe_sessions s
JOIN sys.dm_xe_session_targets t ON s.address = t.event_session_address
WHERE s.name = 'SlowQueries' AND t.target_name = 'ring_buffer'
) AS Data
CROSS APPLY TargetData.nodes('//RingBufferTarget/event') AS XEventData(event_data)
ORDER BY EventTime DESC;
-- 读取 XEL 文件
SELECT
event_data.value('(event/@name)[1]', 'NVARCHAR(50)') AS EventName,
event_data.value('(event/@timestamp)[1]', 'DATETIME2') AS EventTime,
event_data.value('(event/data[@name="duration"]/value)[1]','BIGINT') / 1000 AS DurationMs,
event_data.value('(event/action[@name="sql_text"]/value)[1]','NVARCHAR(MAX)') AS SQLText
FROM sys.fn_xe_file_target_read_file('D:\XEvents\SlowQueries*.xel', NULL, NULL, NULL)
CROSS APPLY (SELECT CAST(event_data AS XML)) AS XData(event_data)
ORDER BY EventTime DESC;
9.5 内存优化表(In-Memory OLTP)
In-Memory OLTP(代号 Hekaton,SQL Server 2014+)将数据完全存储在内存中,使用乐观并发控制,可将 OLTP 性能提升 30~100 倍。
9.5.1 创建内存优化表
-- 1. 数据库需要有内存优化文件组
ALTER DATABASE [YourDB] ADD FILEGROUP IMOLTPFileGroup CONTAINS MEMORY_OPTIMIZED_DATA;
ALTER DATABASE [YourDB] ADD FILE (
NAME = 'IMOLTPData',
FILENAME = 'D:\Data\IMOLTPData'
) TO FILEGROUP IMOLTPFileGroup;
-- 2. 创建内存优化表(DURABILITY 决定是否持久化到磁盘)
CREATE TABLE dbo.SessionCache (
SessionID NVARCHAR(50) NOT NULL,
UserID INT NOT NULL,
Data NVARCHAR(2000) NULL,
CreatedAt DATETIME2 NOT NULL,
ExpiresAt DATETIME2 NOT NULL,
CONSTRAINT PK_SessionCache PRIMARY KEY NONCLUSTERED HASH (SessionID)
WITH (BUCKET_COUNT = 131072) -- 哈希索引桶数,设为预期行数的2倍(取2的幂)
) WITH (
MEMORY_OPTIMIZED = ON,
DURABILITY = SCHEMA_AND_DATA -- 持久化架构和数据(重启后不丢失)
-- DURABILITY = SCHEMA_ONLY -- 仅持久化架构(适合临时数据,重启后数据丢失)
);
-- 内存优化表的索引(只支持 HASH 和 RANGE 两种)
ALTER TABLE dbo.SessionCache
ADD INDEX IX_SessionCache_UserID NONCLUSTERED (UserID);
-- 3. 原生编译存储过程(Natively Compiled SP)
-- 编译为 DLL,绕过解释器,性能极致
CREATE PROCEDURE dbo.usp_SetSession
@SessionID NVARCHAR(50),
@UserID INT,
@Data NVARCHAR(2000),
@ExpiresAt DATETIME2
WITH NATIVE_COMPILATION, SCHEMABINDING, EXECUTE AS OWNER
AS BEGIN ATOMIC WITH (
TRANSACTION ISOLATION LEVEL = SNAPSHOT,
LANGUAGE = N'Simplified Chinese'
)
-- 原生编译 SP 内只能用有限的 T-SQL 子集
IF EXISTS (SELECT 1 FROM dbo.SessionCache WHERE SessionID = @SessionID)
BEGIN
UPDATE dbo.SessionCache
SET Data = @Data, ExpiresAt = @ExpiresAt
WHERE SessionID = @SessionID;
END
ELSE
BEGIN
INSERT INTO dbo.SessionCache (SessionID, UserID, Data, CreatedAt, ExpiresAt)
VALUES (@SessionID, @UserID, @Data, SYSDATETIME(), @ExpiresAt);
END;
END;
GO
9.5.2 延迟持久化(Delayed Durability)
对于可接受少量数据丢失但要求极高写入吞吐量的场景(如日志表),可启用延迟持久化。
-- 数据库级别启用延迟持久化
ALTER DATABASE [YourDB] SET DELAYED_DURABILITY = ALLOWED; -- 允许(由查询决定)
-- ALTER DATABASE [YourDB] SET DELAYED_DURABILITY = FORCED; -- 强制所有事务延迟持久
-- 事务级别启用
BEGIN TRANSACTION;
INSERT INTO dbo.EventLog (EventType, Message, LoggedAt)
VALUES ('INFO', 'User login', GETDATE());
COMMIT TRANSACTION WITH (DELAYED_DURABILITY = ON);
-- 日志写入在内存中聚集,每隔几ms或日志缓冲区满时才真正写磁盘
-- 风险:服务器崩溃时最后几毫秒的数据可能丢失
9.6 综合性能调优 SOP
-- ========================================
-- 每日性能巡检标准流程(运行一次)
-- ========================================
-- 1. 检查等待统计(找出当日最大瓶颈)
-- → 参考 9.1 节的等待统计查询
-- 2. 找出 TOP CPU/IO 查询
-- → 参考 9.2 节的 DMV 查询
-- 3. 检查是否有长时间运行的阻塞
SELECT session_id, blocking_session_id, wait_type, wait_time/1000 AS WaitSec
FROM sys.dm_exec_requests
WHERE blocking_session_id > 0;
-- 4. 检查 TempDB 使用情况
SELECT SUM(user_object_reserved_page_count) * 8 / 1024 AS UserObjMB,
SUM(internal_object_reserved_page_count) * 8 / 1024 AS InternalObjMB,
SUM(version_store_reserved_page_count) * 8 / 1024 AS VersionStoreMB
FROM sys.dm_db_file_space_usage;
-- 5. 检查索引碎片(每周一次即可)
-- → 参考 4.4 节的碎片检测查询
-- 6. 检查统计信息更新情况
SELECT
OBJECT_NAME(s.object_id) AS TableName,
s.name AS StatName,
sp.last_updated,
sp.rows,
sp.modification_counter,
CAST(sp.modification_counter * 100.0 / NULLIF(sp.rows, 0) AS DECIMAL(5,2)) AS ModPct
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE sp.modification_counter > 0.2 * sp.rows -- 修改超过20%触发重建
AND sp.rows > 1000
ORDER BY ModPct DESC;
-- 7. 检查 Query Store 的计划回归
-- → 在 SSMS 中查看数据库的 Query Store 报表
-- → "回归查询" 报表直观显示性能下降的查询
-- 8. 数据库文件剩余空间告警
SELECT
DB_NAME(database_id) AS DBName,
name AS FileName,
CAST(size * 8.0 / 1024 AS INT) AS FileSizeMB,
CAST(FILEPROPERTY(name, 'SpaceUsed') * 8.0 / 1024 AS INT) AS UsedMB,
CAST((size - FILEPROPERTY(name, 'SpaceUsed')) * 8.0 / 1024 AS INT) AS FreeMB,
CAST((size - FILEPROPERTY(name, 'SpaceUsed')) * 100.0 / NULLIF(size, 0) AS INT) AS FreePct
FROM sys.database_files
WHERE type = 0 -- 数据文件
HAVING CAST((size - FILEPROPERTY(name, 'SpaceUsed')) * 100.0 / NULLIF(size, 0) AS INT) < 20;
-- FreePct < 20% 时告警
9.7 小结
| 工具 | 主要用途 |
|---|---|
| 等待统计(Wait Stats) | 找到整体性能瓶颈类别(CPU/IO/锁/内存) |
| DMV(sys.dmexec*) | 定位具体消耗资源的查询 |
| Query Store | 追踪计划回归,强制使用好的执行计划 |
| Extended Events | 低开销精细捕获,替代 SQL Profiler |
| In-Memory OLTP | 高并发 OLTP 热点表的极限优化 |
| 延迟持久化 | 日志类写密集场景的吞吐提升 |
下一章:附录:常见报错与解决方案
文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有