SQL Server 数据库巡检报告脚本
SQL Server 数据库巡检报告脚本
脚本结构
sqlserver_health_check.sql
├── 1. 实例基础信息
├── 2. 服务器资源状态
├── 3. 数据库列表与状态
├── 4. 数据文件与日志空间
├── 5. 备份状态检查
├── 6. 作业(Job)执行状况
├── 7. 索引健康度(碎片/缺失)
├── 8. 统计信息过期检查
├── 9. TOP 高消耗 SQL
├── 10. 等待统计与阻塞
├── 11. 错误日志扫描
├── 12. AlwaysOn/镜像/日志传送状态
└── 13. 巡检告警汇总
完整脚本
sql
/*==========================================================
SQL Server 日常巡检报告
适用版本: SQL Server 2014 / 2016 / 2017 / 2019 / 2022
执行账号: sysadmin 或 VIEW SERVER STATE + VIEW ANY DATABASE
==========================================================*/
SET NOCOUNT ON;
SET ANSI_WARNINGS OFF;
PRINT '╔═══════════════════════════════════════════════════╗';
PRINT '║ SQL Server 数据库巡检报告 ║';
PRINT '╚═══════════════════════════════════════════════════╝';
PRINT '巡检时间: ' + CONVERT(VARCHAR(20), GETDATE(), 120);
PRINT '服务器名: ' + @@SERVERNAME;
PRINT '';
/*==========================================================
【1】实例基础信息
==========================================================*/
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 1. 实例基础信息 │';
PRINT '└─────────────────────────────────────┘';
SELECT
SERVERPROPERTY('MachineName') AS [主机名],
SERVERPROPERTY('ServerName') AS [实例名],
SERVERPROPERTY('Edition') AS [版本],
SERVERPROPERTY('ProductVersion') AS [产品版本],
SERVERPROPERTY('ProductLevel') AS [补丁级别],
SERVERPROPERTY('Collation') AS [排序规则],
SERVERPROPERTY('IsClustered') AS [是否群集],
SERVERPROPERTY('IsHadrEnabled') AS [是否启用AlwaysOn],
SERVERPROPERTY('IsFullTextInstalled') AS [全文索引],
CAST(si.cpu_count AS VARCHAR) AS [CPU核数],
CAST(si.physical_memory_kb/1024 AS VARCHAR) + ' MB' AS [物理内存],
si.sqlserver_start_time AS [启动时间],
DATEDIFF(HOUR, si.sqlserver_start_time, GETDATE()) AS [运行小时数]
FROM sys.dm_os_sys_info si;
/*==========================================================
【2】服务器资源状态
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 2. 服务器资源状态 │';
PRINT '└─────────────────────────────────────┘';
-- 2.1 内存使用
PRINT '--- 2.1 内存使用 ---';
SELECT
(physical_memory_in_use_kb / 1024) AS [SQL进程使用MB],
(locked_page_allocations_kb / 1024) AS [锁页内存MB],
(total_virtual_address_space_kb / 1024) AS [虚拟地址总MB],
(virtual_address_space_committed_kb / 1024) AS [已提交虚拟内存MB],
(memory_utilization_percentage) AS [使用率%],
process_physical_memory_low AS [物理内存不足],
process_virtual_memory_low AS [虚拟内存不足]
FROM sys.dm_os_process_memory;
-- 2.2 Buffer Pool 命中率(关键指标,应>95%)
PRINT '--- 2.2 Buffer Pool 命中率 ---';
SELECT
(CAST(a.cntr_value AS FLOAT) / b.cntr_value) * 100 AS [缓冲命中率%]
FROM sys.dm_os_performance_counters a
JOIN sys.dm_os_performance_counters b
ON a.object_name = b.object_name
WHERE a.counter_name = 'Buffer cache hit ratio'
AND b.counter_name = 'Buffer cache hit ratio base';
-- 2.3 Page Life Expectancy (PLE, 应>300秒)
PRINT '--- 2.3 页生命周期(PLE) ---';
SELECT
object_name AS [对象],
cntr_value AS [PLE秒],
CASE WHEN cntr_value < 300 THEN '🔴 偏低'
WHEN cntr_value < 1000 THEN '🟡 警告'
ELSE '🟢 正常' END AS [状态]
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Page life expectancy'
AND object_name LIKE '%Buffer Manager%';
/*==========================================================
【3】数据库列表与状态
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 3. 数据库列表与状态 │';
PRINT '└─────────────────────────────────────┘';
SELECT
database_id AS [DBID],
name AS [数据库名],
state_desc AS [状态],
recovery_model_desc AS [恢复模式],
compatibility_level AS [兼容级别],
collation_name AS [排序规则],
is_read_only AS [只读],
is_auto_close_on AS [自动关闭],
is_auto_shrink_on AS [自动收缩],
page_verify_option_desc AS [页校验],
create_date AS [创建时间]
FROM sys.databases
ORDER BY database_id;
/*==========================================================
【4】数据文件与日志空间
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 4. 数据文件与日志空间 │';
PRINT '└─────────────────────────────────────┘';
-- 4.1 每个数据库的文件大小和可用空间
IF OBJECT_ID('tempdb..#FileSpace') IS NOT NULL DROP TABLE #FileSpace;
CREATE TABLE #FileSpace (
DBName SYSNAME,
FileType VARCHAR(10),
LogicalName SYSNAME,
PhysicalName NVARCHAR(500),
SizeMB DECIMAL(18,2),
UsedMB DECIMAL(18,2),
FreeMB DECIMAL(18,2),
UsedPct DECIMAL(5,2),
GrowthSetting VARCHAR(50),
MaxSizeMB VARCHAR(20)
);
DECLARE @sql NVARCHAR(MAX) = N'
USE [?];
IF DB_ID(''?'') > 4 OR DB_ID(''?'') IN (1,2,4) -- 含系统库
INSERT INTO #FileSpace
SELECT
DB_NAME() AS DBName,
CASE f.type WHEN 0 THEN ''DATA'' WHEN 1 THEN ''LOG''
WHEN 2 THEN ''FILESTREAM'' ELSE ''OTHER'' END AS FileType,
f.name AS LogicalName,
f.physical_name AS PhysicalName,
CAST(f.size * 8.0 / 1024 AS DECIMAL(18,2)) AS SizeMB,
CAST(FILEPROPERTY(f.name, ''SpaceUsed'') * 8.0 / 1024 AS DECIMAL(18,2)) AS UsedMB,
CAST((f.size - FILEPROPERTY(f.name, ''SpaceUsed'')) * 8.0 / 1024 AS DECIMAL(18,2)) AS FreeMB,
CAST(FILEPROPERTY(f.name, ''SpaceUsed'') * 100.0 / NULLIF(f.size,0) AS DECIMAL(5,2)) AS UsedPct,
CASE WHEN f.is_percent_growth = 1 THEN CAST(f.growth AS VARCHAR) + ''%''
ELSE CAST(f.growth * 8 / 1024 AS VARCHAR) + '' MB'' END AS GrowthSetting,
CASE WHEN f.max_size = -1 THEN ''UNLIMITED''
WHEN f.max_size = 268435456 THEN ''2TB''
ELSE CAST(f.max_size * 8 / 1024 AS VARCHAR) + '' MB'' END AS MaxSizeMB
FROM sys.database_files f;
';
EXEC sp_MSforeachdb @sql;
SELECT
DBName AS [数据库],
FileType AS [文件类型],
LogicalName AS [逻辑名],
SizeMB AS [总大小MB],
UsedMB AS [已用MB],
FreeMB AS [剩余MB],
UsedPct AS [使用率%],
CASE WHEN UsedPct >= 90 THEN '🔴 严重'
WHEN UsedPct >= 80 THEN '🟡 警告'
ELSE '🟢 正常' END AS [状态],
GrowthSetting AS [增长设置],
MaxSizeMB AS [最大大小],
PhysicalName AS [物理路径]
FROM #FileSpace
ORDER BY UsedPct DESC, DBName, FileType;
-- 4.2 磁盘可用空间
PRINT '--- 4.2 磁盘可用空间 ---';
SELECT DISTINCT
vs.volume_mount_point AS [磁盘],
vs.logical_volume_name AS [卷标],
vs.file_system_type AS [文件系统],
CAST(vs.total_bytes/1024.0/1024/1024 AS DECIMAL(10,2)) AS [总GB],
CAST(vs.available_bytes/1024.0/1024/1024 AS DECIMAL(10,2)) AS [可用GB],
CAST((1 - vs.available_bytes*1.0/vs.total_bytes)*100 AS DECIMAL(5,2)) AS [使用率%],
CASE WHEN (1 - vs.available_bytes*1.0/vs.total_bytes) >= 0.9 THEN '🔴 严重'
WHEN (1 - vs.available_bytes*1.0/vs.total_bytes) >= 0.8 THEN '🟡 警告'
ELSE '🟢 正常' END AS [状态]
FROM sys.master_files mf
CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id) vs;
/*==========================================================
【5】备份状态检查
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 5. 备份状态检查 │';
PRINT '└─────────────────────────────────────┘';
SELECT
d.name AS [数据库],
d.recovery_model_desc AS [恢复模式],
MAX(CASE WHEN b.type='D' THEN b.backup_finish_date END) AS [最近完整备份],
MAX(CASE WHEN b.type='I' THEN b.backup_finish_date END) AS [最近差异备份],
MAX(CASE WHEN b.type='L' THEN b.backup_finish_date END) AS [最近日志备份],
DATEDIFF(HOUR, MAX(CASE WHEN b.type='D' THEN b.backup_finish_date END), GETDATE()) AS [完整备份距今小时],
DATEDIFF(HOUR, MAX(CASE WHEN b.type='L' THEN b.backup_finish_date END), GETDATE()) AS [日志备份距今小时],
CASE
WHEN MAX(CASE WHEN b.type='D' THEN b.backup_finish_date END) IS NULL THEN '🔴 从未备份'
WHEN DATEDIFF(HOUR, MAX(CASE WHEN b.type='D' THEN b.backup_finish_date END), GETDATE()) > 24*7 THEN '🔴 超过一周'
WHEN DATEDIFF(HOUR, MAX(CASE WHEN b.type='D' THEN b.backup_finish_date END), GETDATE()) > 24 THEN '🟡 超过一天'
ELSE '🟢 正常' END AS [备份状态]
FROM sys.databases d
LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name
WHERE d.database_id > 4 -- 排除系统库(按需注释)
AND d.state_desc = 'ONLINE'
GROUP BY d.name, d.recovery_model_desc
ORDER BY [完整备份距今小时] DESC;
/*==========================================================
【6】作业(Job)执行状况
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 6. SQL Agent 作业执行状况 │';
PRINT '└─────────────────────────────────────┘';
SELECT
j.name AS [作业名],
CASE j.enabled WHEN 1 THEN '启用' ELSE '禁用' END AS [状态],
CASE h.run_status
WHEN 0 THEN '❌ 失败' WHEN 1 THEN '✅ 成功'
WHEN 2 THEN '重试' WHEN 3 THEN '取消'
WHEN 4 THEN '运行中' ELSE '未知' END AS [上次结果],
msdb.dbo.agent_datetime(h.run_date, h.run_time) AS [上次运行时间],
STUFF(STUFF(RIGHT('000000' + CAST(h.run_duration AS VARCHAR), 6), 5, 0, ':'), 3, 0, ':') AS [持续时长],
SUBSTRING(h.message, 1, 100) AS [消息摘要]
FROM msdb.dbo.sysjobs j
OUTER APPLY (
SELECT TOP 1 run_status, run_date, run_time, run_duration, message
FROM msdb.dbo.sysjobhistory
WHERE job_id = j.job_id AND step_id = 0
ORDER BY run_date DESC, run_time DESC
) h
ORDER BY
CASE WHEN h.run_status = 0 THEN 0 ELSE 1 END, -- 失败的排前面
h.run_date DESC;
/*==========================================================
【7】索引健康度(碎片 & 缺失 & 未用)
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 7. 索引健康度检查 │';
PRINT '└─────────────────────────────────────┘';
-- 7.1 碎片率 > 30% 的索引(需重建)
PRINT '--- 7.1 高碎片索引 (>30%) ---';
SELECT TOP 30
DB_NAME(ips.database_id) AS [数据库],
OBJECT_SCHEMA_NAME(ips.object_id, ips.database_id) + '.' +
OBJECT_NAME(ips.object_id, ips.database_id) AS [表],
i.name AS [索引名],
ips.index_type_desc AS [索引类型],
CAST(ips.avg_fragmentation_in_percent AS DECIMAL(5,2)) AS [碎片率%],
ips.page_count AS [页数],
CASE
WHEN ips.avg_fragmentation_in_percent > 30 THEN 'REBUILD'
WHEN ips.avg_fragmentation_in_percent > 10 THEN 'REORGANIZE'
ELSE 'OK' END AS [建议操作]
FROM sys.dm_db_index_physical_stats(NULL, NULL, NULL, NULL, 'LIMITED') ips
JOIN sys.indexes i
ON ips.object_id = i.object_id
AND ips.index_id = i.index_id
WHERE ips.avg_fragmentation_in_percent > 30
AND ips.page_count > 1000 -- 小表忽略
AND ips.database_id > 4
AND i.name IS NOT NULL
ORDER BY ips.avg_fragmentation_in_percent DESC;
-- 7.2 缺失索引建议(按收益排序)
PRINT '--- 7.2 系统推荐的缺失索引 (TOP 10) ---';
SELECT TOP 10
DB_NAME(mid.database_id) AS [数据库],
OBJECT_NAME(mid.object_id, mid.database_id) AS [表名],
CAST(migs.avg_total_user_cost * migs.avg_user_impact *
(migs.user_seeks + migs.user_scans) AS DECIMAL(18,2)) AS [预估收益],
migs.user_seeks AS [Seek次数],
migs.user_scans AS [Scan次数],
migs.last_user_seek AS [最近使用],
mid.equality_columns AS [等值列],
mid.inequality_columns AS [不等值列],
mid.included_columns AS [包含列],
'CREATE INDEX IX_' + OBJECT_NAME(mid.object_id, mid.database_id) +
'_' + REPLACE(REPLACE(REPLACE(ISNULL(mid.equality_columns,''), '[',''), ']',''), ', ','_') +
' ON ' + mid.statement +
' (' + ISNULL(mid.equality_columns,'') +
CASE WHEN mid.inequality_columns IS NOT NULL
THEN ISNULL(',' + mid.inequality_columns, '') ELSE '' END + ')' +
ISNULL(' INCLUDE (' + mid.included_columns + ')', '') AS [建议创建语句]
FROM sys.dm_db_missing_index_details mid
JOIN sys.dm_db_missing_index_groups mig
ON mid.index_handle = mig.index_handle
JOIN sys.dm_db_missing_index_group_stats migs
ON mig.index_group_handle = migs.group_handle
WHERE mid.database_id > 4
ORDER BY [预估收益] DESC;
/*==========================================================
【8】统计信息过期检查
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 8. 统计信息过期检查 │';
PRINT '└─────────────────────────────────────┘';
IF OBJECT_ID('tempdb..#StatsInfo') IS NOT NULL DROP TABLE #StatsInfo;
CREATE TABLE #StatsInfo (
DBName SYSNAME, TableName SYSNAME, StatName SYSNAME,
LastUpdated DATETIME, RowsInTable BIGINT, RowsSampled BIGINT,
ModifiedRows BIGINT, ModifiedPct DECIMAL(5,2)
);
EXEC sp_MSforeachdb N'
USE [?];
IF DB_ID() > 4
INSERT INTO #StatsInfo
SELECT
DB_NAME(),
OBJECT_SCHEMA_NAME(s.object_id) + ''.'' + OBJECT_NAME(s.object_id),
s.name,
sp.last_updated,
sp.rows,
sp.rows_sampled,
sp.modification_counter,
CAST(sp.modification_counter * 100.0 / NULLIF(sp.rows,0) AS DECIMAL(5,2))
FROM sys.stats s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) sp
WHERE OBJECTPROPERTY(s.object_id, ''IsUserTable'') = 1
AND sp.rows > 10000
AND (sp.last_updated < DATEADD(DAY, -7, GETDATE())
OR sp.modification_counter > sp.rows * 0.2);
';
SELECT TOP 30
DBName AS [数据库],
TableName AS [表],
StatName AS [统计信息],
LastUpdated AS [最后更新],
DATEDIFF(DAY, LastUpdated, GETDATE()) AS [距今天数],
RowsInTable AS [表行数],
ModifiedRows AS [修改行数],
ModifiedPct AS [修改比例%]
FROM #StatsInfo
ORDER BY ModifiedPct DESC;
/*==========================================================
【9】TOP 高消耗 SQL
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 9. TOP 高消耗 SQL │';
PRINT '└─────────────────────────────────────┘';
-- 9.1 CPU 消耗 TOP 10
PRINT '--- 9.1 CPU 消耗 TOP 10 ---';
SELECT TOP 10
qs.execution_count AS [执行次数],
qs.total_worker_time / 1000 AS [总CPU毫秒],
qs.total_worker_time / qs.execution_count / 1000 AS [平均CPU毫秒],
qs.total_elapsed_time / qs.execution_count / 1000 AS [平均耗时毫秒],
qs.total_logical_reads / qs.execution_count AS [平均逻辑读],
qs.last_execution_time AS [最后执行时间],
DB_NAME(qt.dbid) AS [数据库],
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 [SQL语句]
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qs.execution_count > 0
ORDER BY qs.total_worker_time DESC;
-- 9.2 逻辑读消耗 TOP 10
PRINT '--- 9.2 逻辑读 TOP 10 ---';
SELECT TOP 10
qs.execution_count AS [执行次数],
qs.total_logical_reads AS [总逻辑读],
qs.total_logical_reads / qs.execution_count AS [平均逻辑读],
qs.total_physical_reads / qs.execution_count AS [平均物理读],
qs.last_execution_time AS [最后执行],
DB_NAME(qt.dbid) AS [数据库],
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 [SQL语句]
FROM sys.dm_exec_query_stats qs
CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) qt
WHERE qs.execution_count > 0
ORDER BY qs.total_logical_reads DESC;
/*==========================================================
【10】等待统计 & 当前阻塞
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 10. 等待统计 & 当前阻塞 │';
PRINT '└─────────────────────────────────────┘';
-- 10.1 TOP 等待事件
PRINT '--- 10.1 TOP 15 等待事件 ---';
SELECT TOP 15
wait_type AS [等待类型],
waiting_tasks_count AS [等待次数],
wait_time_ms AS [总等待ms],
wait_time_ms / NULLIF(waiting_tasks_count,0) AS [平均等待ms],
signal_wait_time_ms AS [信号等待ms],
CAST(signal_wait_time_ms * 100.0 / NULLIF(wait_time_ms,0) AS DECIMAL(5,2)) AS [CPU压力%]
FROM sys.dm_os_wait_stats
WHERE wait_type NOT IN (
'SLEEP_TASK','BROKER_TASK_STOP','SQLTRACE_BUFFER_FLUSH',
'CLR_AUTO_EVENT','CLR_MANUAL_EVENT','LAZYWRITER_SLEEP',
'CHECKPOINT_QUEUE','REQUEST_FOR_DEADLOCK_SEARCH',
'XE_TIMER_EVENT','XE_DISPATCHER_WAIT','LOGMGR_QUEUE',
'FT_IFTS_SCHEDULER_IDLE_WAIT','BROKER_TO_FLUSH',
'BROKER_EVENTHANDLER','TRACEWRITE','FT_IFTSHC_MUTEX',
'SQLTRACE_INCREMENTAL_FLUSH_SLEEP','DIRTY_PAGE_POLL',
'SP_SERVER_DIAGNOSTICS_SLEEP','HADR_FILESTREAM_IOMGR_IOCOMPLETION',
'HADR_WORK_QUEUE','QDS_ASYNC_QUEUE','QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP',
'WAITFOR','DISPATCHER_QUEUE_SEMAPHORE','BROKER_RECEIVE_WAITFOR'
)
AND waiting_tasks_count > 0
ORDER BY wait_time_ms DESC;
-- 10.2 当前阻塞会话链
PRINT '--- 10.2 当前阻塞链 ---';
SELECT
s.session_id AS [会话SID],
s.blocking_session_id AS [阻塞者SID],
s.status AS [状态],
s.wait_type AS [等待类型],
s.wait_time AS [等待ms],
s.wait_resource AS [等待资源],
DB_NAME(s.database_id) AS [数据库],
es.login_name AS [登录],
es.host_name AS [主机],
es.program_name AS [应用],
SUBSTRING(qt.text, s.statement_start_offset/2+1,
(CASE s.statement_end_offset WHEN -1 THEN DATALENGTH(qt.text)
ELSE s.statement_end_offset END - s.statement_start_offset)/2 + 1) AS [正在执行的SQL]
FROM sys.dm_exec_requests s
JOIN sys.dm_exec_sessions es ON s.session_id = es.session_id
OUTER APPLY sys.dm_exec_sql_text(s.sql_handle) qt
WHERE s.blocking_session_id <> 0
OR s.session_id IN (SELECT blocking_session_id
FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0);
/*==========================================================
【11】错误日志扫描(最近24小时)
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 11. 错误日志关键字扫描 │';
PRINT '└─────────────────────────────────────┘';
IF OBJECT_ID('tempdb..#ErrLog') IS NOT NULL DROP TABLE #ErrLog;
CREATE TABLE #ErrLog (
LogDate DATETIME,
ProcessInfo VARCHAR(50),
LogText NVARCHAR(MAX)
);
INSERT INTO #ErrLog
EXEC xp_readerrorlog 0, 1, NULL, NULL,
@p5 = NULL, @p6 = NULL, @p7 = N'desc';
SELECT TOP 50
LogDate AS [时间],
ProcessInfo AS [进程],
LEFT(LogText, 200) AS [日志摘要]
FROM #ErrLog
WHERE LogDate >= DATEADD(HOUR, -24, GETDATE())
AND (
LogText LIKE '%error%'
OR LogText LIKE '%fail%'
OR LogText LIKE '%severity: 1[6-9]%'
OR LogText LIKE '%severity: 2%'
OR LogText LIKE '%corrupt%'
OR LogText LIKE '%deadlock%'
OR LogText LIKE '%I/O%error%'
OR LogText LIKE '%timeout%'
)
ORDER BY LogDate DESC;
/*==========================================================
【12】AlwaysOn / 镜像 / 日志传送 状态
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 12. 高可用组件状态 │';
PRINT '└─────────────────────────────────────┘';
-- 12.1 AlwaysOn
IF SERVERPROPERTY('IsHadrEnabled') = 1
BEGIN
PRINT '--- 12.1 AlwaysOn 可用性组 ---';
SELECT
ag.name AS [可用性组],
ar.replica_server_name AS [副本服务器],
ars.role_desc AS [角色],
ars.connected_state_desc AS [连接状态],
ars.synchronization_health_desc AS [同步健康度],
ars.operational_state_desc AS [运行状态],
ar.availability_mode_desc AS [同步模式],
ar.failover_mode_desc AS [故障转移模式]
FROM sys.availability_groups ag
JOIN sys.availability_replicas ar ON ag.group_id = ar.group_id
JOIN sys.dm_hadr_availability_replica_states ars
ON ar.replica_id = ars.replica_id;
PRINT '--- 12.2 AlwaysOn 数据库同步延迟 ---';
SELECT
DB_NAME(drs.database_id) AS [数据库],
ar.replica_server_name AS [副本],
drs.synchronization_state_desc AS [同步状态],
drs.synchronization_health_desc AS [健康度],
drs.log_send_queue_size AS [日志发送队列KB],
drs.redo_queue_size AS [重做队列KB],
drs.redo_rate AS [重做速率KB/s],
drs.last_commit_time AS [最后提交时间]
FROM sys.dm_hadr_database_replica_states drs
JOIN sys.availability_replicas ar ON drs.replica_id = ar.replica_id;
END
ELSE
PRINT ' (本实例未启用 AlwaysOn)';
-- 12.2 数据库镜像
IF EXISTS (SELECT 1 FROM sys.database_mirroring WHERE mirroring_state IS NOT NULL)
BEGIN
PRINT '--- 12.3 数据库镜像 ---';
SELECT
DB_NAME(database_id) AS [数据库],
mirroring_role_desc AS [角色],
mirroring_state_desc AS [状态],
mirroring_safety_level_desc AS [安全级别],
mirroring_partner_name AS [伙伴],
mirroring_witness_name AS [见证]
FROM sys.database_mirroring
WHERE mirroring_state IS NOT NULL;
END
/*==========================================================
【13】巡检告警汇总
==========================================================*/
PRINT '';
PRINT '┌─────────────────────────────────────┐';
PRINT '│ 13. 巡检告警汇总 │';
PRINT '└─────────────────────────────────────┘';
IF OBJECT_ID('tempdb..#Alert') IS NOT NULL DROP TABLE #Alert;
CREATE TABLE #Alert (AlertLevel VARCHAR(10), AlertItem NVARCHAR(500));
-- 数据库非ONLINE状态
INSERT #Alert
SELECT '🔴 严重', '数据库 [' + name + '] 状态异常: ' + state_desc
FROM sys.databases WHERE state_desc <> 'ONLINE';
-- 文件使用率 > 85%
INSERT #Alert
SELECT
CASE WHEN UsedPct >= 95 THEN '🔴 严重' ELSE '🟡 警告' END,
'数据库[' + DBName + ']文件[' + LogicalName + ']使用率: ' +
CAST(UsedPct AS VARCHAR) + '%'
FROM #FileSpace WHERE UsedPct >= 85;
-- 备份告警
INSERT #Alert
SELECT '🔴 严重', '数据库 [' + d.name + '] 从未完整备份'
FROM sys.databases d
LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name AND b.type = 'D'
WHERE d.database_id > 4 AND d.state_desc = 'ONLINE'
GROUP BY d.name
HAVING MAX(b.backup_finish_date) IS NULL;
INSERT #Alert
SELECT '🟡 警告',
'数据库 [' + d.name + '] 完整备份已超过24小时: 最后备份 ' +
CONVERT(VARCHAR(20), MAX(b.backup_finish_date), 120)
FROM sys.databases d
JOIN msdb.dbo.backupset b ON d.name = b.database_name AND b.type = 'D'
WHERE d.database_id > 4 AND d.state_desc = 'ONLINE'
GROUP BY d.name
HAVING DATEDIFF(HOUR, MAX(b.backup_finish_date), GETDATE()) > 24;
-- 失败的Job
INSERT #Alert
SELECT '🔴 严重', '作业 [' + j.name + '] 最近执行失败'
FROM msdb.dbo.sysjobs j
WHERE j.enabled = 1
AND EXISTS (
SELECT 1 FROM msdb.dbo.sysjobhistory h
WHERE h.job_id = j.job_id AND h.step_id = 0
AND h.run_status = 0
AND msdb.dbo.agent_datetime(h.run_date, h.run_time) > DATEADD(DAY, -1, GETDATE())
);
-- PLE 过低
INSERT #Alert
SELECT '🟡 警告', '页生命周期(PLE)过低: ' + CAST(cntr_value AS VARCHAR) + ' 秒'
FROM sys.dm_os_performance_counters
WHERE counter_name = 'Page life expectancy'
AND object_name LIKE '%Buffer Manager%'
AND cntr_value < 300;
-- 存在阻塞
INSERT #Alert
SELECT '🟡 警告',
'当前存在阻塞会话: ' + CAST(COUNT(*) AS VARCHAR) + ' 个会话被阻塞'
FROM sys.dm_exec_requests
WHERE blocking_session_id <> 0
HAVING COUNT(*) > 0;
-- 自动收缩/自动关闭
INSERT #Alert
SELECT '🟡 警告', '数据库 [' + name + '] 启用了不推荐的选项: ' +
CASE WHEN is_auto_shrink_on = 1 THEN 'AUTO_SHRINK ' ELSE '' END +
CASE WHEN is_auto_close_on = 1 THEN 'AUTO_CLOSE' ELSE '' END
FROM sys.databases
WHERE database_id > 4
AND (is_auto_shrink_on = 1 OR is_auto_close_on = 1);
-- 输出告警
IF EXISTS (SELECT 1 FROM #Alert)
SELECT AlertLevel AS [级别], AlertItem AS [告警项]
FROM #Alert
ORDER BY CASE AlertLevel WHEN '🔴 严重' THEN 1 WHEN '🟡 警告' THEN 2 ELSE 3 END;
ELSE
PRINT '✅ 未发现异常项,数据库运行正常';
PRINT '';
PRINT '═══════════ 巡检完成 ═══════════';
-- 清理临时表
DROP TABLE IF EXISTS #FileSpace;
DROP TABLE IF EXISTS #StatsInfo;
DROP TABLE IF EXISTS #ErrLog;
DROP TABLE IF EXISTS #Alert;
使用方式
方式一:SSMS 手动执行
- 打开 SQL Server Management Studio
- 连接实例(用 sysadmin 账号)
- 新建查询,粘贴上述脚本
- 结果输出到**"结果到文本"**模式(Ctrl+T)阅读更清晰
方式二:命令行导出报告
cmd
sqlcmd -S YourServer -E -i health_check.sql -o C:\DBA\Report\Check_20260422.txt -w 500
方式三:定时任务自动化(推荐)
建一个 SQL Agent 作业,每日早上 8 点执行并邮件推送:
sql
-- 在作业步骤中使用 sqlcmd + sp_send_dbmail
DECLARE @ReportFile NVARCHAR(200) =
'C:\DBA\Report\Check_' + FORMAT(GETDATE(),'yyyyMMdd') + '.txt';
EXEC xp_cmdshell 'sqlcmd -S . -E -i C:\DBA\Scripts\health_check.sql -o ... -w 500';
EXEC msdb.dbo.sp_send_dbmail
@profile_name = 'DBA_Mail',
@recipients = 'dba@company.com',
@subject = 'SQL Server 日常巡检报告',
@body = '附件为今日巡检结果,请查收。',
@file_attachments = @ReportFile;
关键指标阈值参考
| 监控项 | 正常 | 警告 | 严重 |
|---|---|---|---|
| Buffer 命中率 | > 99% | 95%~99% | < 95% |
| PLE 页生命周期 | > 1000秒 | 300~1000秒 | < 300秒 |
| 数据文件使用率 | < 80% | 80%~90% | > 90% |
| 磁盘可用空间 | > 20% | 10%~20% | < 10% |
| 索引碎片率 | < 10% | 10%~30% | > 30% |
| 备份间隔 | < 24小时 | 24~72小时 | > 72小时 |
| AlwaysOn 重做队列 | < 1MB | 1MB~100MB | > 100MB |
建议的巡检频率
| 巡检频率 | 检查内容 |
|---|---|
| 每日 | 1/2/3/5/6/10/11/13 节 |
| 每周 | 7/8 节(索引碎片、统计信息) |
| 每月 | 全量完整报告并归档留存 |
脚本包含的 13 项检查可按需裁剪,建议第一次运行先在测试库验证,再部署到生产环境。