简介:PDF文件汇总了SQL Server高频面试题及答案,按“数据库基础知识”“SqlServer笔试基础”“SqlServer笔试高级”三部分编排,适合准备数据库岗位笔试或面试的开发者、运维人员使用。内容覆盖主键与外键、自增主键优势、触发器与存储过程、视图与游标、drop/truncate/delete差异、临时表、数据库范式、内连接外连接与笛卡尔积、varchar与char场景、count(*)/count(1)/count(column)区别、最左前缀原则及索引优化等高频考点,并附带年龄与平均成绩查询、成绩更新、SQL注入防范等基础与高级笔试题示例,既有概念梳理也有可操作代码说明。资源为单个PDF文档,共1个文件,压缩包大小776KB,目录层次清晰,便于下载后离线阅读、按章节检索。该资料已有2838人学习下载,适合作为SQL Server知识点自查、笔试刷题和考前冲刺的实用手册。
1. SQLServer高频面试题:为什么你背了答案还是挂
SQLServer岗位面试和别的后端技术面不太一样,考官很少让你手写一个排序算法或者设计模式,更多时候是抛出一个生产环境里真实发生过的场景,比如“这个索引为什么没走”“这个死锁是怎么来的”“这个存储过程为什么越跑越慢”。你如果只背概念,没在本地环境里亲手复现过,很容易在追问环节露馅。SQLServer高频面试题这个词看起来是“背答案”,实际上考的是你对事务、锁、索引、执行计划这些底层机制的理解深度,以及你排错时看的是不是关键证据。
这篇文章不打算给你列一百道题再配一百个标准答案。那样你记不住,考官也一眼看穿。我把面试里最常被追问的几大块拆开——索引与执行计划、事务与锁、阻塞与死锁、性能排查、存储过程与触发器——每一块都按“原理先立住、再给你能直接复现的最小案例、最后告诉你坑在哪”来写。你跟着把脚本跑一遍,比背二十道题有用得多。适合正在准备高级开发或DBA方向面试的人,也适合在现网被SQLServer折磨过、想系统补一遍底层逻辑的从业者。
先说个反直觉的结论:SQLServer面试里翻车率最高的不是最难的分布式事务,反而是“索引为什么失效”这种基础得不能再基础的问题。因为大多数人只记得“不要在列上做函数操作”这个结论,却说不清优化器到底怎么选索引、书签查找和覆盖索引的执行成本差在哪里。这篇文章就从这里开始拆。
2. 索引与执行计划:面试必问但最容易答偏的一关
2.1 聚集索引和非聚集索引的本质区别:别再只说“叶子节点存数据”
面试官问“聚集索引和非聚集索引的区别”,十个有八个会答“聚集索引叶子节点存整行数据,非聚集索引叶子节点存索引列加主键”。这句话没错,但只值两分。真正拉开差距的是下面这层:聚集索引决定了表的物理存储顺序,所以一张表只能有一个聚集索引;而非聚集索引是独立的存储结构,叶子节点存的是聚集索引键(如果没有聚集索引,就是行标识符RID),所以非聚集索引可以建很多个。
面试追问通常是这样的:“如果一个表没有聚集索引,非聚集索引的叶子节点存什么?”答案是RID,也就是物理行号。这张表就成了堆表。堆表的非聚集索引在插入、删除频繁时,RID会变,索引维护成本更高。这个问题能答出来,说明你不只是背了书,而是理解了两个结构在存储引擎层面的差异。
还有一个高频追问:“为什么覆盖索引能避免书签查找?”书签查找的意思是,通过非聚集索引找到索引键和聚集键之后,还要再用聚集键回表查一遍才能拿到其他列的数据。如果查询需要的列全部包含在非聚集索引里,优化器就不需要回表,这就是覆盖索引。你可以用它来验证:
-- 建测试表 CREATE TABLE dbo.TestIndex ( Id INT IDENTITY PRIMARY KEY, Name NVARCHAR(50), Score INT, CreateTime DATETIME ); GO -- 插入十万行测试数据 WITH Nums AS ( SELECT TOP 100000 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS n FROM sys.all_columns a CROSS JOIN sys.all_columns b ) INSERT INTO dbo.TestIndex(Name, Score, CreateTime) SELECT 'User' + CAST(n AS NVARCHAR(10)), n % 100, DATEADD(day, n % 365, '2024-01-01') FROM Nums; GO -- 创建非聚集索引 CREATE INDEX IX_TestIndex_Name ON dbo.TestIndex(Name); GO -- 开启执行计划信息 SET STATISTICS IO ON; SET STATISTICS TIME ON; GO -- 情景A:查询不在索引里的列,出现书签查找 SELECT Id, Name, Score FROM dbo.TestIndex WHERE Name = 'User50000'; GO运行之后看消息窗口,如果Score列不在索引里,执行计划里会出现“键查找”(Key Lookup),这就是书签查找,IO逻辑读取次数会是两到三次。然后我们把索引改成覆盖:
DROP INDEX IX_TestIndex_Name ON dbo.TestIndex; GO CREATE INDEX IX_TestIndex_Name_Include ON dbo.TestIndex(Name) INCLUDE (Score); GO SELECT Id, Name, Score FROM dbo.TestIndex WHERE Name = 'User50000'; GO这次执行计划里只剩“索引查找”(Index Seek),IO次数降到一次。把两次IO数据对比给面试官看,比光背概念要可信得多。
2.2 索引失效的真相:优化器不是“不用”,而是“算完发现不划算”
关于索引失效,流传甚广的说法是“对索引列使用函数会导致索引失效,所以千万别这么写”。这句话在SQLServer里并不完全准确。SQLServer的优化器有时候会产生Compute Scalar计算列,把函数结果计算出来再去匹配索引,索引照样能用。真正让索引失效的,是优化器算完基数之后发现走索引的成本比全表扫描还高,这时候它不会为了“用索引”而用索引。
我一般会在面试时直接抛例子:如果一张十万行的表,查询条件Name = 'User50000',这个条件能筛掉绝大多数行,优化器会选择索引。但如果把条件改成Name LIKE '%50000%',前导通配符让索引无法用于定位,优化器一算,回表加索引查找的成本比全表扫描还大,干脆就不走这个索引了。你可以用下面这组对比验证:
-- 查询计划对比 SET SHOWPLAN_ALL ON; GO -- 前导模糊查询:大概率表扫描 SELECT Id, Name FROM dbo.TestIndex WHERE Name LIKE '%50000%'; GO -- 非前导模糊查询:索引查找 SELECT Id, Name FROM dbo.TestIndex WHERE Name LIKE 'User50%'; GO SET SHOWPLAN_ALL OFF; GO注意,前导通配符导致的“索引失效”本质不是索引坏了,而是这种查询本身无法从B树的有序性里获得快速定位的好处,必须扫描所有条目。优化器选择扫描,在逻辑上毫无问题,是应用层的查询方式错了。最直白的生产教训是:别依赖“SQLServer很智能”这个说法,能用等于号就用等于号,能用范围查询就用范围查询,前端搜索框里那种模糊搜索如果必须支持前导通配符,就应该单独走全文索引方案,而不是硬扣普通索引。
2.3 统计信息和参数嗅探:面试里最容易被问倒的暗坑
统计信息是优化器生成执行计划时的决策依据。表里的数据分布变了,统计信息没更新,优化器还按老黄历估算行数,执行计划就会跑偏。面试题经常这么问:“为什么昨天还跑得好好的SQL,今天突然变慢了?”答案里十有六七是统计信息过期或参数嗅探。
参数嗅探是指存储过程第一次执行时,优化器根据传入的具体参数值生成了执行计划,之后不管传入什么参数,都沿用第一次的执行计划。如果第一次传入的是个高频值,走索引划算,后面传了个低频值或者数据分布变了,这个计划就不划算了。复现它有两种路子。一种是直接伪造:
-- 构造数据倾斜 DROP TABLE IF EXISTS dbo.SniffTest; CREATE TABLE dbo.SniffTest ( Id INT IDENTITY PRIMARY KEY, Category VARCHAR(20), Value DECIMAL(10,2) ); GO -- 99%的数据是'A',只有1%是'B' INSERT INTO dbo.SniffTest(Category, Value) SELECT 'A', RAND() * 1000 FROM sys.all_columns; GO INSERT INTO dbo.SniffTest(Category, Value) SELECT 'B', RAND() * 1000 FROM sys.all_columns; GO CREATE INDEX IX_SniffTest_Category ON dbo.SniffTest(Category); GO -- 第一次执行用高频值'A',计划走索引或扫描取决于优化器判断 EXEC sp_executesql N'SELECT * FROM dbo.SniffTest WHERE Category = @cat', N'@cat VARCHAR(10)', @cat = 'A'; GO -- 第二次用低频值'B',大概率沿用计划 EXEC sp_executesql N'SELECT * FROM dbo.SniffTest WHERE Category = @cat', N'@cat VARCHAR(10)', @cat = 'B'; GO生产环境里怎么看?用sys.dm_exec_query_stats和sys.dm_exec_plan_attributes去查计划缓存里这个存储过程的创建时间和参数值。面试答到这个深度,基本能压住绝大多数候选人。再往下追问就是解决方案:OPTION (RECOMPILE)每次重新生成计划,适合执行频率低但单次耗时长的小存储过程;OPTION (OPTIMIZE FOR UNKNOWN)让优化器按平均分布生成计划,适合参数值分布稳定但每次传入值差异大的场景。你把这些说清楚,面试官就知道你不只是见过这个概念,而是在线上用过。用WITH RECOMPILE则是对参数嗅探的另一种解法,但代价是每次都丢缓存,需要权衡。
3. 事务与锁:别等到死锁才来翻这篇
3.1 隔离级别到底隔离了什么:读已提交的坑比你想的多
SQLServer的隔离级别有四种:读未提交、读已提交、可重复读、可序列化。默认是读已提交,也就是在语句级别加共享锁,读完后立即释放。很多人以为“读已提交就不会读到脏数据”,这是对的,但读已提交解决不了不可重复读——同一个事务里两次相同的SELECT,结果可能不一样,因为别的事务在这两次查询之间提交了修改。
面试题经典问法是:“可重复读能避免幻读吗?”答案是不能。可重复读只锁住了当前读到的行,不让别的事务修改或删除这些行;但别的事务可以往范围里插入新行,第二次读的时候多出几行,这就是幻读。只有可序列化级别会用范围锁把整个区间锁住,才能挡住插入。你可以在本地开两个查询窗口验证:
-- 窗口1:开启事务,查询特定范围 BEGIN TRANSACTION; SELECT * FROM dbo.TestIndex WHERE Score = 50; GO -- 此时不要提交,切到窗口2插入一行Score=50的数据 INSERT INTO dbo.TestIndex(Name, Score, CreateTime) VALUES ('Phantom', 50, GETDATE()); GO在默认读已提交下,窗口2会立即成功,因为这个隔离级别下窗口1的共享锁在读完之后就释放了,没有锁挡住插入。如果把隔离级别改成可序列化再试,窗口2会被阻塞。面试官追问“那你实际开发中怎么选”,你最好用读已提交加UPDLOCK表提示来折中,这比动不动就上可序列化要专业得多。毕竟可序列化并发度极低,生产环境用它是拿性能换一致性,要格外小心。
3.2 锁升级、锁碎片和锁等待:这些才是线上真实痛点
锁升级是指SQLServer把大量细粒度的行锁或页锁自动提升为表锁。触发阈值大约是5000个锁或者锁内存超过总内存的40%。锁升级不是bug,是节省内存开销的策略,但代价是并发骤降。面试题经常这么问:“为什么我的表并发插入越来越慢?”答案可能就是锁升级。
常见规避手段有三种:分区表、用ALTER TABLE ... SET LOCK_ESCALATION = DISABLE(仅限分区表)、以及控制单事务内操作的行数。分区表的做法是把数据物理拆到不同分区,每个分区最多5000把锁;禁用锁升级则要承受内存压力;控制行数最简单,但业务上不一定允许。
死锁的经典场景是两个事务互相持有对方需要的资源,然后各自等待。SQLServer的锁管理器会检测死锁,选一个牺牲者回滚,另一个继续。面试官喜欢问“你怎么找到死锁的根源”。我会这么答:用DBCC INPUTBUFFER查看死锁发生时两个会话各自执行的语句,同时打开跟踪标志1204或者1222,或者用扩展事件sqlserver.lock_deadlock把死锁图抓出来。例如:
DBCC TRACEON (1204, -1); GO -- 死锁发生之后,错误日志里会记录死锁的详细资源链 -- 排查完后关闭 DBCC TRACEOFF (1204, -1); GO生产环境我更推荐用脚本监控系统健康会话里的死锁报告,或者建立扩展事件会话,把死锁图保存成.xdl文件,用SSMS打开图形化查看。死锁图里能看到两个进程的输入缓冲、持有锁和等待锁的资源类型,排错会比纯日志高效很多。死锁的经典解决套路是让所有事务按照相同顺序访问资源,比如不管哪条业务路径,都先更新主表再更新子表。但这个方案在复杂业务流程里很难落地,更实际的办法是缩小事务范围、减少锁持有时间,以及合理设置索引减少锁定的行数。
3.3 快照隔离和基于行版本控制的隔离:面试加分项
说到隔离级别,SQLServer里还有两个基于行版本控制的选项:读已提交快照和快照隔离。读已提交快照是在数据库级别开启READ_COMMITTED_SNAPSHOT = ON之后,读已提交的读操作改为从版本链上读取提交版本,不会阻塞写操作,写操作也不会阻塞读。这个选项能显著降低读写阻塞,但会占用tempdb的空间,因为每次更新都要在tempdb里分配版本存储。
面试考题一般是“快照隔离和读已提交快照的区别是什么”。核心区别有两点:读已提交快照的每条语句都拿当前语句开始时的版本,快照隔离是整个事务开始时的版本;快照隔离需要设置数据库选项ALLOW_SNAPSHOT_ISOLATION = ON,同时应用程序也要在事务里显式指定隔离级别。开启这些选项后,更新冲突处理也不同:快照隔离下如果两个事务同时更新同一行,后提交者会收到3960错误,需要重试;读已提交快照则没有这个冲突检测机制。
给你的建议是,面试被问到这类问题时,先表明自己知道行版本控制的原理,再说清楚开启后的代价,最后补一句“生产环境开启前需要评估tempdb的大小和写入负载,不然版本存储会让tempdb爆炸”。能够权衡利弊而不是无脑推荐,才是面试官想看到的。
4. SQLServer常见问题避坑:你大概率踩过的五个坑
4.1 字符串转数字:转换报错还是隐式转换拖慢查询
sqlserver 字符串转数字在热搜词里的排名不低,因为生产环境里经常要把NVARCHAR列转成数字做关联或者排序。直接写CAST(column AS INT)遇到非数字字符就报错。这类问题通常出现在导入外部数据时,数据里混着空字符串、千分位、货币符号或换行符。现象是SSMS报“在将nvarchar值转换成数据类型int时失败”。
原因分两种:一是数据本身脏,需要清洗;二是设计时不该用字符串存数字,这个属于历史包袱。解决思路是先查脏数据再转换,用TRY_CAST处理并记录失败行:
-- 找出来不能转成数字的行 SELECT BadValue, TRY_CAST(BadValue AS INT) AS Converted FROM dbo.ImportStaging WHERE TRY_CAST(BadValue AS INT) IS NULL AND BadValue IS NOT NULL;TRY_CAST是SQLServer 2012以后提供的安全转换函数,转换失败返回NULL,不会抛异常。如果你想强制让失败的行进入错误处理逻辑而不是静默变NULL,可以用TRY_CONVERT搭配CASE做重试,或者把失败的行原样写入一张异常表。这里还有个隐藏坑:WHERE TRY_CAST(...) IS NULL会把本来就是NULL的行也过滤掉,但团队里有人会把空字符串转出来的NULL和真实NULL混为一谈,所以清洗前先确定这两者的处理策略。
更深一层的坑是隐式转换。比如一个VARCHAR列和一个INT列做等值关联,SQLServer会把VARCHAR列隐式转成INT。一旦列上有索引,这个隐式转换就成了“函数操作”,索引直接失效。排查方法是用执行计划看有没有CONVERT_IMPLICIT相关的警告,或者练就一双看数据类型的眼睛。解决方法是改表结构,或者写代码时主动显式转换。
4.2 安装和配置:版本选不对,后面全是坑
sqlserver安装教程是另一个高频搜索词。很多人一开始就装了个企业版或者开发版,但不知道各版本之间的许可差别。SQLServer的版本有Express、Standard、Enterprise、Developer等。Express版免费但有数据库大小限制,Standard版没有大小限制但缺少部分高级功能,Developer版功能完整但仅许可用于开发和测试,不能用于生产环境。开发机上选Developer版最合适,生产环境按预算和功能需求选Standard或Enterprise。
安装过程的坑通常在服务账号和身份验证模式上。安装时选了Windows身份验证,后面Java或Python程序要用用户名密码连接,就会觉得“连不上”。安装完再改身份验证模式需要在SSMS里改服务器属性,还要重启服务。另一个坑是默认端口1433被其他程序占用了,或者防火墙没放行,客户端报“在建立到服务器的连接时发生错误”。所以安装完成后第一件事不是连接写SQL,而是检查SQLServer服务是否启动、端口监听是否正常、防火墙是否放行。
4.3 无法导入数据,数据无效:兜底方案要先设计
sqlserver 无法导入数据 数据无效这个热搜词,通常指向导入向导导入CSV或Excel时报错。很多人直接打开SSMS的导入向导,选完文件就点“下一步”,结果报错“无法解析列数据”。常见原因有三种:文件编码不是UTF-8而是带BOM或无BOM的GBK、分隔符选错、列类型推断失败。
我的习惯是先别让向导自动推断类型。导入向导每一步都有“高级”按钮,可以手动指定每列的数据类型和长度,尤其是日期时间列,最好显式写成nvarchar先导进去,再用SQL做类型转换和清洗。特别是当源文件里有“N/A”“-”这类占位符时会导出失败,改为先用脚本把脏值替换成NULL再导入,就稳得多。
更推荐的方案是放弃可视化向导,直接写BULK INSERT或OPENROWSET(BULK ...),这样清洗逻辑可以脚本化,遇到同样的脏数据能直接复用:
BULK INSERT dbo.Staging_Users FROM 'C:\Data\users.csv' WITH ( FIELDTERMINATOR = ',', ROWTERMINATOR = '\n', FIRSTROW = 2, CODEPAGE = '65001' );CODEPAGE='65001'指UTF-8编码;如果源文件是GBK,改成'936'。FIRSTROW=2跳过表头,ROWTERMINATOR='\n'需要根据文件实际换行符调整,Windows下可能是'\r\n'。数据导进临时表后,再用TRY_CAST逐列清洗,最后INSERT INTO ... SELECT进正式表,这样每个环节都有日志可查。配套地做一张异常记录表,把转换失败的行原样保留下来,比导入一半报错然后全部回滚要人性化得多。
4.4 日志文件暴涨:数据库说没就没的服务器
日志文件暴涨一般是事务日志没有做好备份管理。数据库的恢复模式如果设置成“完整”,日志不会自动截断,除非定期做日志备份。如果只做全量备份不做日志备份,ldf文件会一直膨胀,直到占满磁盘。这个坑我还真踩过:凌晨一个跑批事务回滚,日志文件几分钟内撑爆了磁盘,整个实例上的其他数据库全部遭殃。
解决分两步。第一步是止损,如果当前不需要时间点恢复,把恢复模式临时切到“简单”,然后收缩日志文件:
ALTER DATABASE MyDB SET RECOVERY SIMPLE; DBCC SHRINKFILE(MyDB_Log, 100);第二步是治本,恢复完整恢复模式后,配置定期日志备份任务。DBCC SHRINKFILE的第二个参数是目标大小,单位MB,一次性缩得太小会导致次日日志暴涨得更猛,建议逐步收缩到合理区间。这里有个细节:如果你在做镜像或AlwaysOn可用性组,主库的恢复模式不能随便切换,必须先处理可用性组的同步和备份策略,否则整个高可用链路会出问题。
面试中聊到这个点,我会主动提“日志备份的频率取决于你允许丢失多少数据”,比如允许丢失15分钟,日志备份就可以15分钟一次;如果能接受1小时,就1小时一次。这个说法能让面试官看到你不是只会执行命令,而是懂的取舍。
4.5 数据库恢复模式:完整和简单之间的取舍
很多新手把恢复模式改成简单模式,以为只是省空间,其实丢掉的是时间点恢复能力。完整恢复模式下,日志从最后一次完整备份开始连续记录,你可以把数据库还原到日志中任意一个时间点,比如某个误删数据的时刻。简单模式下,事务日志会被自动截断,只能还原到最近一次备份的时间点。
面试题经常这么问:“为什么我不能还原到昨天下午两点?”回答思路是先看恢复模式,如果已经是简单模式,那需要先做一次完整备份,然后切到完整恢复模式,再定期做日志备份,才能支持时间点还原。把这个逻辑讲清楚,比背“简单模式没有日志备份”要深刻得多。生产环境的选择建议是:核心交易系统用完整恢复模式加定期日志备份,分析型报表库如果不太需要考虑时间点恢复,用简单模式省心。
5. 性能排查实战:从执行计划到动态管理视图的完整路径
5.1 抓取耗时最高的SQL:别再让DBA翻库存了
面试官问“线上突然变慢你怎么排查”,最差的回答是“看下是不是CPU满了”。正确的排查路径应该是:先找到占用资源最高的会话,再定位到具体SQL,然后看执行计划。SQLServer提供了一套现成的动态管理视图,组合起来用效率很高。
SELECT s.session_id, r.status, r.command, r.cpu_time, r.total_elapsed_time, t.text 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.status NOT IN ('background', 'sleeping') ORDER BY r.cpu_time DESC; GO这个查询会把当前正在执行的会话里CPU时间最长的SQL捞出来。sys.dm_exec_requests是每一个正在执行的请求,sys.dm_exec_sql_text基于sql_handle把SQL语句文本解析出来。如果SQL太长,dm_exec_sql_text.text拿到的是完整批处理文本,还要配合STATEMENT_START_OFFSET和STATEMENT_END_OFFSET才能截出当前正在跑的语句,这个细节可以写进面试答案里。
如果要查历史跑得慢的SQL,可以用sys.dm_exec_query_stats:
SELECT TOP 10 qs.total_worker_time / qs.execution_count AS avg_worker_time, qs.total_elapsed_time / qs.execution_count AS avg_elapsed_time, qs.total_logical_reads / qs.execution_count AS avg_logical_reads, qs.execution_count, SUBSTRING(st.text, (qs.statement_start_offset + 2) / 2, (CASE WHEN qs.statement_end_offset = -1 THEN LEN(CONVERT(NVARCHAR(MAX), st.text)) * 2 ELSE qs.statement_end_offset - qs.statement_start_offset END) / 2) AS sql_text FROM sys.dm_exec_query_stats qs CROSS APPLY sys.dm_exec_sql_text(qs.sql_handle) st ORDER BY avg_worker_time DESC; GOtotal_worker_time是CPU总耗时,execution_count是执行次数,两者相除得到平均CPU消耗;total_logical_reads则反映内存里逻辑读了多少页,这个指标对索引缺失的判断比CPU更直观。你在面试里说“我先按平均CPU时间排序,再结合逻辑读看是否缺索引”,比一上来就抓执行计划要专业得多。
5.2 读懂执行计划里的代价百分比:哪些值得信,哪些不能信
执行计划上每个算子都带一个“估计成本百分比”,但新手容易误以为它是实际的耗时占比。SQLServer的估计成本是基于统计信息算出来的,不是实测值;如果统计信息不准,这个百分比会骗人。真正靠谱的做法是看每个算子实际返回的行数,和估计行数对比。如果估计行数和实际行数差几个数量级,说明统计信息过时了。
UPDATE STATISTICS dbo.TestIndex; GO -- 或者单独更新某条索引的统计信息 UPDATE STATISTICS dbo.TestIndex IX_TestIndex_Name_Include WITH FULLSCAN; GOUPDATE STATISTICS ... WITH FULLSCAN表示扫描全表来计算统计信息,耗时更长但更精确;默认是采样统计,适合大表。面试里提到这个点,最好补一句“更新统计信息只能在业务低峰期做,不要在大表上频繁FULLSCAN,否则统计信息更新本身会拖垮实例”。
执行计划里还有一个经常被忽略的东西:缺失索引提示。优化器在生成计划时如果发现某个表的扫描明显缺索引,会有一段绿色的斟酌提示“缺少索引”。SQLServer会把建议存到sys.dm_db_missing_index_details里。你可以直接查这个视图来生成候选索引:
SELECT d.statement AS table_name, d.equality_columns, d.inequality_columns, d.included_columns, s.avg_total_user_cost * s.avg_user_impact / 100.0 AS potential_improvement FROM sys.dm_db_missing_index_details d JOIN sys.dm_db_missing_index_group_stats s ON d.index_group_handle = s.group_handle ORDER BY potential_improvement DESC; GO需要提醒的是,“缺少索引”的建议不是让你无脑建索引。索引能加速查询,但会拖慢写入、占用磁盘和内存。而且这个动态管理视图里的缺失索引信息从实例启动后开始累计,不会自动重置,长时间运行的系统里建议可能堆积很多。实操上我会先把potential_improvement高、且inequality_columns和included_columns都不冗长的建议挑出来,在测试环境验证后再说。别迷信SQLServer生成的建议,关键是读懂它为什么这么建议。
5.3 等待类型:性能排查的另一个入口
除了看SQL和执行计划,sys.dm_os_wait_stats能告诉你整个实例在什么类型上等待最多。常见的等待类型有PAGEIOLATCH_SH(读数据页时等待磁盘IO)、LCK_M_X(等锁)、WRITELOG(写日志等待)、RESOURCE_SEMAPHORE(并发查询内存不足)。面试官如果问到“SQLServer实例整体变慢怎么定位”,你答“先看等待类型再针对性排查”,一般能留下不错的第一印象。
SELECT wait_type, wait_time_ms, waiting_tasks_count, wait_time_ms / waiting_tasks_count AS avg_wait_ms, max_wait_time_ms FROM sys.dm_os_wait_stats WHERE wait_type NOT LIKE '%SLEEP%' AND wait_type NOT LIKE '%CLR%' ORDER BY wait_time_ms DESC; GO常见的过滤器会排掉一些无关等待类型,比如SLEEP、CLR、BROKER和QUEUE这些系统内部任务。PAGEIOLATCH_SH高说明磁盘IO跟不上,优先看是不是缺索引导致读了太多没用到的页,而不是马上升级硬件;WRITELOG高说明日志写入压力大,优先看是不是事务太频繁、日志磁盘太慢;RESOURCE_SEMAPHORE高说明并发查询太多,内存Grant不够分配,优先看MAXDOP和查询内存配置。
面试答这个点,可以加一句“等待类型只是入口,最后还是落到具体SQL”来体现自己是实战派而不是背视图名。
5.4 数据库备份策略:开发可能不管,面试一定会问
备份是每个DBA岗位面试的必问内容,但开发岗也会被问到,因为很多小公司没有专职DBA,设置备份任务的任务就落在后端开发身上。SQLServer的备份类型有完整备份、差异备份、事务日志备份,以及文件组备份。我对小团队的默认建议是:完整备份一天一次放凌晨低峰期,事务日志备份看RPO要求,如果允许丢15分钟数据就15分钟一次。差异备份在这个框架里属于锦上添花,如果你的完整备份耗时很久,才考虑用差异备份减少恢复时间。
备份语句本身不算复杂,但要注意备份文件的命名规范,最好包含数据库名、备份类型和时间戳:
BACKUP DATABASE MyDB TO DISK = N'D:\Backup\MyDB_FULL_20250601.bak' WITH INIT, COMPRESSION, CHECKSUM; GOWITH INIT覆盖同名文件,COMPRESSION做备份压缩,CHECKSUM在备份过程中做校验和验证,能提早发现页面损坏。恢复语句就不写了,因为面试官很少让你默写RESTORE ... WITH RECOVERY和RESTORE ... WITH NORECOVERY,但你要知道区别:RECOVERY是恢复完直接上线,NORECOVERY是继续恢复下一个备份。做日志还原时,除了最后一个备份用RECOVERY,其余都要用NORECOVERY。
这个章节的内容对应到面试场景,建议你从五类问题去准备构建自己的“排查脑图”:一是会话级排查,二是语句级排查,三是执行计划分析,四是等待类型分析,五是备份恢复验证。面试官如果问“你在SQLServer这个领域最擅长什么”,你就可以说“性能排查和基础运维,能在半小时内从会话定位到SQL再给出索引优化建议”,这个定位足够让大多数岗位产生兴趣。
6. 存储过程与触发器:面试考的是边界感
6.1 存储过程的优缺点:别把它当成万能药
存储过程面试题一般这么问:“存储过程相比应用程序直接执行SQL有什么好处?”你要抓住几个核心点:预编译执行计划可以复用,网络开销更小因为只传过程名和参数、不传输整个SQL文本,更利于权限控制比如只给存储过程的执行权限不让用户直接操作表,还方便版本化管理和集中维护。
但存储过程的缺点也要说,不然面试官会认为你只有单一视角。缺点包括:业务逻辑写进数据库后,版本控制和灰度发布比代码困难;数据库服务器的CPU压力会变大,主要是SQL编译和排序哈希操作占CPU;跨数据库或跨实例调用不如应用程序层灵活。我更推荐把复杂计算放在应用层,数据库只负责存取和事务边界。面试能说出“存储过程用得太多会导致数据库成为瓶颈”这种话,面试官会认为你是个有架构观的开发。
另外一个高频考点是存储过程里的错误处理。TRY...CATCH在SQLServer里居于BEGIN TRY和BEGIN CATCH之间,CATCH块里用ERROR_NUMBER()、ERROR_MESSAGE()、ERROR_LINE()捞取错误信息,配合RAISERROR抛出自定义错误。一组最简单的模式是:
CREATE PROCEDURE dbo.usp_UpdateScore @Id INT, @Score INT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; UPDATE dbo.TestIndex SET Score = @Score WHERE Id = @Id; IF @@ROWCOUNT = 0 BEGIN THROW 50001, '记录不存在', 1; END COMMIT TRANSACTION; END TRY BEGIN CATCH IF XACT_STATE() <> 0 ROLLBACK TRANSACTION; THROW; END CATCH END GOSET NOCOUNT ON的作用是关闭“受影响行数”的返回消息,减少网络流量。THROW是SQLServer 2012以后提供的错误抛出方式,比RAISERROR简单,支持在CATCH块里直接调用THROW重新抛出原始错误信息。XACT_STATE()检查当前事务是否仍然有效,-1表示事务已被提交或回滚,0表示没有事务,1表示事务可继续。使用THROW重抛时,错误号需要在50000以上,错误严重级别默认为16。这一套逻辑说下来,面试官马上就知道你写过质量不低的生产存储过程。
6.2 触发器:能不用就不用,用了必须知道这几个副作用
触发器面试题的激进问法是“你什么时候用过触发器,后悔吗”。我不会只给一个否定的答案,而是先说明触发器的适用场景:审计日志、数据同步、或者无法在应用层弹事务的时候做补偿。但副作用一定要说清楚——触发器内的操作和原事务在同一个事务里,触发器抛错会导致整个事务回滚;隐式触发还可能引发嵌套调用,性能影响很难预测。
生产环境我一般建议写成“保持触发器足够短,只做必要的写操作,不查大表,不加不必要的索引操作”。如果确实需要审计,可以用扩展事件或CDC(Change Data Capture)替代,它们对业务表的侵入比触发器小得多,也不容易造成事务回滚的连带意外。开发岗被问触发器时,你表现出“能不用就不用”的克制,比炫耀用过多少个触发器更受认可。
6.3 动态SQL与SQL注入:这道题是必答题
存储过程里如果拼了动态SQL,面试官大概率会追问注入怎么防。最稳的答案是参数化查询,也就是用sp_executesql加参数声明的方式把参数传进去,而不是字符串拼接。sp_executesql能在每次执行时重新编译并参数化,还能复用计划缓存。
DECLARE @sql NVARCHAR(4000); DECLARE @params NVARCHAR(4000); SET @sql = N'SELECT * FROM dbo.TestIndex WHERE Name = @name'; SET @params = N'@name NVARCHAR(50)'; EXEC sp_executesql @sql, @params, @name = 'User50000'; GO7. 验证你的SQLServer技能:本地环境与一套自测用例
7.1 从零搭一个面试演练环境:虚拟机还是容器
没有实际环境的前提下备战SQLServer面试,光看面经心里是没底的。我建议你在本地开一个开发者版本实例,对,就是那个只能用于开发和测试的版本。它和标准版、企业版在功能上没有差异,足够练习全部索引、事务、性能排查操作。装好后最好开启READ_COMMITTED_SNAPSHOT和ALLOW_SNAPSHOT_ISOLATION,把生产里常用的开关先打开。
安装SQLServer开发者版没什么复杂选项,但有两个细节值得注意:实例名称如果是默认实例,连接字符串里不需要指定端口;如果是命名实例,连接时要用“机器名\实例名”的写法。身份验证模式建议选“混合模式”,既能用Windows身份验证登录,也能用sa账号测试应用层连接。如果机器上已经装了旧版SQLServer,注意实例冲突和端口占用,通常我会把旧服务停掉再装新的。
7.2 面试自测清单:十五分钟能走完的验证路径
这套自测能帮你在面试前快速检一遍自己的掌握程度。先用脚本建好测试表和基础数据,然后依次跑四件事。第一件事,检查索引覆盖和书签查找。第二件事,开启SET STATISTICS IO ON看逻辑读次数。第三件事,用DBCC SHOW_STATISTICS看统计信息抽样率,你能讲清楚密度向量是什么,就是加分项。第四件事,用扩展事件或跟踪标志抓死锁图。
这套流程跑完,你对SQLServer的理解就不再停留在“背过答案”层面,而是能说出每个机制在什么条件下触发、怎么复现、怎么验证。面试时你要主动把这些“验证路径”讲出来,比如“我遇到过统计信息过期导致执行计划跑偏,所以我平时会先看DBCC SHOW_STATISTICS的Last updated列,再决定要不要更新”。这比回答“我会更新统计信息”有说服力得多。
7.3 长期价值:把面试准备变成你的排障手册
面试刷题有个通病:为了面试而面。其实SQLServer这些高频考点,本质上就是你日常工作会踩的坑。从索引维护到锁等待,从统计信息过期到日志文件暴涨,每一道题都能对应一个线上事故。每遇到一个线上问题,如果你能记录当时的现象、排查路径、最终解决手段,并把对应的SQL脚本存到个人代码库,等到面试季,这些记录会比任何面经都有价值。
我自己有个习惯:每次解决一个SQLServer线上问题,就写一条“现象、原因、证据、解法、验证”五行的笔记。面试问到某个点的时候,直接讲这个真实案例,解释当时怎么从dm_exec_requests看到阻塞,怎么用dbcc inputbuffer确认是哪条SQL,一步步怎么找到缺失索引。这种案例即使过了半年再被问到,你依然记得住细节。准备SQLServer面试,与其背一百道题,不如认真演练一遍上面这几组脚本,把执行计划和等待类型的排查路径刻进肌肉记忆里。面完你会发现,自己不仅能回答面试题,处理线上问题的底气也硬了一截。希望这些脚本和思路能帮到你。
本文还有配套的精品资源,点击获取