SQL Server 15.0.2000.5(2019 CU5)性能分析基线构建实战教程
SQL Server 15.0.2000.5 是 SQL Server 2019 CU5 ,引入了 Intelligent Query Improvement(智能查询改进)、Batch Mode on Rowstore、T-SQL 增量编译等重磅特性。但要发挥这些优势,第一步永远是建立性能基线------你不知道现在的性能,就无法知道优化是否有效。本文用一篇文章讲透如何为 15.0.2000.5 构建完整的性能分析基线。

一句话总结
SQL Server 15.0.2000.5 性能基线的核心是多维度数据采集 + 历史对比 + 持续监控:通过查询存储(Query Store)+ 扩展事件(Extended Events)+ 系统视图(DMV/DMF)采集 CPU、IO、等待、执行计划等关键指标,与历史基线对比定位偏差,实现从"凭感觉调优"到"数据驱动调优"的转变。
一、为什么需要性能基线?
1.1 没有基线的困境
❌ "数据库好像变慢了" → 多慢?慢了多久?慢了多少?
❌ "最近查询有点卡" → 哪个查询?比以前慢多少?
❌ "CPU 突然很高" 多高?从什么时候开始?对比基线正常值是多少?
❌ "这个月报表明天就能出完" → 以前是不是半天就出了?为什么变慢了?
有了基线:
✅ 当前平均响应时间 2.3s,基线 0.8s,增长 187%
✅ Top CPU 查询 #SQL_1A2B 从基线 120ms 增加到 3.2s,增长率 2566%
✅ 最近 3 天 avg_cpu_percent 从 35% 上升到 82%,偏离基线 > 2σ
✅ 该趋势与上周新增索引失效的时间点吻合
1.2 性能基线覆盖的维度
| 维度 | 采集内容 | 工具 |
|---|---|---|
| CPU | avg_cpu_percent、批次数、SQL 统计 | sys.dm_os_performance_counters |
| 内存 | buffer cache hit ratio、page life expectancy | sys.dm_os_performance_counters |
| IO | IOPS、延迟、吞吐 | sys.dm_io_virtual_file_stats |
| 等待 | 类型、数量、持续时间 | sys.dm_os_wait_stats |
| 查询 | 执行计划、逻辑读、物理读、运行时间 | Query Store、sys.dm_exec_query_stats |
| 锁 | 阻塞链、死锁、锁等待 | 扩展事件、sp_whoisactive |
| 统计信息 | 更新时间、陈旧度 | sys.dm_db_stats_properties |
二、版本确认与环境检查
2.1 确认版本信息
sql
-- 查看完整版本信息
SELECT @@VERSION;
-- 预期输出包含: Microsoft SQL Server 2019 (RTM-CU5) ... 15.0.2000.5
-- 更精确的版本号
SELECT SERVERPROPERTY('ProductVersion') AS ProductVersion,
SERVERPROPERTY('ProductLevel') AS ProductLevel,
SERVERPROPERTY('Edition') AS Edition,
SERVERPROPERTY('EngineEdition') AS EngineEdition,
SERVERPROPERTY('IsClustered') AS IsClustered,
SERVERPROPERTY('InstanceName') AS InstanceName;
-- 列出所有累积更新热修复(CU hotfixes)
SELECT * FROM sys.dm_os_build_info;
2.2 检查兼容性级别
sql
-- 兼容性级别影响查询优化器行为
SELECT name, compatibility_level,
CASE compatibility_level
WHEN 150 THEN 'SQL Server 2019'
WHEN 140 THEN 'SQL Server 2017'
WHEN 130 THEN 'SQL Server 2016'
WHEN 120 THEN 'SQL Server 2014'
END AS CompatibilityDesc
FROM sys.databases;
15.0.2000.5 注意:SQL Server 2019 默认兼容性级别为 150,启用了智能查询改进等全部新特性。不要为了兼容降级到 140!
2.3 环境健康检查
sql
-- 检查数据库状态
SELECT name, state_desc, recovery_model_desc,
page_verify_option_desc, is_auto_create_stats_on,
is_auto_update_stats_on, is_parameterization_forced
FROM sys.databases
WHERE name NOT IN ('master','tempdb','model','msdb');
-- 检查内存状态
SELECT total_physical_memory_kb/1024/1024 AS Total_Physical_GB,
available_physical_memory_kb/1024/1024 AS Available_Physical_GB
FROM sys.dm_os_sys_memory;
-- 检查 CPU 配置
SELECT cpu_count, hyperthread_ratio, scheduler_count
FROM sys.dm_os_sys_info;
-- 检查 IO 子系统
SELECT COUNT(DISTANCE physical_name),
MAX(read_latency_ms) AS max_read_lat,
MAX(write_latency_ms) AS max_write_lat
FROM sys.dm_io_virtual_file_stats(NULL, NULL);
三、建立性能计数器基线
3.1 关键性能计数器
sql
-- =============================================
-- 采集 SQL Server 2019 CU5 关键性能计数器
-- =============================================
-- 1. CPU 相关
SELECT counter_name, cntr_value, cntr_type
FROM sys.dm_os_performance_counters
WHERE counter_name IN (
'SQL Compilations/sec',
'SQL Re-Compilations/sec',
'Batch Requests/sec',
'SQL Attention rate',
'Lazy Writes/sec'
);
-- 计算编译率(每秒编译数 / 每秒批处理请求数)
SELECT CAST(cntr_compilations.cntr_value AS FLOAT) / NULLIF(cntr_batches.cntr_value, 0)
AS Compilation_Ratio
FROM sys.dm_os_performance_counters cntr_compilations
JOIN sys.dm_os_performance_counters cntr_batches
ON cntr_compilations.counter_name = 'SQL Compilations/sec'
AND cntr_batches.counter_name = 'Batch Requests/sec';
-- 正常值:< 0.1(软解析比例高)
-- 2. Buffer Manager
SELECT counter_name, cntr_value
FROM sys.dm_os_performance_counters
WHERE counter_name IN (
'Buffer cache hit ratio',
'Page life expectancy',
'Checkpoint pages/sec',
'Lazy writes/sec',
'Readahead pages/sec',
'Free list stalls/sec',
'Stolen server memory',
'Target server memory'
);
-- Page Life Expectancy (PLE) --- 低于 300 需要关注
SELECT CAST(cntr_value AS BIGINT) AS PLE_Seconds
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Page life expectancy';
-- Buffer Cache Hit Ratio --- 应接近 100
SELECT TOP 1
cntr_value AS Buffer_Cache_Hit_Ratio
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Buffer cache hit ratio';
-- 3. Memory Broker
SELECT memory_clerk_type, SUM(pages_kb)/1024/1024 AS Memory_MB
FROM sys.dm_os_memory_clerks
GROUP BY memory_clerk_type
ORDER BY 2 DESC;
-- 4. Latch 等待
SELECT latch_class, waiting_requests, wait_time_ms,
CAST(wait_time_ms/(NULLIF(waiting_requests,0)*1.0) AS DECIMAL(10,2)) AS Avg_Wait_ms
FROM sys.dm_os_latch_stats
WHERE waiting_requests > 0
ORDER BY wait_time_ms DESC;
-- 5. Locks
SELECT resource_type, request_mode, request_status, COUNT(*) AS Count
FROM sys.dm_tran_locks
GROUP BY resource_type, request_mode, request_status
ORDER BY Count DESC;
3.2 自动化计数器采集脚本
sql
-- =============================================
-- 创建性能计数器采集表
-- =============================================
USE DBA_Monitor;
GO
CREATE TABLE PerfCounters_Baseline (
CollectionTime DATETIME2(0) DEFAULT SYSUTCDATETIME(),
CounterName NVARCHAR(512),
CounterValue BIGINT,
CounterType INT
);
GO
-- =============================================
-- 创建采集作业(SQL Agent Job)
-- =============================================
-- 建议每 15 分钟采集一次,保留 90 天数据
-- =============================================
DECLARE @SQL NVARCHAR(MAX) = N'
INSERT INTO DBA_Monitor.dbo.PerfCounters_Baseline (CounterName, CounterValue, CounterType)
SELECT counter_name, cntr_value, cntr_type
FROM sys.dm_os_performance_counters
WHERE counter_name IN (
''Page life expectancy'',
''Buffer cache hit ratio'',
''Lazy writes/sec'',
''Checkpoints/sec'',
''SQL Compilations/sec'',
''SQL Re-Compilations/sec'',
''Batch Requests/sec'',
''Free list stalls/sec'',
''Lazy writes/sec'',
''Readahead pages/sec'',
''Active temporary databases'',
''Log bytes flushed/sec'',
''Transactions/sec''
)';
-- 使用 sp_add_jobstep 添加步骤
-- EXEC msdb.dbo.sp_add_jobstep ...
四、建立等待事件基线
4.1 采集等待统计
sql
-- =============================================
-- SQL Server 2019 CU5 等待事件基线采集
-- =============================================
-- 1. 整体等待统计(自上次重启以来)
WITH WaitStats AS (
SELECT
ws.wait_type,
ws.waiting_tasks_count,
CAST(ws.wait_time_ms / 1000.0 AS DECIMAL(12,2)) AS Wait_S,
CAST(ws.max_wait_time_ms / 1000.0 AS DECIMAL(12,2)) AS MaxWait_S,
CAST(ws.signal_wait_time_ms / 1000.0 AS DECIMAL(12,2)) AS Signal_S,
CAST((ws.wait_time_ms - ws.signal_wait_time_ms) / 1000.0 AS DECIMAL(12,2)) AS Resource_S,
CAST(100.0 * ws.wait_time_ms / SUM(ws.wait_time_ms) OVER () AS DECIMAL(10,2)) AS Pct,
CAST(100.0 * ws.signal_wait_time_ms / NULLIF(ws.wait_time_ms, 0) AS DECIMAL(10,2)) AS SignalPct
FROM sys.dm_os_wait_stats ws
WHERE ws.wait_type NOT IN (
-- 忽略 idle/waitable 类型等待(它们代表空闲时间)
N'BROKER_EVENTWAIT',N'BROKER_RECEIVE_WAITFOR',N'BROKER_TASK_STOP',
N'BROKER_TO_FLUSH',N'BROKER_TRANSMITTER',N'CHECKPOINT_QUEUE',
N'CHKPT',N'CLR_AUTO_EVENT',N'CLR_MANUAL_EVENT',N'CLR_SEMAPHORE',
N'DBMIRROR_DBM_EVENT',N'DBMIRROR_EVENTS_QUEUE',N'DBMIRROR_WORKER_WAIT',
N'DDR_FRAME_WORK',N'DDR_SOURCE_RECOVERY_THREAD',N'DDR_TARGET_RECOVERY_THREAD',
N'DDR_WORKER',N'DDS_LOCK',N'DDS_NETWORK_ISSUES',
N'KSOURCE_WAKEUP',N'LAZYWRITER_SLEEP',N'LOGMGR_QUEUE',
N'MEMORY_ALLOCATION_EXT',N'ONDEMAND_TASK_QUEUE',
N'PREEMPTIVE_XE_CALLBACKEXECUTE',N'PREEMPTIVE_XE_DISPATCHER',
N'PREEMPTIVE_XE_GETTARGETSTATE',N'PREEMPTIVE_XE_SESSIONCOMMIT',
N'PREEMPTIVE_XE_TARGETINIT',N'PREEMPTIVE_XE_TARGETFINALIZE',
N'PBWORKER_STOPAND_WAIT',N'PNUTILS','PWAIT_ALL_COMPONENTS_INITIALIZED',
N'QUEUEDPOSTREP_SUPPORT_THREAD',
N'QDS_CLEANUP_STALE_TRANSTABLE',N'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP',
N'QDS_ASYNC_QUEUE',N'QDS_SHUTDOWN_QUEUE',
N'REDO_THREAD_PENDING_WORK',N'REQUEST_FOR_DEADLOCK_SEARCH',
N'RESOURCE_GATEAWAY',N'SERVER_IDLE_CHECK',N'SLEEP_BPOOL_FLUSH',
N'SLEEP_DBSTARTUP',N'SLEEP_DCOMSTARTUP',N'SLEEP_MASTERDBREADY',
N'SLEEP_MASTERMDREADY',N'SLEEP_MASTERUPGRADED',N'SLEEP_MSDBSTARTUP',
N'SLEEP_SYSTEMTASK',N'SLEEP_TASK',N'SLEEP_TEMPDBSTARTUP',
N'SNI_HTTP_ACCEPT',N'SOS_WORKDispatcher',N'SP_SERVER_DIAGNOSTICS_SLEEP',
N'SQLTRACE_BUFFER_FLUSH',N'SQLTRACE_INCREMENTAL_FLUSH_SLEEP',
N'SQLTRACE_WAIT_ENTRIES',N'STARTUP_DEPENDENCY_MANAGER',
N'TASK_EXELOOP',N'TEAMMER_COLLECTOR_SUSPEND',N'TEAMMER_JOB_SUSPEND',
N'TEAMMER_WORKER_SUSPEND',N'TRACEWRITE',
N'WTI_CLIENT_FAILOVER_LISTENER_LISTEN',
N'XE_DISPATCHER_WAIT',N'XE_LIVE_TARGET_TVF',N'XE_TIMER_EVENT'
)
)
SELECT
ROW_NUMBER() OVER (ORDER BY wait_time_ms DESC) AS RowNum,
wait_type AS WaitingEventType,
waiting_tasks_count AS EventCount,
Wait_S AS SecondsWaiting,
MaxWait_S AS MaxSecondsPerEvent,
Resource_S AS ResourceTime_S,
Signal_S AS SignalTime_S,
Pct,
SignalPct,
CASE
WHEN wait_type LIKE N'LCK%' THEN 'Lock/Latch'
WHEN wait_type LIKE N'PAGE%LATCH%' THEN 'Latch'
WHEN wait_type LIKE N'PAGEIO_LATCH%' OR wait_type LIKE N'PAGELATCH%' THEN 'Latch'
WHEN wait_type LIKE N'ASYNC_%' OR wait_type LIKE N'BMK%' THEN 'Background'
WHEN wait_type IN (N'CXPACKET',N'CMEMTHREAD',N'CXCONSUMER') THEN 'Parallelism'
WHEN wait_type LIKE N'WRITELOG' OR wait_type LIKE N'LOGBUFFER%' THEN 'LogWriter'
WHEN wait_type LIKE N'PAGEIOLATCH%' THEN 'DiskIO'
ELSE 'Other'
END AS Category
FROM WaitStats
WHERE waiting_tasks_count > 0
ORDER BY wait_time_ms DESC;
4.2 关键等待事件解读(15.0.2000.5 环境)
| 等待类型 | 含义 | 正常阈值 | 优化方向 |
|---|---|---|---|
| PAGEIOLATCH_SH/EX | 数据页读取到内存等待 | < 10% | 加内存、改善 IO |
| PAGEIOLATCH_DT | 脏页写入磁盘等待 | < 5% | 改善 IO 子系统 |
| WRITELOG | Redo 日志写入等待 | < 10% | 优化事务、RAID10、独立日志盘 |
| CXPACKET | 并行查询协调等待 | < 25% | 调整 MAXDOP、优化导致并行的查询 |
| *LCK_M_系列 | 锁等待 | < 5% | 优化事务隔离、减少锁粒度 |
| SOS_SCHEDULER_YIELD | CPU 调度器让步 | < 25% | CPU 过载,优化 SQL |
| RESOURCE_SEMAPHORE | 内存查询 grant 等待 | < 5% | 调整 max server memory、优化 SQL |
| THREADPOOL | 线程池耗尽 | 任何 > 0 需立即关注 | 增加 worker_threads、排查长时间运行查询 |
4.3 基线存储与趋势分析
sql
-- 创建等待统计基线表
USE DBA_Monitor;
GO
CREATE TABLE WaitStats_Baseline (
CollectionTime DATETIME2(0) DEFAULT SYSUTCDATETIME(),
InstanceName SYSNAME DEFAULT @@SERVERNAME,
WaitType NVARCHAR(60),
WaitingTasksCount BIGINT,
WaitTimeMS BIGINT,
MaxWaitTimeMS BIGINT,
SignalWaitMS BIGINT
);
GO
-- 采样采集存储过程
CREATE OR ALTER PROCEDURE CollectWaitStatsSnapshot
AS
BEGIN
SET NOCOUNT ON;
DECLARE @SampleTime DATETIME2(0) = SYSUTCDATETIME();
-- 插入快照数据(累计值,供后续差分计算)
INSERT INTO DBA_Monitor.dbo.WaitStats_Baseline (
CollectionTime, InstanceName, WaitType,
WaitingTasksCount, WaitTimeMS, MaxWaitTimeMS, SignalWaitMS
)
SELECT
@SampleTime,
@@SERVERNAME,
ws.wait_type,
ws.waiting_tasks_count,
ws.wait_time_ms,
ws.max_wait_time_ms,
ws.signal_wait_time_ms
FROM sys.dm_os_wait_stats ws
WHERE ws.wait_type NOT IN (/* 同上忽略列表 */);
END;
GO
-- 趋势分析查询(对比最近7天和基线平均值)
SELECT
wb.WaitType,
AVG(wb.WaitTimeMS) AS Baseline_Avg_PerMin,
MAX(wb.WaitTimeMS) AS Baseline_Max_PerMin,
-- 最近7天平均
(SELECT AVG(WaitTimeMS) FROM DBA_Monitor.dbo.WaitStats_Baseline wbi
WHERE wbi.WaitType = wb.WaitType
AND wbi.CollectionTime >= DATEADD(DAY, -7, @CurrentTime)) AS Last7Day_Avg_PerMin,
-- 偏离度
CAST((
(SELECT AVG(WaitTimeMS) FROM DBA_Monitor.dbo.WaitStats_Baseline wbi
WHERE wbi.WaitType = wb.WaitType
AND wbi.CollectionTime >= DATEADD(DAY, -7, @CurrentTime))
/ NULLIF(AVG(wb.WaitTimeMS), 0)
) * 100 AS DECIMAL(5,1)) AS DeviationFromBaseline_Pct
FROM DBA_Monitor.dbo.WaitStats_Baseline wb
WHERE wb.CollectionTime >= DATEADD(DAY, -90, @CurrentTime)
GROUP BY wb.WaitType
HAVING SUM(CASE WHEN wb.CollectionTime >= DATEADD(DAY, -7, @CurrentTime) THEN 1 ELSE 0 END) > 0
AND SUM(CASE WHEN wb.CollectionTime < DATEADD(DAY, -7, @CurrentTime) THEN 1 ELSE 0 END) > 0
ORDER BY DeviationFromBaseline_Pct DESC;
五、查询存储(Query Store)基线
5.1 为什么用 Query Store 做基线?
SQL Server 2016+ 引入 Query Store,15.0.2000.5 中进一步增强。它是最适合做查询性能基线的工具,因为它自动记录每个查询的执行计划、运行时统计和性能指标,支持按时间对比。
5.2 启用并配置 Query Store
sql
-- =============================================
-- 为业务数据库启用 Query Store(以 OLTP 为例)
-- =============================================
ALTER DATABASE OLTP
SET QUERY_STORE = ON;
ALTER DATABASE OLTP
SET QUERY_STORE (
OPERATION_MODE = READ_WRITE, -- 采集模式
CLEANUP_POLICY = (STALE_QUERY_THRESHOLD_DAYS = 90), -- 数据保留天数
DATA_FLUSH_INTERVAL_SECONDS = 900, -- 每 15 分钟刷新一次
INTERVAL_LENGTH_MINUTES = 60, -- 按小时聚合
MAX_STORAGE_SIZE_MB = 512, -- 最大存储
SIZE_BASED_CLEANUP_MODE = AUTO, -- 自动清理
QUERY_CAPTURE_MODE = AUTO, -- 自动捕获(可改 ALL 或 NONE)
MAX_PLANS_PER_QUERY = 200, -- 单查询最多存储计划数
WAIT_STATS_CAPTURE_MODE = ON -- 同时捕获等待统计
);
-- 验证
SELECT name, actual_state, actual_state_desc, desired_state, desired_state_desc,
readonly_reason, current_storage_size_mb, max_storage_size_mb,
query_capture_mode_desc, stale_query_threshold_days
FROM sys.database_query_store_config;
5.3 查询基线中的异常查询
sql
-- =============================================
-- 查询存储基线分析查询
-- =============================================
USE OLTP;
GO
-- 1. Top 最耗时间的查询(对比两个时间段)
-- 查找最近 7 天比之前 7 天平均响应时间增加最多的查询
DECLARE @StartBaseline DATETIME2 = DATEADD(WEEK, -2, SYSUTDGETIME());
DECLARE @EndBaseline DATETIME2 = DATEADD(WEEK, -1, SYSUTDGETIME());
DECLARE @StartCurrent DATETIME2 = DATEADD(WEEK, -1, SYSUTDGETIME());
DECLARE @EndCurrent DATETIME2 = SYSUTDGETIME();
-- 最近 vs 之前:查询平均 duration 变化最大的前 20 条
SELECT TOP 20
qsq.query_id,
qsqt.query_sql_text AS QueryText,
-- 之前基线平均
AVG(CASE WHEN qrm.start_time >= @StartBaseline AND qrm.start_time < @EndBaseline
THEN qrm.avg_logical_io_reads END) AS Baseline_AvgReads,
-- 当前平均
AVG(CASE WHEN qrm.start_time >= @StartCurrent AND qrm.start_time <= @EndCurrent
THEN qrm.avg_logical_io_reads END) AS Current_AvgReads,
-- 之前基线平均 duration(ms)
AVG(CASE WHEN qrm.start_time >= @StartBaseline AND qrm.start_time < @EndBaseline
THEN qrm.avg_duration END) AS Baseline_AvgDuration_MS,
-- 当前平均 duration(ms)
AVG(CASE WHEN qrm.start_time >= @StartCurrent AND qrm.start_time <= @EndCurrent
THEN qrm.avg_duration END) AS Current_AvgDuration_MS,
-- 执行次数变化
SUM(CASE WHEN qrm.start_time >= @StartBaseline AND qrm.start_time < @EndBaseline
THEN qrm.count_executions ELSE 0 END) AS Baseline_ExecCount,
SUM(CASE WHEN qrm.start_time >= @StartCurrent AND qrm.start_time <= @EndCurrent
THEN qrm.count_executions ELSE 0 END) AS Current_ExecCount
FROM sys.query_store_runtime_metric_range qmr
JOIN sys.query_store_runtime_metric qrm ON qmr.metric_id = qrm.metric_id
AND qmr.query_runtime_metric_range_id = qmr.query_runtime_metric_range_id
JOIN sys.query_store_query qsq ON qrm.query_id = qsq.query_id
JOIN sys.query_store_query_text qsqt ON qsq.query_text_id = qsqt.query_text_id
WHERE qmr.metric_id IN (1, 2) -- 1 = Duration, 2 = LogicalIOReads
AND qmr.range_start_time >= @StartBaseline
AND qmr.range_end_time <= @EndCurrent
AND qsq.is_internal_object = 0
GROUP BY qsq.query_id, qsqt.query_sql_text
HAVING SUM(CASE WHEN qrm.start_time >= @StartCurrent AND qrm.start_time <= @EndCurrent
THEN qrm.count_executions ELSE 0 END) > 0
ORDER BY
(AVG(CASE WHEN qrm.start_time >= @StartCurrent AND qrm.start_time <= @EndCurrent
THEN qrm.avg_duration END)
- AVG(CASE WHEN qrm.start_time >= @StartBaseline AND qrm.start_time < @EndBaseline
THEN qrm.avg_duration END)) DESC;
5.4 检测执行计划回归
sql
-- 查找同一查询有多个执行计划且性能退化的
SELECT
qsqt.query_sql_text,
qsq.query_id,
qsp.plan_id,
qsp.is_compatible_plan,
qsp.last_execution_time,
qsp.avg_logical_io_reads,
qsp.avg_duration,
qsp.count_executions,
qsp.avg_cpu_time,
qsp.first_compiler_version,
CASE
WHEN qsp.first_compiler_version LIKE '15.0.2000%' THEN 'SQL Server 2019 CU5'
WHEN qsp.first_compiler_version LIKE '15.0.1%' THEN 'SQL Server 2019 RTM-CU1'
ELSE qsp.first_compiler_version
END AS CompilerVersion
FROM sys.query_store_plan qsp
JOIN sys.query_store_query qsq ON qsp.query_id = qsq.query_id
JOIN sys.query_store_query_text qsqt ON qsq.query_text_id = qsqt.query_text_id
WHERE qsq.is_internal_object = 0
ORDER BY qsp.avg_duration DESC;
-- 强制使用已知良好的执行计划(如果检测到退化)
-- EXEC sp_query_store_force_plan @query_id = XX, @plan_id = YY;
-- EXEC sp_query_store_unforce_plan @plan_id = YY;
六、执行计划基线
6.1 采集当前活跃查询的执行计划
sql
-- =============================================
-- 实时查询活动监控(快照)
-- =============================================
SELECT
req.session_id,
req.blocking_session_id,
EST.text AS QueryText,
req.status,
SUBSTRING(EST.text, (req.statement_start_offset/2)+1,
((CASE req.statement_end_offset
WHEN -1 THEN DATALENGTH(EST.text)
ELSE req.statement_end_offset
END - req.statement_start_offset)/2)+1) AS StatementText,
DATEDIFF(SECOND, req.start_time, GETDATE()) AS Elapsed_Sec,
req.cpu_time,
req.total_elapsed_time,
req.logical_reads,
req.reads,
req.writes,
req.granted_query_memory_pages,
req.wait_type,
req.wait_time,
req.last_wait_type,
r.plan_handle,
qp.query_plan AS XMLExecutionPlan,
DB_NAME(req.database_id) AS DatabaseName,
t.task_alloc_mb,
t.task_dealloc_mb,
ses.program_name,
ses.host_name,
ses.login_name
FROM sys.dm_exec_requests req
CROSS APPLY sys.dm_exec_sql_text(req.sql_handle) EST
OUTER APPLY sys.dm_exec_query_plan(r.plan_handle) qp
LEFT JOIN (
SELECT session_id,
SUM(user_objects_alloc_page_count - user_objects_dealloc_page_count) * 8 / 1024 AS task_alloc_mb,
SUM(internal_objects_alloc_page_count - internal_objects_dealloc_page_count) * 8 / 1024 AS task_dealloc_mb
FROM sys.dm_db_task_space_usage
GROUP BY session_id
) t ON req.session_id = t.session_id
JOIN sys.dm_exec_sessions ses ON req.session_id = ses.session_id
WHERE req.session_id > 50 -- 排除系统会话
AND req.command NOT IN ('BACKGROUND', 'LAZY WRITER', 'CHECKPOINT')
ORDER BY req.cpu_time DESC, req.total_elapsed_time DESC;
6.2 长期执行计划统计基线
sql
-- =============================================
-- 查询性能统计基线(基于 dm_exec_query_stats)
-- 这是最核心的性能基线数据来源
-- =============================================
-- Top 20 最耗资源的 SQL(按平均 CPU 时间)
SELECT TOP 20
sql_txt.text AS SQL_Text,
qt.query_plan AS Execution_Plan,
qs.creation_time AS Plan_Created,
qs.last_execution_time AS Last_Run,
qs.execution_count AS Execute_Count,
CAST((qs.total_elapsed_time / 1000000.0) / NULLIF(qs.execution_count, 0) AS DECIMAL(12,2)) AS Avg_Elapsed_Sec,
CAST((qs.total_worker_time / 1000000.0) / NULLIF(qs.execution_count, 0) AS DECIMAL(12,2)) AS Avg_CPU_Sec,
CAST((qs.total_elapsed_time / 1000000.0) AS DECIMAL(12,2)) AS Total_Elapsed_Sec,
CAST((qs.total_worker_time / 1000000.0) AS DECIMAL(12,2)) AS Total_CPU_Sec,
qs.min_worker_time / 1000000.0 AS Min_CPU_Sec,
qs.max_worker_time / 1000000.0 AS Max_CPU_Sec,
qs.min_elapsed_time / 1000000.0 AS Min_Elapsed_Sec,
qs.max_elapsed_time / 1000000.0 AS Max_Elapsed_Sec,
qs.min_granted_query_memory * 8 / 1024 AS Min_Grant_MB,
qs.max_granted_query_memory * 8 / 1024 AS Max_Grant_MB,
qs.min_rows,
qs.max_rows,
qs.total_logical_reads / NULLIF(qs.execution_count, 0) AS Avg_Logical_Reads,
qs.total_physical_reads AS Total_Physical_Reads,
qs.total_logical_writes AS Total_Logical_Writes,
qs.plan_handle
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) sql_txt
CROSS APPLY sys.dm_exec_query_plan(qs.plan_handle) qt
WHERE sql_txt.dbid > 4 -- 排除系统库
AND sql_txt.text NOT LIKE '%sys.%'
AND sql_txt.text NOT LIKE '%dm_exec%'
ORDER BY Avg_CPU_Sec DESC;
七、扩展事件(Extended Events)高性能采集
7.1 为什么用扩展事件而不是 Profiler?
SQL Server Profiler 已被标记为弃用,扩展事件是 15.0.2000.5 的性能跟踪首选。开销小(1-3% vs Profiler 的 10-30%),可以长期运行采集生产环境性能数据。
7.2 创建性能监控会话
sql
-- =============================================
-- 创建长期运行的性能监控扩展事件会话
-- 监控慢查询(> 1 秒)、死锁、阻塞、编译重编译
-- =============================================
-- 1. 创建会话
IF EXISTS (SELECT * FROM sys.server_event_sessions WHERE name = 'PerfMonitor')
DROP EVENT SESSION PerfMonitor ON SERVER;
GO
CREATE EVENT SESSION PerfMonitor ON SERVER
-- 追踪慢查询
ADD EVENT sqlserver.rpc_completed(
ACTION(sqlserver.client_app, sqlserver.client_hostname, sqlserver.database_name,
sqlserver.nt_username, sqlserver.username, sqlserver.session_id, sqlserver.sql_text)
WHERE duration > 1000000 -- > 1 秒
AND database_name <> 'master' -- 排除 master
AND database_name <> 'tempdb'
AND sqlserver.username <> 'sa' -- 排除 sa
),
ADD EVENT sqlserver.sql_batch_completed(
ACTION(sqlserver.client_app, sqlserver.client_hostname, sqlserver.database_name,
sqlserver.nt_username, sqlserver.username, sqlserver.session_id, sqlserver.sql_text)
WHERE duration > 1000000 -- > 1 秒
AND database_name <> 'master'
AND database_name <> 'tempdb'
),
-- 追踪死锁
ADD EVENT sqlserver.xml_deadlock_report(
ACTION(sqlserver.client_app, sqlserver.client_hostname, sqlserver.database_name)
),
-- 追踪阻塞
ADD EVENT sqlos.wait_info(
ACTION(sqlserver.database_name, sqlserver.session_id, sqlserver.sql_text)
WHERE wait_type IN (
N'LCK_M_*', N'OLEDB', N'WSOCK', N'SOS Scheduler Yield',
N'CXPACKET', N'CXCONSUMER', N'CXFETCH BY ROWS'
)
AND duration > 5000 -- > 5 秒
),
-- 追踪资源告警
ADD EVENT sqlserver.error_reported(
ACTION(sqlserver.client_app, sqlserver.client_hostname, sqlserver.database_name, sqlserver.sql_text)
WHERE severity >= 17 -- 严重性 >= 17(潜在硬件问题)
),
-- 追踪编译和重编译
ADD EVENT sqlserver.sp_statement_recompile(
ACTION(sqlserver.database_name, sqlserver.sql_text)
),
ADD EVENT sqlserver.sql_statement_recompile(
ACTION(sqlserver.database_name, sqlserver.sql_text)
),
ADD EVENT sqlserver.hash_warning(
ACTION(sqlserver.database_name, sqlserver.sql_text)
),
ADD EVENT sqlserver.sort_warnings(
ACTION(sqlserver.database_name, sqlserver.sql_text)
),
ADD EVENT sqlserver.auto_stats(
ACTION(sqlserver.database_name, sqlserver.sql_text, sqlserver.username)
),
-- SPID 事件(用于关联查询和执行)
ADD EVENT sqlserver.module_end(
ACTION(sqlserver.database_name, sqlserver.sql_text, sqlserver.username,
sqlserver.session_nt_username, sqlserver.client_app, sqlserver.client_hostname,
sqlserver.plan_handle, sqlserver.tsql_stack)
WHERE duration > 100000 -- > 100ms
),
ADD EVENT sqlserver.rpc_completed(
ACTION(sqlserver.database_name, sqlserver.sql_text, sqlserver.plan_handle)
WHERE duration > 100000
),
ADD EVENT sqlserver.sql_batch_completed(
ACTION(sqlserver.database_name, sqlserver.sql_text, sqlserver.plan_handle)
WHERE duration > 100000
);
GO
-- 2. 启动会话
ALTER EVENT SESSION PerfMonitor ON SERVER STATE = START;
GO
7.3 从扩展事件读取数据
sql
-- =============================================
-- 查询扩展事件数据
-- =============================================
-- 读取慢查询事件
SELECT
xe.event_data.value('(event/@timestamp)[1]', 'DATETIME2') AS EventTime,
xe.event_data.value('(event/action[@name="database_name"]/value)[1]', 'NVARCHAR(128)') AS DatabaseName,
xe.event_data.value('(event/action[@name="client_app"]/value)[1]', 'NVARCHAR(128)') AS ClientApp,
xe.event_data.value('(event/action[@name="client_hostname"]/value)[1]', 'NVARCHAR(128)') AS ClientHost,
xe.event_data.value('(event/action[@name="nt_username"]/value)[1]', 'NVARCHAR(128)') AS Username,
xe.event_data.value('(event/data[@name="duration"]/value)[1]', 'BIGINT') / 1000000.0 AS Duration_Seconds,
xe.event_data.value('(event/data[@name="cpu_time"]/value)[1]', 'BIGINT') / 1000000.0 AS CPU_Seconds,
xe.event_data.value('(event/data[@name="logical_reads"]/value)[1]', 'BIGINT') AS LogicalReads,
xe.event_data.value('(event/data[@name="row_count"]/value)[1]', 'BIGINT') AS RowsReturned,
xe.event_data.value('(event/action[@name="sql_text"]/value)[1]', 'NVARCHAR(MAX)') AS SqlText,
xe.event_data.value('(event/data[@name="object_name"]/value)[1]', 'NVARCHAR(256)') AS ObjectName,
xe.event_data.value('(event/action[@name="plan_handle"]/value)[1]', 'VARBINARY(64)') AS PlanHandle
FROM sys.fn_xe_file_target_read_file(N'PerfMonitor*.xel', NULL, NULL, NULL) xe
CROSS APPLY (SELECT CAST(event_data AS XML) AS event_data) casted
ORDER BY EventTime DESC;
八、数据库引擎核心指标基线
8.1 采集核心运行时指标
sql
-- =============================================
-- 数据库引擎核心指标基线(一次性快照)
-- 建议在业务高峰期采集(如上午 10:00-11:00)
-- =============================================
-- 1. 缓冲区命中率
SELECT
cntr_value AS BufferCacheHitRatio_Percent
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Buffer cache hit ratio'
AND instance_name = '_Total';
-- 2. 页面寿命期望
SELECT
cntr_value AS PageLifeExpectancy_Seconds
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Page life expectancy'
AND instance_name = '_Total';
-- 3. 可用目标内存
SELECT
ROUND(available_target_memory_kb / 1024.0 / 1024.0, 2) AS AvailableTargetMemory_GB
FROM sys.dm_os_sys_memory;
-- 4. 已提交目标内存百分比
SELECT
ROUND(CONVERT(FLOAT, committed_target_kb) / NULLIF(committed_target_key, 0) * 100, 2) AS CommittedTargetPercent
FROM sys.dm_os_sys_memory;
-- 5. 内存授予
SELECT
granted_memory_kb / 1024 AS Granted_MB,
waiting_for_grant_count AS WaitingForGrantCount,
MAX(grantee_count) AS GranteeCount
FROM sys.dm_exec_query_resource_semaphores;
-- 6. 临时数据库使用情况
SELECT
SUM(user_objects_alloc_page_count) * 8 / 1024 AS UserObjects_MB,
SUM(internal_objects_alloc_page_count) * 8 / 1024 AS InternalObjects_MB
FROM tempdb.sys.dm_db_session_space_usage;
8.2 IO 子系统基线
sql
-- =============================================
-- IO 子系统基线采集
-- =============================================
SELECT
DB_NAMEvfs.database_id AS DatabaseName,
mf.name AS LogicalName,
mf.physical_name,
vfs.num_of_reads,
vfs.num_of_writes,
vfs.io_stall_read_ms / NULLIF(vfs.num_of_reads, 0) AS AvgReadLatency_ms,
vfs.io_stall_write_ms / NULLIF(vfs.num_of_writes, 0) AS AvgWriteLatency_ms,
(vfs.io_stall_read_ms + vfs.io_stall_write_ms) / NULLIF((vfs.num_of_reads + vfs.num_of_writes), 0) AS AvgIO_Latency_ms,
vfs.num_of_bytes_read / 1024 / 1024 AS ReadMB,
vfs.num_of_bytes_written / 1024 / 1024 AS WriteMB,
vf.type_desc AS FileType
FROM sys.dm_io_virtual_file_stats(NULL, NULL) vfs
JOIN sys.master_files mf ON vfs.database_id = mf.database_id AND vfs.file_id = mf.file_id
LEFT JOIN sys.master_files vf ON vf.database_id = vfs.database_id AND vf.file_id = vfs.file_id
ORDER BY (vfs.io_stall_read_ms + vfs.io_stall_write_ms) DESC;
-- IO 延迟阈值(SQL Server 2019 推荐值)
-- Data files: < 20ms
-- Log files: < 15ms
-- TempDB: < 10ms
8.3 统计信息健康度基线
sql
-- =============================================
-- 统计信息陈旧度基线
-- =============================================
SELECT
OBJECT_NAME(sp.object_id) AS TableName,
sp.name AS StatsName,
sp.has_filter,
sp.filter_definition,
STATS_DATE(sp.object_id, sp.stats_id) AS LastUpdated,
DATEDIFF(HOUR, STATS_DATE(sp.object_id, sp.stats_id), SYSUTDGETIME()) AS Age_Hours,
sp.user_updates, -- 统计信息被更新的次数
sp.user_seeks, -- 被seek的次数
sp.user_scans, -- 被扫描的次数
sp.user_lookups, -- 被查找的次数
s.row_count,
s.used_page_count * 8 / 1024 AS UsedSpace_MB
FROM sys.stats sp
CROSS APPLY sys.dm_db_stats_properties(sp.object_id, sp.stats_id) sp_prop
CROSS APPLY sys.dm_db_partition_stats(s.object_id, s.partition_number, INDEX_ID_OR_STAT_ID) s
WHERE OBJECTPROPERTY(sp.object_id, 'IsUserTable') = 1
AND sp.auto_created = 0 -- 只看手动创建的统计信息(自动创建的通常没问题)
AND sp.is_improved = 0 -- 不看系统统计信息
ORDER BY DATEDIFF(HOUR, STATS_DATE(sp.object_id, sp.stats_id), SYSUTDGETIME()) DESC;
九、自动化基线采集框架
9.1 创建基线管理表结构
sql
USE DBA_Monitor;
GO
-- 核心指标汇总表
CREATE TABLE PerfBaseline_Summary (
SnapshotTime DATETIME2(0) PRIMARY KEY DEFAULT SYSUTCDATEIME(),
InstanceName SYSNAME DEFAULT @@SERVERNAME,
-- CPU 指标
CpuUsagePercent DECIMAL(5,2),
BatchRequestsPerSec BIGINT,
SQLCompilationsPerSec BIGINT,
SQLReCompilationsPerSec BIGINT,
-- 内存指标
BufferCacheHitRatio DECIMAL(5,2),
PageLifeExpectancySec BIGINT,
TargetServerMemKB BIGINT,
TotalServerMemKB BIGINT,
-- IO 指标
TotalReadsPerSec BIGINT,
TotalWritesPerSec BIGINT,
CheckPointsPerSec BIGINT,
LazyWritesPerSec BIGINT,
-- 等待统计 Top 3
TopWaitType_1 NVARCHAR(60),
TopWaitType_2 NVARCHAR(60),
TopWaitType_3 NVARCHAR(60),
-- 会话统计
ActiveSessions INT,
RunningSessions INT,
BlockedSessions INT
);
GO
-- 查询性能汇总表
CREATE TABLE PerfBaseline_QuerySummary (
SnapshotTime DATETIME2(0),
DatabaseName NVARCHAR(128),
SQLHandle VARBINARY(64),
PlanHandle VARBINARY(64),
SQLText NVARCHAR(MAX),
ExecuteCount BIGINT,
AvgDurationMs DECIMAL(12,2),
AvgCPUMs DECIMAL(12,2),
AvgLogicalReads DECIMAL(12,2),
MaxDurationMs DECIMAL(12,2),
FirstExecution DATETIME2(0),
LastExecution DATETIME2(0),
PRIMARY KEY CLUSTERED (SnapshotTime, SQLHandle)
);
GO
9.2 基线采集存储过程
sql
CREATE OR ALTER PROCEDURE CollectPerfBaselineSnapshot
AS
BEGIN
SET NOCOUNT ON;
DECLARE @SnapshotTime DATETIME2(0) = SYSUTCDATETIME();
-- ==========================================
-- 1. 收集核心指标
-- ==========================================
DECLARE @CpuUsagePercent DECIMAL(5,2);
DECLARE @BufferCacheHitRatio DECIMAL(5,2);
DECLARE @PageLifeExpectancySec BIGINT;
DECLARE @BatchRequestsPerSec BIGINT;
DECLARE @TopWaitType_1 NVARCHAR(60);
DECLARE @TopWaitType_2 NVARCHAR(60);
DECLARE @TopWaitType_3 NVARCHAR(60);
-- CPU
SELECT TOP 1 @CpuUsagePercent = CAST(counter_value AS DECIMAL(5,2))
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Processor Information:% Processor Time'
AND instance_name = '_Total'
AND object_name = 'System';
-- 缓冲命中率
SELECT TOP 1 @BufferCacheHitRatio = CAST(cntr_value AS DECIMAL(5,2))
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Buffer cache hit ratio';
-- PLE
SELECT TOP 1 @PageLifeExpectancySec = cntr_value
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Page life expectancy';
-- 获取 Top 等待类型
WITH TopWaits AS (
SELECT ROW_NUMBER() OVER (ORDER BY wait_time_ms DESC) AS rn, wait_type
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (/* 忽略列表 */)
)
SELECT @TopWaitType_1 = wait_type FROM TopWaits WHERE rn = 1;
SELECT @TopWaitType_2 = wait_type FROM TopWaits WHERE rn = 2;
SELECT @TopWaitType_3 = wait_type FROM TopWaits WHERE rn = 3;
-- 插入汇总
INSERT INTO DBA_Monitor.dbo.PerfBaseline_Summary (
SnapshotTime, InstanceName, CpuUsagePercent,
BatchRequestsPerSec, SQLCompilationsPerSec, SQLReCompilationsPerSec,
BufferCacheHitRatio, PageLifeExpectancySec,
TopWaitType_1, TopWaitType_2, TopWaitType_3,
ActiveSessions, RunningSessions, BlockedSessions
)
SELECT
@SnapshotTime,
@@SERVERNAME,
ISNULL(@CpuUsagePercent, 0),
BR.cntr_value, SC.cntr_value, SRCN.cntr_value,
@BufferCacheHitRatio, @PageLifeExpectancySec,
@TopWaitType_1, @TopWaitType_2, @TopWaitType_3,
SUM(CASE WHEN ses.status != 'sleeping' THEN 1 ELSE 0 END),
SUM(CASE WHEN r.command IS NOT NULL THEN 1 ELSE 0 END),
SUM(CASE WHEN r.blocking_session_id > 0 THEN 1 ELSE 0 END)
FROM sys.dm_exec_sessions ses
LEFT JOIN sys.dm_exec_requests r ON ses.session_id = r.session_id
CROSS JOIN sys.dm_os_performance_counters BR
LEFT JOIN sys.dm_os_performance_counters SC ON SC.counter_name = 'SQL Compilations/sec'
LEFT JOIN sys.dm_os_performance_counters SRCN ON SRCN.counter_name = 'SQL Re-Compilations/sec'
WHERE BR.counter_name = 'Batch Requests/sec';
-- ==========================================
-- 2. 收集查询性能快照
-- ==========================================
INSERT INTO DBA_Monitor.dbo.PerfBaseline_QuerySummary
SELECT
@SnapshotTime,
DB_NAME(qt.dbid),
qs.sql_handle,
qs.plan_handle,
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),
qs.execution_count,
CAST((qs.total_elapsed_time / 1000000.0) / NULLIF(qs.execution_count, 0) AS DECIMAL(12,2)),
CAST((qs.total_worker_time / 1000000.0) / NULLIF(qs.execution_count, 0) AS DECIMAL(12,2)),
CAST((qs.total_logical_reads / 1.0) / NULLIF(qs.execution_count, 0) AS DECIMAL(12,2)),
qs.max_elapsed_time / 1000000.0,
qs.first_execution_time,
qs.last_execution_time
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qt.dbid > 4
AND qt.text NOT LIKE '%sys.%'
AND qt.text NOT LIKE '%dm_exec%'
AND qs.execution_count > 0;
END;
GO
9.3 基线报告查询
sql
-- =============================================
-- 基线分析报告:检测性能异常
-- =============================================
-- 1. 过去 30 天的指标趋势
SELECT
SnapshotTime,
CpuUsagePercent,
BufferCacheHitRatio,
PageLifeExpectancySec,
ActiveSessions,
RunningSessions,
BlockedSessions,
TopWaitType_1,
TopWaitType_2,
TopWaitType_3
FROM DBA_Monitor.dbo.PerfBaseline_Summary
WHERE SnapshotTime >= DATEADD(DAY, -30, SYSUTCDAETIMEL())
ORDER BY SnapshotTime DESC;
-- 2. 查询性能回归检测
-- 对比最近 7 天 vs 之前 7 天的平均运行时间
DECLARE @Now DATETIME2 = SYSUTCADATEIME();
WITH BaselineData AS (
SELECT DatabaseName, SQLText,
AVG(AvgDurationMs) AS AvgDuration_Baseline
FROM DBA_Monitor.dbo.PerfBaseline_QuerySummary
WHERE SnapshotTime BETWEEN DATEADD(DAY, -14, @Now) AND DATEADD(DAY, -7, @Now)
GROUP BY DatabaseName, SQLText
),
CurrentData AS (
SELECT DatabaseName, SQLText,
AVG(AvgDurationMs) AS AvgDuration_Current
FROM DBA_Monitor.dbo.PerfBaseline_QuerySummary
WHERE SnapshotTime >= DATEADD(DAY, -7, @Now)
GROUP BY DatabaseName, SQLText
)
SELECT
cd.DatabaseName,
cd.SQLText,
bd.AvgDuration_Baseline,
cd.AvgDuration_Current,
CASE WHEN bd.AvgDuration_Baseline > 0
THEN CAST((cd.AvgDuration_Current - bd.AvgDuration_Baseline) / bd.AvgDuration_Baseline * 100 AS DECIMAL(5,1))
ELSE 0 END AS Change_Pct
FROM CurrentData cd
JOIN BaselineData bd ON cd.DatabaseName = bd.DatabaseName AND cd.SQLText = bd.SQLText
WHERE bd.AvgDuration_Baseline > 0
AND (cd.AvgDuration_Current - bd.AvgDuration_Baseline) > 100 -- 至少增加 100ms
AND (cd.AvgDuration_Current - bd.AvgDuration_Baseline) / bd.AvgDuration_Baseline > 0.5 -- 增长超过 50%
ORDER BY Change_Pct DESC;
-- 3. 等待事件趋势
SELECT
SnapshotTime,
TopWaitType_1, TopWaitType_2, TopWaitType_3,
ActiveSessions,
CpuUsagePercent
FROM DBA_Monitor.dbo.PerfBaseline_Summary
WHERE SnapshotTime >= DATEADD(DAY, -7, SYSUTCDATETIME())
AND TopWaitType_1 IN (
N'PAGEIOLATCH_EX', N'WRITELOG', N'LCK_M_X',
N'CXPACKET', N'RESOURCE_SEMAPHORE', N'SOS_SCHEDULER_YIELD'
)
ORDER BY SnapshotTime DESC;
十、生产环境部署方案
10.1 SQL Agent 作业
sql
-- 创建性能基线采集作业
USE msdb;
GO
EXEC sp_add_job
@job_name = N'[DBA] Performance Baseline Collection',
@enabled = 1,
@description = N'每小时采集一次性能基线数据';
GO
EXEC sp_add_jobstep
@job_name = N'[DBA] Performance Baseline Collection',
@step_name = N'Collect Perf Snapshot',
@subsystem = N'TSQL',
@command = N'EXEC DBA_Monitor.dbo.CollectPerfBaselineSnapshot;',
@database_name = N'master';
GO
EXEC sp_add_jobschedule
@job_name = N'[DBA] Performance Baseline Collection',
@name = N'Hourly',
@freq_type = 4, -- 每天
@freq_interval = 1,
@freq_subday_type = 8, -- 按小时
@freq_subday_interval = 1;
GO
EXEC sp_add_jobserver
@job_name = N'[DBA] Performance Baseline Collection',
@server_name = N'(local)'
GO
10.2 监控告警规则
| 指标 | 告警阈值 | 级别 | 动作 |
|---|---|---|---|
| CPU Usage > 80% 持续 10min | WARNING | 查 Top CPU 查询 | |
| CPU Usage > 95% 持续 5min | CRITICAL | 立即通知 DBA | |
| PAGE_LATCH 等待占比 > 30% | WARNING | 检查 IO | |
| WRITELOG 等待占比 > 20% | WARNING | 检查 Redo | |
| CXPACKET > 40% | INFO | 检查并行查询 | |
| PLE < 300 sec | WARNING | 检查内存 | |
| PLE < 100 sec | CRITICAL | 可能内存不足 | |
| Blocking 链 > 5 节点 | WARNING | 检查事务 | |
| 查询平均响应时间超基线 200% | WARNING | 查执行计划 |
十一、SQL Server 2019 CU5 特有优化特性
11.1 Intelligent Query Improvement(智能查询改进)
sql
-- 15.0.2000.5 中的新特性
-- 在兼容性级别 150 下自动生效
-- 1. 自适应连接(Adaptive Join)
-- 根据实际数据量选择嵌套循环或哈希连接
-- 检查是否启用
SELECT name, value_in_use
FROM sys.configurations
WHERE name = 'optimize for ad hoc workloads';
-- 2. 批处理模式行存储(Batch Mode on Rowstore)
-- 允许列存储索引在行存储上也能使用批处理模式
-- 无需列存储索引即可受益
-- 3. T-SQL 增量编译(Incremental Compilation)
-- 仅重新编译受统计信息变更影响的语句,而非整个批次
-- 减少重编译开销
-- 4. 标量 UDF 内联(Scalar UDF Inlining)
-- 将标量 UDF 内联到查询中,消除额外调用开销
-- 需要开启 COMPATIBILITY_LEVEL = 150
11.2 大内存页面(Large Pages)
sql
-- SQL Server 2019 推荐在 Linux 上也使用 Large Pages
-- Windows 下确保已启用
-- 配置 SQL Server 使用大页面
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'large pages', 1;
RECONFIGURE;
-- 重启 SQL Server 服务生效
11.3 内存优化表(In-Memory OLTP)
sql
-- 如果需要极致性能,可以考虑 In-Memory OLTP
-- 适用于高并发、低延迟场景
-- 创建内存优化数据库
CREATE DATABASE OLTP_IM
CONTAINS MEMORY_OPTIMIZED_DATA;
GO
-- 创建内存优化表
CREATE TABLE dbo.Accounts (
AccountID INT NOT NULL PRIMARY KEY NONCLUSTERED HASH
WITH (BUCKET_COUNT = 1000000),
Balance DECIMAL(18,4),
ModifiedDate DATETIME2
) WITH (MEMORY_OPTIMIZED = ON, DURABILITY = SCHEMA_AND_DATA);
十二、总结
12.1 基线采集频率建议
| 数据类型 | 采集频率 | 保留时间 | 用途 |
|---|---|---|---|
| 核心计数器 | 15 分钟 | 90 天 | 短期趋势分析 |
| 等待统计 | 15 分钟 | 180 天 | 瓶颈定位 |
| 查询性能(Query Store) | 每小时聚合 | 180 天 | 查询回归检测 |
| 扩展事件(慢查询) | 持续采集 | 30 天(轮转) | 故障排查 |
| 执行计划统计 | 每小时快照 | 365 天 | 长期趋势 |
| 告警触发 | 实时 | 永久归档 | 快速响应 |
12.2 基线建设路线图
阶段 1(第 1 周):基础数据采集
├── 启用核心性能计数器采集
├── 建立等待统计快照
└── 收集初始性能快照
阶段 2(第 2-4 周):完善指标
├── 启用 Query Store
├── 配置扩展事件会话
├── 采集 IO 和内存基线
└── 识别 Top 查询模式
阶段 3(第 5-8 周):自动化与告警
├── 建立自动化采集框架
├── 设置告警阈值
├── 生成基线报告
└── 建立变更影响评估流程
阶段 4(持续运营):持续优化
├── 定期回顾基线报告
├── 对比优化前后效果
├── 持续调整告警阈值
└── 建立容量规划模型
系列文章
总结 :性能基线不是建完就完了的事,它是要养出来的 。第一周的数据不准没关系,第二周会更准,三个月后就是一面镜子------能照出任何性能偏差。记住:没有基线就没有调优的方向,就像没有地图就没有航行的坐标!