为什么需要系统准备SQL Server面试题?
在数据库岗位招聘中,SQL Server作为微软核心数据库产品,广泛应用于金融、电信、政务、制造业等关键行业系统。 相比MySQL、PostgreSQL等开源方案,SQL Server更强调企业级稳定性、安全性和与Windows生态的深度集成,因此在技术考察上具有鲜明特色:
- 🔹 高可用机制复杂:Always On AG、数据库镜像、故障转移集群(WSFC)需掌握配置原理与故障切换流程
- 🔹 事务隔离级别多样:READ COMMITTED SNAPSHOT、SNAPSHOT等特性影响并发性能,面试高频
- 🔹 性能监控工具丰富:DMV、扩展事件(XEvents)、查询存储(Query Store)是调优核心手段
- 🔹 安全策略严格:TDE透明数据加密、行级安全(RLS)、动态数据掩码等合规要求需掌握
本页面内容基于2024年最新企业招聘需求(包括BAT、三大运营商、大型银行IT部门等)整理,包含SQL Server面试题共12大类、86道高频真题, 每题均提供标准答案、扩展解析、代码示例与避坑指南,确保知识结构完整、实操性强。
基础篇:T-SQL语法与数据库对象
【高频题】请解释T-SQL中WHERE与HAVING的区别?
标准答案:WHERE在分组前过滤行,HAVING在分组后过滤组。WHERE不能使用聚合函数,HAVING可以。
-- 错误示例:WHERE不能用聚合函数
SELECT DepartmentID, AVG(Salary) AS AvgSalary
FROM Employees
WHERE AVG(Salary) > 50000; -- ❌ 报错:聚合函数不能用于WHERE
-- 正确写法
SELECT DepartmentID, AVG(Salary) AS AvgSalary
FROM Employees
GROUP BY DepartmentID
HAVING AVG(Salary) > 50000; -- ✅ 正确
扩展解析:SQL Server查询逻辑处理顺序为:FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY。理解此顺序可避免逻辑错误。
【避坑指南】VARCHAR(MAX) vs NVARCHAR(MAX) 如何选择?
面试官意图:考察字符存储原理与性能权衡意识。
- 🔹 VARCHAR(MAX):支持最大2GB存储,按单字节存储,适合ASCII或英文数据(如日志文本)
- 🔹 NVARCHAR(MAX):支持最大2GB,按双字节存储(UTF-16),支持中文/emoji,但容量减半(逻辑上)
- 🔹 注意:当字段长度≤4000时,NVARCHAR比VARCHAR多一倍存储空间;>4000时两者均走MAX存储路径
最佳实践:业务字段明确需要多语言支持时用NVARCHAR;纯日志/配置信息可用VARCHAR(MAX)节省空间。
【实战题】如何用T-SQL实现字符串拆分(Split String)?
SQL Server 2016+支持STRING_SPLIT,但需注意排序问题:
-- 方法1:STRING_SPLIT(无序)
SELECT value FROM STRING_SPLIT('apple,banana,cherry', ',');
-- 方法2:STRING_SPLIT + ROW_NUMBER(保序)
SELECT value, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS idx
FROM STRING_SPLIT('apple,banana,cherry', ',');
-- 方法3:自定义TVF(推荐生产使用)
CREATE FUNCTION dbo.SplitString(@str NVARCHAR(MAX), @sep NVARCHAR(10))
RETURNS @tbl TABLE (id INT IDENTITY, value NVARCHAR(4000))
AS
BEGIN
DECLARE @x XML = N'<root><r>' +
REPLACE(@str, @sep, N'</r><r>') + N'</r></root>';
INSERT INTO @tbl(value)
SELECT T.c.value('.', 'NVARCHAR(4000)')
FROM @x.nodes('//root/r') T(c);
RETURN;
END;
-- 调用示例
SELECT * FROM dbo.SplitString('apple,banana,cherry', ',');
【冷知识】SQL Server中NULL的特殊处理规则
- 🔹 NULL ≠ NULL:比较时返回UNKNOWN(非TRUE/FALSE),需用IS NULL/IS NOT NULL
- 🔹 聚合函数忽略NULL:COUNT(*)统计行数,COUNT(col)忽略NULL值
- 🔹 ANSI_NULLS设置影响行为:SET ANSI_NULLS OFF时,col = NULL可匹配NULL值(不推荐)
- 🔹 COALESCE vs ISNULL:COALESCE支持多参数,返回第一个非NULL;ISNULL仅2参数且返回类型由第一个参数决定
查询优化:执行计划与性能瓶颈
解析:该值是SQL Server基于统计信息估算的当前子树总成本(单位:秒),由I/O成本、CPU成本组成。注意:
- 🔹 仅用于比较不同执行计划的相对开销,非实际耗时
- 🔹 当统计信息过期时,估算值可能严重偏离真实值
- 🔹 关注高成本节点(如Table Scan、Key Lookup)而非总成本
-- 查看执行计划的两种方式
EXPLAIN PLAN FOR SELECT * FROM Orders WHERE OrderDate > '2023-01-01';
-- 或
SET SHOWPLAN_XML ON;
GO
SELECT * FROM Orders WHERE OrderDate > '2023-01-01';
GO
SET SHOWPLAN_XML OFF;
【经典案例】参数嗅探(Parameter Sniffing)问题
现象:同一存储过程在不同参数下性能差异巨大(如:参数A耗时0.1s,参数B耗时30s)。
原理:SQL Server缓存执行计划时,会根据首次传入参数生成最优计划。若后续参数数据分布差异大,则计划不适用。
-- 存储过程示例
CREATE PROCEDURE GetOrdersByDate(@StartDate DATE)
AS
BEGIN
SELECT * FROM Orders
WHERE OrderDate >= @StartDate; -- 可能触发参数嗅探
END;
-- 解决方案1:OPTION (RECOMPILE)(推荐小结果集)
SELECT * FROM Orders WHERE OrderDate >= @StartDate
OPTION (RECOMPILE);
-- 解决方案2:使用局部变量(避免参数嗅探)
DECLARE @LocalDate DATE = @StartDate;
SELECT * FROM Orders WHERE OrderDate >= @LocalDate;
验证方法:查询sys.dm_exec_query_stats查看计划缓存,对比execution_count与total_elapsed_time。
索引设计:覆盖索引、过滤索引与列存储
【对比表】聚集索引 vs 非聚集索引
| 特性 | 聚集索引(Clustered) | 非聚集索引(Nonclustered) |
|---|---|---|
| 数量限制 | 每表仅1个 | 最多999个(SQL Server 2016+) |
| 数据存储 | 索引叶节点即数据页 | 索引叶节点含行定位器(RID/Clustered Key) |
| 适用场景 | 主键、范围查询(WHERE BETWEEN)、ORDER BY | 等值查询(WHERE =)、覆盖索引 |
| 写入性能 | 高(需维护物理顺序) | 中(需更新非叶节点) |
【实战】如何设计覆盖索引避免Key Lookup?
当查询列未全部包含在索引中时,SQL Server需回表查找(Key Lookup),成本高昂。
-- 原始表结构
CREATE TABLE Employees (
EmployeeID INT IDENTITY PRIMARY KEY,
Name NVARCHAR(50),
DepartmentID INT,
Salary DECIMAL(10,2),
HireDate DATE
);
-- ❌ 性能差:需要Key Lookup
CREATE INDEX IX_Department ON Employees(DepartmentID);
SELECT Name, Salary FROM Employees WHERE DepartmentID = 5;
-- ✅ 覆盖索引:将查询列加入INCLUDE
CREATE INDEX IX_Department_Cover
ON Employees(DepartmentID)
INCLUDE (Name, Salary);
-- 验证:执行计划应显示“Index Seek”,无Key Lookup
注意:INCLUDE列不参与索引排序,可突破900字节/16列限制,但总大小≤1740字节。
【大数据场景】列存储索引(Columnstore Index)原理与限制
适用于数据仓库/OLAP场景,压缩率高、查询快,但不支持高频写入。
- 🔹 压缩原理:按列存储+字典编码+位图索引,压缩率通常5~10倍
- 🔹 行组(Rowgroup):每组1048576行,压缩后称“Closed Delta”或“Compressed”
- 🔹 限制:仅企业版支持;不支持LOB列;主键/外键不可用
-- 创建非聚集列存储索引(SQL Server 2012+)
CREATE NONCLUSTERED COLUMNSTORE INDEX IX_Orders_CS
ON Orders(OrderDate, CustomerID, Amount);
-- 更新操作后需手动压缩Delta Store
ALTER INDEX IX_Orders_CS ON Orders REBUILD;
-- SQL Server 2016+:支持更新列存储索引表
CREATE CLUSTERED COLUMNSTORE INDEX IX_Orders_CCI
ON Orders; -- 允许INSERT/UPDATE/DELETE
事务与锁:隔离级别与死锁处理
通过版本控制实现读写不阻塞,替代传统共享锁,大幅降低阻塞。
允许事务基于事务开始时的快照读取数据,解决不可重复读问题。
提升高吞吐场景性能,允许事务在日志刷盘前提交(牺牲Durability)。
【高频题】SQL Server五种隔离级别对比
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 适用场景 |
|---|---|---|---|---|
| READ UNCOMMITTED | ✓ | ✓ | ✓ | 仅用于日志分析等非关键场景 |
| READ COMMITTED(默认) | ✗ | ✓ | ✓ | 普通OLTP业务 |
| READ COMMITTED SNAPSHOT | ✗ | ✗ | ✓ | 需启用数据库选项:ALTER DATABASE SET READ_COMMITTED_SNAPSHOT ON |
| REPEATABLE READ | ✗ | ✗ | ✓ | 需保证同一事务内多次读取一致 |
| SERIALIZABLE | ✗ | ✗ | ✗ | 最高隔离级别,性能最差 |
扩展:SNAPSHOT隔离需在tempdb存储版本行,需预留足够空间。
标准答案:
- 🔹 系统视图:sys.dm_tran_locks、sys.dm_exec_requests、sys.dm_os_waiting_tasks
- 🔹 XEvents会话:创建extended event session捕获xml_deadlock_report
- 🔹 Trace Flag 1222:在错误日志中记录死锁信息(SET TRACEON(1222,-1))
-- 创建XEvents会话
CREATE EVENT SESSION [Deadlock_Monitor] ON SERVER
ADD EVENT sqlserver.xml_deadlock_report
ADD TARGET package0.event_file(SET filename=N'Deadlock_Monitor.xel')
WITH (MAX_MEMORY=4MB, EVENT_RETENTION_MODE=ALLOW_SINGLE_EVENT_LOSS);
-- 启动会话
ALTER EVENT SESSION Deadlock_Monitor ON SERVER STATE = START;
高可用架构:Always On与故障转移集群
【核心架构】Always On可用性组(AG)组成要素
- 🔹 可用性副本(Replica):主副本(可读写)、辅助副本(可读/只读/备份)
- 🔹 可用性数据库(Database):需加入AG的用户数据库
- 🔹 监听器(Listener):虚拟网络名称(VNN),客户端通过它连接
- 🔹 分布式事务协调器(MSDTC):仅在需要跨副本事务时启用
同步模式:
- 🔹 同步提交(SYNCHRONOUS_COMMIT):保证强一致性,主副本等待辅助副本硬提交
- 🔹 异步提交(ASYNC_COMMIT):高性能,主副本不等待,可能丢失数据
【对比】Always On AG vs 故障转移集群实例(FCI)
| 特性 | Always On AG | 故障转移集群实例(FCI) |
|---|---|---|
| 保护粒度 | 数据库级(可单个数据库故障转移) | 实例级(整个SQL Server实例) |
| 辅助节点用途 | 支持只读查询、备份、报表 | 仅作热备,不处理请求 |
| 跨数据中心 | ✓(需WSFC跨域支持) | ✗(必须同一子网) |
| SQL Server版本要求 | 企业版 | 标准版及以上 |
【历史回顾】数据库镜像(Database Mirroring)为何被淘汰?
SQL Server 2005~2012主流方案,但存在以下缺陷:
- 🔹 仅支持单数据库,无法批量管理
- 🔹 镜像数据库不可读,无法分担查询压力
- 🔹 不支持跨域部署(需复杂证书配置)
- 🔹 2012后不再推荐,2019已移除功能
迁移建议:使用ALTER DATABASE SET HADR启用AG,或升级到Always On AG。
【面试加分题】AG的自动故障转移条件
需同时满足以下所有条件:
- 🔹 三个或更多副本(含主副本)
- 🔹 主副本与辅助副本均配置为SYNCHRONOUS_COMMIT
- 🔹 自动故障转移模式(AUTOMATIC)
- 🔹 所有副本在WSFC中处于“在线”状态
- 🔹 主副本检测到故障(如网络中断、服务停止)
若仅两个副本,即使配置自动模式,也需手动故障转移(避免脑裂)。
性能调优:DMV、查询存储与实际案例
【必备工具】常用动态管理视图(DMV)速查表
| DMV | 用途 | 典型查询 |
|---|---|---|
| sys.dm_exec_query_stats | 缓存查询的执行统计 | SELECT TOP 10 * FROM sys.dm_exec_query_stats ORDER BY total_elapsed_time DESC |
| sys.dm_exec_requests | 当前活动请求 | SELECT * FROM sys.dm_exec_requests WHERE session_id > 50 |
| sys.dm_os_wait_stats | 等待统计(诊断瓶颈) | SELECT * FROM sys.dm_os_wait_stats WHERE wait_time_ms > 1000 ORDER BY wait_time_ms DESC |
| sys.dm_db_index_usage_stats | 索引使用情况 | SELECT * FROM sys.dm_db_index_usage_stats WHERE database_id = DB_ID() AND user_seeks = 0 |
| sys.query_store_query | 查询存储(SQL Server 2016+) | SELECT * FROM sys.query_store_query WHERE query_id IN (SELECT query_id FROM sys.query_store_query_text WHERE query_sql_text LIKE '%SELECT%') |
标准排查流程:
- 🔹 用perfmon监控Processor\_% Processor Time确认是否SQL Server导致
- 🔹 查询sys.dm_exec_requests找到高CPU会话ID(session_id)
- 🔹 用DBCC INPUTBUFFER(session_id)获取当前执行语句
- 🔹 用sys.dm_exec_sql_text(sql_handle)获取完整SQL
- 🔹 分析执行计划,检查是否缺少索引、参数嗅探、表扫描
-- 快速定位高CPU会话
SELECT TOP 10
r.session_id,
r.cpu_time,
t.text,
qp.query_plan
FROM sys.dm_exec_requests r
CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) t
CROSS APPLY sys.dm_exec_query_plan(r.plan_handle) qp
WHERE r.session_id > 50
ORDER BY r.cpu_time DESC;
【实战案例】某电商订单表性能优化过程
背景:订单表Orders(5亿行),查询订单详情平均耗时2.3秒。
问题诊断:
- 🔹 执行计划显示Clustered Index Scan(全表扫描)
- 🔹 索引碎片率42%(需重建)
- 🔹 WHERE条件字段(OrderNo)无索引
优化措施:
- 🔹 创建非聚集索引:CREATE NONCLUSTERED INDEX IX_Orders_OrderNo ON Orders(OrderNo) INCLUDE (CustomerID, TotalAmount)
- 🔹 重建碎片索引:ALTER INDEX IX_Orders_OrderNo ON Orders REBUILD
- 🔹 启用READ_COMMITTED_SNAPSHOT减少阻塞
结果:查询时间降至0.08秒,CPU使用率下降35%。
备份恢复:策略设计与灾难恢复
【标准备份策略】分层备份模型
| 备份类型 | 备份内容 | 频率 | 恢复目标 |
|---|---|---|---|
| 全备份(FULL) | 整个数据库 | 每周日 | 基础恢复点 |
| 差异备份(DIFF) | 自上次全备后变化的数据 | 每日 | 减少还原时间(避免还原多个日志) |
| 事务日志备份(LOG) | 日志记录 | 每15分钟 | 点-in-时间恢复(RPO目标) |
| 文件/文件组备份 | 指定文件组 | 按需 | 大型数据库局部恢复 |
关键参数:使用BACKUP DATABASE ... WITH COMPRESSION, COPY_ONLY(避免影响差异备份链)
标准答案:必须执行RESTORE VERIFYONLY和RESTORE HEADERONLY:
-- 验证备份集完整性
RESTORE VERIFYONLY FROM DISK = 'D:\Backup\MyDB_full.bak';
-- 查看备份头信息(确认备份时间、类型、LSN链)
RESTORE HEADERONLY FROM DISK = 'D:\Backup\MyDB_full.bak';
-- 检查LSN连续性(用于日志备份链)
RESTORE LABELONLY FROM DISK = 'D:\Backup\MyDB_log.trn';
最佳实践:每周自动执行RESTORE ... WITH RECOVERY到测试环境,验证可恢复性。
面试真题精选:10个高频问题深度解析
解析:READ COMMITTED使用共享锁防止脏读,但可能导致阻塞;READ COMMITTED SNAPSHOT使用行版本控制,读操作不加锁,写操作不阻塞读。
启用方式:ALTER DATABASE [DB] SET READ_COMMITTED_SNAPSHOT ON;
- 1. 检查执行计划(是否有表扫描、Key Lookup)
- 2. 检查统计信息是否最新(UPDATE STATISTICS)
- 3. 添加/优化索引(覆盖索引、过滤索引)
- 4. 检查参数嗅探(OPTION (RECOMPILE))
- 5. 重写SQL(避免SELECT *、减少函数嵌套)
- 6. 考虑分区表或列存储索引(大数据场景)
步骤:
- 🔹 在AG属性中设置“辅助副本”→“允许连接”为“只读”
- 🔹 配置只读路由URL:ALTER AVAILABILITY GROUP [AG] MODIFY REPLICA ON 'Node2' WITH (SECONDARY_ROLE (READ_ONLY_ROUTING_URL = 'TCP://Node2.kaoacca.cn:1433'))
- 🔹 在AG中设置只读路由列表:ALTER AVAILABILITY GROUP [AG] MODIFY REPLICA ON 'Node1' WITH (PRIMARY_ROLE (READ_ONLY_ROUTING_LIST = ('Node2','Node3')))
客户端连接:Connection String中添加Application Intent=ReadOnly
定义:基于用户上下文动态过滤数据行,实现细粒度访问控制。
-- 创建安全策略
CREATE FUNCTION dbo.fn_securitypredicate(@SalesRep AS SYSNAME)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS fn_securitypredicate_result
WHERE @SalesRep = USER_NAME() OR USER_IS_MEMBER('Sales Managers') = 1;
-- 应用策略
CREATE SECURITY POLICY SalesFilter
ADD FILTER PREDICATE dbo.fn_securitypredicate(SalesRep)
ON dbo.Sales
WITH (STATE = ON);
场景:销售系统中,普通销售员只能看到自己的订单,经理可看全部。
方法:
- 🔹 使用XEvents捕获xml_deadlock_report事件
- 🔹 查询sys.dm_os_performance_counters:SELECT cntr_value FROM sys.dm_os_performance_counters WHERE counter_name = 'Number of Deadlocks/sec'
- 🔹 启用Trace Flag 1222(写入错误日志)
缺点:性能差、占用内存高、不支持并行执行。
替代方案:
- 🔹 表变量 + WHILE循环(小数据集)
- 🔹 集合操作(ROW_NUMBER() + CTE)
- 🔹 应用层处理(如C#)
何时可用:仅在必须逐行处理且无集合替代方案时(如调用外部存储过程)。
定义:
- 🔹 逻辑读:从缓冲池读取数据页
- 🔹 物理读:从磁盘读取数据页到缓冲池
降低物理读方法:
- 🔹 增加内存(增大缓冲池)
- 🔹 优化索引(减少需读取的数据页)
- 🔹 启用READ_COMMITTED_SNAPSHOT(减少重复物理读)
- 🔹 使用查询存储强制最佳计划
推荐OFFSET-FETCH(SQL Server 2012+):
SELECT * FROM Orders
ORDER BY OrderDate DESC
OFFSET 20 ROWS FETCH NEXT 10 ROWS ONLY; -- 第3页(每页10条)
注意:必须配合ORDER BY使用,否则结果无序。
ROW_NUMBER()方案(兼容旧版):
WITH CTE AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY OrderDate DESC) AS rn
FROM Orders
)
SELECT * FROM CTE WHERE rn BETWEEN 21 AND 30;
定义:持久化存储查询计划、统计信息的元数据仓库。
核心功能:
- 🔹 强制使用历史最优计划(Plan Force)
- 🔹 比较不同时间段的查询性能
- 🔹 识别性能回归(Regression)
启用:ALTER DATABASE [DB] SET QUERY_STORE = ON;
标准方法:使用sp_rename存储过程,但需注意:
- 🔹 仅重命名元数据,不影响数据
- 🔹 依赖对象(视图、存储过程、触发器)需手动更新
- 🔹 授权不会继承(需重新分配权限)
- 🔹 不建议在OLTP系统中频繁使用(可能引发执行计划缓存失效)
EXEC sp_rename 'OldTableName', 'NewTableName';
-- 检查依赖
SELECT * FROM sys.sql_expression_dependencies
WHERE referenced_entity_name = 'OldTableName';