
接到一台没人交接的SQL Server服务器除了账号和密码什么资料都没有你会从哪里下手这个问题我遇到过很多次也是后台私信里被问得最频繁的问题之一。所谓“基础查看”并不只是打开SSMS看两眼数据库列表那么简单而是要有顺序、有目的地去探查一个实例的家底跑在什么版本上、资源给了多少、管着哪些库、库里有哪些结构、现在正在发生什么。这套能力在正式环境尤其重要因为你随时可能接手一台陌生机器也随时可能被拉去救火——DB报慢了你要在十分钟内说清楚瓶颈在哪一层。这篇就按我自己的排查习惯把SQL Server基础查看的完整思路和可复用脚本整理出来适合刚转岗的运维同学也适合那些对DMV不熟的开发朋友直接抄作业。1. 先理顺“查看”的目的不是翻数据而是摸家底很多人拿到一个数据库只干一件事SELECT一下业务表看有没有数据。但基础查看的真正目标是把一台实例当做一个黑盒系统去探测搞清楚四个问题这台机器是什么规格、这个实例装了什么版本、里面有哪些库、这些库的资源配置是否健康。只有把这四层摸清楚后面谈性能、谈优化才有立足点。1.1 实例身份四件套版本、补丁、版本类型、可用性第一眼要看的是版本信息。版本决定了你手里的SQL Server支持哪些语法、哪些DMV可用也直接影响你后续排查手段的选择。比如2012和2016的系统动态管理视图就有不少差异2016以后才有的sys.dm_db_file_space_usage在2012上就直接报对象名无效。SELECT SERVERPROPERTY(MachineName) AS ServerName, SERVERPROPERTY(InstanceName) AS InstanceName, SERVERPROPERTY(ProductVersion) AS ProductVersion, SERVERPROPERTY(ProductLevel) AS ProductLevel, SERVERPROPERTY(Edition) AS Edition, SERVERPROPERTY(ProductUpdateLevel) AS BuildUpdateLevel;这组查询的结果我要单独解释一下因为它经常被新手看懵ProductVersion格式一般是“15.0.2000.5”这种15.0对应SQL Server 201916.0对应202213.0是201612.0是201411.0是2012。ProductLevelRTM表示初始发布版SP1、SP2表示补丁包CU表示累积更新。EditionEnterprise还是Standard还是Developer。这决定了可用内存上限、在线索引重建这类特性有没有很多高级功能在Standard上是不生效的。在此基础上我还会顺手查一下VERSION的完整文本它能直接给出操作系统层面的兼容信息配合sys.dm_os_sys_info里的physical_memory_kb可以快速判断这台实例到底能支配多少内存。注意physical_memory_kb反映的是物理内存总量真正给SQL Server缓冲池用的还要看sys.dm_os_memory_clerks这里不展开以后聊内存优化再细说。1.2 最大并发与权限模型基础查看中的隐藏检查项服务器配置信息也是基础查看的一部分。我通常用sys.configurations来检查几个关键选项而不是逐个去翻属性页SELECT name, value_in_use, value, is_dynamic, is_advanced FROM sys.configurations WHERE name IN ( max degree of parallelism, max server memory (MB), min server memory (MB), optimize for ad hoc workloads, backup compression default, cost threshold for parallelism );这六个参数是我接手新实例时必看的。max degree of parallelism如果被设成0且CPU核数很多那就是典型的并行风暴隐患max server memory如果还保持默认值说明这台机器从来没有做过基础的内存规划。有人说“我顺手看一眼配置不就行了”但用查询的方式还有一个好处它可以落脚本、可以留档下次别人问“这台机器改过什么”时你能用快照对比来回答。这是手工点GUI做不到的。另外我还会看sys.server_principals里有哪些sysadmin登录确认是否存在多余的高权限账号。这个属于安全审计范畴但既然在做实例摸底顺手看一下很值得。1.3 库清单与状态别让离线库坑了你到了数据库层面第一件事是用sys.databases拿全量清单并且要仔细看state_desc和recovery_model_descSELECT database_id, name AS DatabaseName, state_desc, recovery_model_desc, compatibility_level, page_verify_option_desc, is_encrypted, create_date, user_access_desc FROM sys.databases ORDER BY database_id;为什么要把user_access_desc也带出来因为我在正式环境见过不止一次有人为了让数据库只给自己访问把SINGLE_USER模式打开了结果业务一起来就报“数据库已处于单用户模式”所有人干瞪眼没人想到去查这个字段。还有is_encrypted有些合规项目要求所有库启用透明加密查看阶段不确认清楚后面做备份恢复会在加密链上栽跟头。state_desc除了ONLINE还可能是OFFLINE、RESTORING、RECOVERING、SUSPECT。看到一个库处在RESTORING并不可怕可能是正在做时间点恢复但如果长时间停在RECOVERING就要注意可能是日志重放卡住。这些状态是“基础查看”里最容易一眼看出来、也最容易忽略的信息。2. 数据库内部的“建筑图纸”文件、空间、表结构了解实例里有几个库之后下一步就要进入单个库内部去查看空间和结构。很多人的空间焦虑都是从这里开始的日志文件疯狂膨胀、数据文件见底、临时库挤爆C盘。与其等报警不如把下面的查询变成例行体检的一部分。2.1 数据文件和日志文件真实占用不要只看“大小”先看数据库文件的基础信息。文件size字段的单位是8KB页换算成MB要乘以8再除以1024这点必须养成习惯否则你会把数据库容量看大8192倍SELECT DB_NAME() AS DatabaseName, file_id, type_desc AS FileType, name AS LogicalFileName, physical_name, size * 8 / 1024 AS SizeMB, CASE WHEN max_size 0 THEN NoGrowth WHEN max_size -1 THEN LimitedByDisk ELSE CAST((max_size * 8 / 1024) AS varchar(20)) MB END AS MaxSize, growth, is_percent_growth FROM sys.database_files;这条查询能看出文件有没有设置自动增长、增长是按兆字节还是按百分比。这里有个非常重要的经验生产库数据文件我一般会建议关闭百分比增长改按固定MB增长比如每次500MB或1GB因为按百分比在小文件阶段显得很“温柔”文件大了以后一次增长几个GB甚至几十GB容易瞬间把磁盘IO打满。这条建议我在好几个生产事故复盘里都提过。查看日志空间占用还有一条专门的路子——DBCC SQLPERF(LOGSPACE)它直接输出每库日志文件的使用百分比。使用率如果长时间在90%以上就要做好日志备份接管的空间回收准备。顺带说一句日志文件已经很大但使用率很低说明里面大量空间是空闲可复用的不要急着收缩收缩日志不但耗时而且会引起严重锁竞争这个坑太经典了。再往下可以看文件内部分配了多少给已提交数据、多少是空闲状态SQL Server 2016以上用sys.dm_db_file_space_usageSELECT DB_NAME(database_id) AS DbName, file_id, database_size_kb / 1024 AS DatabaseSizeMB, unallocated_extent_page_count * 8 / 1024 AS UnallocatedExtentMB, version_store_reserved_page_count * 8 / 1024 AS VersionStoreMB, user_object_reserved_page_count * 8 / 1024 AS UserObjectMB, internal_object_reserved_page_count * 8 / 1024 AS InternalObjectMB, mixed_extent_allocation_page_count * 8 / 1024 AS MixedExtentMB FROM sys.dm_db_file_space_usage;看到version_store_reserved_page_count异常增大时通常是某个长事务持续开着并且伴随大量UPDATE造成的俗称“版本仓库膨胀”。这是很多空间事故的幕后黑手如果只看文件大小你根本不知道空间被谁吃了但这条DMV能直接告诉你答案。2.2 表、列、约束5分钟导出一套字典除了空间查看表结构也是高频需求。基础查看不要求你精通所有元数据视图但至少要知道sys.tables、sys.columns、sys.indexes、sys.types是怎么关联的。我给过一个“傻瓜式字典查询”很多同事拿去以后直接用成了工作模板SELECT s.name AS SchemaName, t.name AS TableName, c.column_id, c.name AS ColumnName, TYPE_NAME(c.user_type_id) AS DataType, c.max_length AS MaxLength, c.precision, c.scale, c.is_nullable, c.is_identity FROM sys.tables t JOIN sys.schemas s ON t.schema_id s.schema_id JOIN sys.columns c ON t.object_id c.object_id WHERE t.is_ms_shipped 0 ORDER BY s.name, t.name, c.column_id;当你要给一个陌生业务库做数据字典、或者设计报表层时这条查询就是最直接的基础设施。注意TYPE_NAME返回的是系统类型名比如int、nvarcharmax_length对于nvarchar是按字节算的所以nvarchar(100)显示的是200这是一个经常让人误会的地方。索引和约束的查看也要会。索引的基础信息非常好查SELECT OBJECT_NAME(i.object_id) AS TableName, i.name AS IndexName, i.type_desc AS IndexType, i.is_unique, i.is_primary_key, i.is_unique_constraint, c.name AS ColumnName, ic.key_ordinal, ic.is_descending_key FROM sys.indexes i LEFT JOIN sys.index_columns ic ON i.object_id ic.object_id AND i.index_id ic.index_id LEFT JOIN sys.columns c ON ic.object_id c.object_id AND ic.column_id c.column_id WHERE OBJECTPROPERTY(i.object_id, IsUserTable) 1 ORDER BY OBJECT_NAME(i.object_id), i.index_id, ic.key_ordinal;一个表如果有主键SQL Server会自动建一个唯一索引如果有大量type_desc为HEAP的表你就要警惕堆表带来的碎片和转发指针问题。约束方面外键走sys.foreign_keys、检查约束走sys.check_constraints它们是结构核查里不能漏掉的环节。查看级联删除时你就得用delete_referential_action_desc这个字段去确认。3. 正在发生的动静会话、请求、阻塞现场家底摸清了下一步就是把目光切换到“此刻正在发生什么”。这一层是性能排查的核心入口也是基础查看里最体现功力的地方。很多慢查询问题、锁等待问题只有实时抓现场才看得到根因。3.1 一张连接视图看清当前所有会话我习惯把sys.dm_exec_sessions、sys.dm_exec_requests、sys.dm_exec_sql_text三张表拼起来看一次输出所有活动连接的状态、最后一条SQL、等待类型和阻塞关系SELECT s.session_id, s.login_name, s.host_name, s.program_name, s.last_request_start_time, r.status, r.command, r.wait_type, r.wait_time, r.blocking_session_id, SUBSTRING(t.text, (r.statement_start_offset / 2) 1, ((CASE r.statement_end_offset WHEN -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset END - r.statement_start_offset) / 2) 1) AS ExecutingSQL FROM sys.dm_exec_sessions s LEFT JOIN sys.dm_exec_requests r ON s.session_id r.session_id OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) t WHERE s.is_user_process 1 ORDER BY r.start_time;这条查询我放在一个快捷操作里平时排查就一键执行。你看那个ExecutingSQL字段的SUBSTRING手法就是为了从整批SQL中截出当前正在执行的那一小段否则一个几百行的存储过程会把查询结果刷得没法看。OUTER APPLY很重要因为有些会话没有活动请求r.sql_handle是NULL用JOIN会把这些连接丢掉。wait_type是性能排查的“风向标”。如果大量会话都在PAGEIOLATCH_SH或PAGEIOLATCH_EX上等待大概率是磁盘IO跟不上查询速度如果是LCK_M_X那就是锁等待有人在拿排他锁挡路如果是RESOURCE_SEMAPHORE说明内存授予排队已经形成典型的并发查询吃内存如果是CXPACKET多半和并行度设置有关。让我列个简表方便新手对照wait_type常见含义优先排查方向PAGEIOLATCH_SH/EX等待数据页从磁盘读入内存磁盘IO、缺失索引、缓冲池内存LCK_M_X排他锁等待阻塞源头、长事务RESOURCE_SEMAPHORE等待查询内存授予内存不足、并发大查询多WRITELOG等待写事务日志日志磁盘IO慢、日志增长频繁CXPACKET并行查询线程调度等待max degree of parallelism设置SOS_SCHEDULER_YIELD高CPU负载下的线程让出CPU压力、查询效率记住单独看一个等待值意义不大要结合wait_time、sql_text、session_id一起分析才能拼出完整现场。3.2 阻塞链怎么挖找出谁挡了谁阻塞是SQL Server最常遇到的“现场类”问题。一条查询卡住不动业务方给你截图说“转圈”你第一反应就应该是查blocking_session_id。最直接的方式是SELECT blocking.session_id AS BlockingSession, blocked.session_id AS BlockedSession, blocked.wait_type, blocked.wait_time, blocked.command, blocking.host_name AS BlockingHost, blocking.login_name AS BlockingLogin FROM sys.dm_exec_requests blocked JOIN sys.dm_exec_sessions blocking ON blocked.blocking_session_id blocking.session_id WHERE blocked.blocking_session_id 0;如果只有一个被阻塞的会话问题还容易排查如果出现多个会话彼此阻塞形成十字或环状就要进一步找出阻塞链的根。我有一个经验不要只盯着blocking_session_id为0的那个会话有时候A阻塞B、B阻塞C真正的问题是A但C报告给客户的却是“等B”。所以我会把sys.dm_exec_requests按blocking_session_id做一个递归式的查询从最底层往根上挖。在一个生产案例里我遇到过某个报表存储过程以READ UNCOMMITTED读取一张大表的同时又对另一张表执行INSERT操作结果在锁升级之后把后面十几个写请求全部堵死。当时就是靠这条阻塞链定位到它的。顺带提醒READ COMMITTED默认级别下读操作会申请共享锁长时间读事务同样会造成大量阻塞不仅仅是写写冲突。3.3 等待统计快照法做性能体检的正确姿势实时会话只能看到“此刻的乌云”想看这份实例的整体瓶颈必须用sys.dm_os_wait_stats。这个视图统计的是实例启动以来累计的等待情况直接看总量会被历史干扰正确做法是先存一次快照隔一段时间再取一次求差值和增量比例SELECT wait_type, waiting_tasks_count, wait_time_ms, max_wait_time_ms, signal_wait_time_ms FROM sys.dm_os_wait_stats WHERE wait_type NOT LIKE %SLEEP% AND wait_type NOT LIKE %WAITFOR% AND wait_type NOT LIKE %QUEUE% ORDER BY wait_time_ms DESC;要拿到“区间增量”就得先建一张临时表存快照等五分钟再算差值。很多人直接看累计值发现最大值永远是WAITFOR或SLEEP然后一头雾水其实这些系统后台类的等待应该先滤掉。真正做基础查看时我会把PAGEIOLATCH、WRITELOG、LCK_M、CXPACKET这些关键等待类型的wait_time_ms变化率算出来然后判断这台实例当前最吃紧的资源是内存、磁盘还是锁。这个方法不需要任何监控软件纯靠DMV就能完成一次有效的健康度体检。4. 事后现场死锁记录与索引使用的真相查看历史现场是基础查看里一个容易被忽略的分支。实时快照抓不到的问题得回到日志和统计历史里去翻。死锁和索引使用是最典型的两个“一看就懂、不查就猜”的场景。4.1 死锁图不用开跟踪直接读系统健康会话死锁一多应用层表现为频繁报错。很多新手第一反应是去开1204和1222跟踪标记抓日志但现在的SQL Server默认就有system_health扩展事件会话它会自动把死锁图录下来。你可以用一段XML解析语句直接把最近几次死锁的完整图拽出来SELECT XEventData.XEvent.value((event/data/value)[1], varchar(max)) AS DeadlockGraph FROM ( SELECT CAST(target_data AS xml) AS TargetData FROM sys.dm_xe_session_targets st JOIN sys.dm_xe_sessions s ON s.address st.event_session_address WHERE s.name system_health AND st.target_name ring_buffer ) AS Data CROSS APPLY TargetData.nodes(RingBufferTarget/event[namexml_deadlock_report]) AS XEventData(XEvent) ORDER BY XEventData.XEvent.value((event/timestamp)[1], datetime2) DESC;死锁图拿到手之后重点看两个东西两个会话各持有哪些锁、又在等待哪些锁以及每个会话最后执行的语句是什么。这里我特别想说一句死锁图是XML格式新手容易被那一大段结构吓住但其实只要搜索owner-modeX和request-modeS就能快速找到冲突点。实际排查中超过90%的死锁是程序访问对象的顺序不一致比如会话A先更新表X再更新表Y会话B先更新表Y再更新表X调整代码顺序比调数据库参数有效得多。如果你在旧版本上工作system_health里可能查不到死锁记录这时才考虑临时开启跟踪标记DBCC TRACEON(1222, -1); DBCC TRACEON(1204, -1);但请记住这是有代价的手段会往错误日志里刷大量信息生产环境用完后要记得关闭而且1222和1204同时开启会重复记录同类信息正常开1222就够了。这里就不详细贴关闭脚本了一句话DBCC TRACEOFF(1222, -1)。4.2 索引到底用没用别靠猜要看列级统计索引使用情况是“基础查看”和“性能优化”之间的一座桥。很多人一上来就加索引结果越加越多从来不看历史使用统计。sys.dm_db_index_usage_stats就是回答这个问题的官方数据源SELECT OBJECT_NAME(i.object_id) AS TableName, i.name AS IndexName, i.type_desc, us.user_seeks, us.user_scans, us.user_lookups, us.user_updates, us.last_user_seek, us.last_user_scan, us.last_user_lookup, us.last_user_update FROM sys.dm_db_index_usage_stats us JOIN sys.indexes i ON us.object_id i.object_id AND us.index_id i.index_id WHERE us.database_id DB_ID() ORDER BY (us.user_seeks us.user_lookups) ASC;我判断“无效索引”的参考标准是user_seeks和user_lookups长期为0而user_updates很大。说明这个索引几乎没有被查询利用过却每次DML都要额外维护一份这种索引就该纳入删除候选。但注意两个坑第一个坑服务器重启后这个视图的数据会清零。你在刚重启完的时候跑这条查询看到全是0不代表索引没用要等业务跑过一轮才能下结论。第二个坑这里的统计只覆盖“已成功编译执行”的查询不包含当时在执行的冒险查询和编译失败的查询所以它是“最小值”不是全部。想看哪些查询经常在表扫描可以连接sys.dm_exec_query_stats和sys.dm_exec_sql_text找出高执行次数却仍走全表扫描的语句这类语句往往是索引建议的直接来源。不过在决定加索引前我会再看一眼sys.dm_db_missing_index_detailsSELECT CONVERT(varchar(50), d.statement) AS TableName, d.equality_columns, d.inequality_columns, d.included_columns, gs.avg_user_impact, gs.user_seeks, gs.last_user_seek FROM sys.dm_db_missing_index_details d JOIN sys.dm_db_missing_index_group_stats gs ON d.group_handle gs.group_handle WHERE d.database_id DB_ID() ORDER BY gs.avg_user_impact DESC;这里必须泼一盆冷水缺失索引建议是成本估算的产物它往往能显著提升单条查询的读取性能但完全没有计算索引维护对写入带来的负担。我见过一个团队照单全收缺失索引建议一次性加了二三十个索引最后写入性能掉了一半。合理做法是挑选avg_user_impact高但实施面积小的建议并且在一个维护窗口内分批创建、分批验证。5. 陌生服务器接手后的标准化巡检流程说了这么多单独的查看角度最后把它们串成一套可以在15分钟内完成的标准流程。这是我从多次接手经历中总结出来的“不看门面看里子”的清单也建议你做成脚本文件存在工具包目录下每次接新实例直接跑输出结果贴到工作记录里。5.1 巡检顺序先机器、再实例、后库表整个流程的先后顺序是有讲究的我认为它符合“从外围到核心、从静止到动态”的原则。先看操作系统和SQL Server版本信息VERSION、SERVERPROPERTY确认环境基础。查看实例级内存和并发配置sys.configurations框定优化边界。拉全部数据库清单sys.databases重点确认有没有异常状态库。对每个在线库查文件空间sys.database_files、sys.dm_db_file_space_usage或DBCC SQLPERF(LOGSPACE)。跑一次活动会话和等待类型快照sys.dm_exec_requests、sys.dm_os_wait_stats记录当时的指标。访问错误日志sys.dm_os_performance_counters或sp_readerrorlog看有没有基础报错。如果有条件采样半小时等待统计和索引使用统计形成增量报告。这个顺序最大的价值在于一旦后面几步出现性能异常你前面已经掌握了环境和空间的底数不会慌。我在一次现场排查中客户只说“数据库特别慢”我按这个顺序走到第4步就发现某个日志文件已经增长到接近磁盘上限自动增长反复触发每触发一次要等几分钟整个库的性能都被拖垮。如果没有先查文件空间而是直接去查阻塞链那就会在错误的方向上浪费半天。5.2 给“下一班交接人”留下什么做完这些查看之后我会顺手写一份极简的交接摘要实例版本、内存上限、每个库的文件和容量、各库当前兼容级别、当前占用最高的等待类型前三名、最近一次死锁时间点。这不是写报告就是给未来的自己或同事留个底。下一次出了问题时你只需要对比这份基线很快就能知道变化出在哪里。这里要特别强调一个接近“潜规则”的经验任何查看动作只要是在生产库上执行的我都会开在只读事务上下文里或者至少在业务低峰期执行。像sys.dm_db_index_usage_stats、sys.databases这些元数据查询本身没什么杀伤力但如果你在一个压力很高的实例上反复跑大范围聚合查询也会给系统添负担。保险的做法是把脚本组织成一个只读查询批次不包含任何UPDATE、DBCC SHRINKFILE等隐性维护操作。5.3 定期查看而不是出事后查看基础查看有一个反直觉的特点最有效的时机往往是在系统还没有出问题的时候。因为等你接到“数据库卡死”的电话再去看counter值已经偏离基线wait_stats里堆满了瞬时峰值你很难快速判断“现在这个样子算不算异常”。所以我会建议每个维护的实例至少每周跑一次第5.1节的巡检脚本把输出保存到工作区留出历史对比。这样等你某天真的需要查看时你手上已经有了一台服务器从健康到亚健康的全过程记录排查问题的速度会快一个量级。我在实际项目里遇到过不少“愈演愈烈”的问题最典型的一种是日志文件每周涨一点开始只有几百兆所有人都不当回事直到某天磁盘可用空间跌破阈值才慌。如果当初把DBCC SQLPERF(LOGSPACE)的输出存成周报那这个问题在第一次巡检时就会暴露根本轮不到它变成事故。基础查看的意义说白了就是把“未知”变成“已知”把“事后救火”变成“事前发现”。这套脚本你完全可以拿过去改改就用需要长期监控的就加个计划任务拿来即用就是它最大的价值。