T-SQL 高级查询
大约 11 分钟约 3395 字
T-SQL 高级查询
简介
T-SQL(Transact-SQL)是 SQL Server 的扩展 SQL 语言。高级查询涵盖窗口函数、CTE 递归、PIVOT 转换、MERGE 合并、动态 SQL 等。掌握这些技巧能大幅提升 SQL 编写效率和查询性能。
特点
窗口函数
ROW_NUMBER / RANK / DENSE_RANK
-- 排名函数
SELECT
Name,
Department,
Salary,
-- 不同排名方式
ROW_NUMBER() OVER (PARTITION BY Department ORDER BY Salary DESC) AS RowNum,
RANK() OVER (ORDER BY Salary DESC) AS RankVal,
DENSE_RANK() OVER (ORDER BY Salary DESC) AS DenseRankVal
FROM Employees;
-- 分页(替代 OFFSET)
WITH RankedEmployees AS (
SELECT *,
ROW_NUMBER() OVER (ORDER BY HireDate DESC) AS RowNum
FROM Employees
WHERE Department = 'IT'
)
SELECT * FROM RankedEmployees
WHERE RowNum BETWEEN 11 AND 20; -- 第2页,每页10条SUM / AVG / COUNT 窗口聚合
-- 累计销售额
SELECT
OrderDate,
Amount,
SUM(Amount) OVER (ORDER BY OrderDate) AS RunningTotal,
SUM(Amount) OVER (PARTITION BY YEAR(OrderDate) ORDER BY OrderDate) AS YearRunningTotal
FROM Orders;
-- 部门工资占比
SELECT
Name,
Department,
Salary,
Salary * 100.0 / SUM(Salary) OVER (PARTITION BY Department) AS PercentOfDept
FROM Employees;
-- 移动平均(最近3天)
SELECT
OrderDate,
Amount,
AVG(Amount) OVER (ORDER BY OrderDate ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS MovingAvg3
FROM Orders;LAG / LEAD
-- 环比增长
SELECT
Month,
Revenue,
LAG(Revenue, 1) OVER (ORDER BY Month) AS PrevMonth,
Revenue - LAG(Revenue, 1) OVER (ORDER BY Month) AS Growth,
CAST((Revenue - LAG(Revenue, 1) OVER (ORDER BY Month)) * 100.0
/ NULLIF(LAG(Revenue, 1) OVER (ORDER BY Month), 0) AS DECIMAL(5,2)) AS GrowthRate
FROM MonthlyRevenue;
-- FIRST_VALUE / LAST_VALUE
SELECT
ProductId,
SaleDate,
Price,
FIRST_VALUE(Price) OVER (PARTITION BY ProductId ORDER BY SaleDate) AS FirstPrice,
LAST_VALUE(Price) OVER (PARTITION BY ProductId ORDER BY SaleDate
ROWS BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING) AS LastPrice
FROM ProductPrices;CTE 公用表表达式
基本 CTE
-- CTE 替代子查询,提高可读性
WITH DepartmentStats AS (
SELECT
Department,
COUNT(*) AS EmpCount,
AVG(Salary) AS AvgSalary,
MAX(Salary) AS MaxSalary
FROM Employees
GROUP BY Department
),
HighSalaryDepts AS (
SELECT * FROM DepartmentStats WHERE AvgSalary > 15000
)
SELECT
d.Department,
d.EmpCount,
d.AvgSalary,
e.Name AS TopEarner
FROM HighSalaryDepts d
CROSS APPLY (
SELECT TOP 1 Name FROM Employees e
WHERE e.Department = d.Department
ORDER BY e.Salary DESC
) e;递归 CTE — 层级查询
-- 组织架构树
WITH OrgChart AS (
-- 锚点:顶级领导
SELECT
Id, Name, ManagerId, Title,
0 AS Level,
CAST(Name AS NVARCHAR(1000)) AS Path
FROM Employees
WHERE ManagerId IS NULL
UNION ALL
-- 递归:逐级展开
SELECT
e.Id, e.Name, e.ManagerId, e.Title,
o.Level + 1,
CAST(o.Path + ' > ' + e.Name AS NVARCHAR(1000))
FROM Employees e
INNER JOIN OrgChart o ON e.ManagerId = o.Id
)
SELECT * FROM OrgChart ORDER BY Path;
-- BOM 物料清单展开
WITH BOM AS (
SELECT ProductId, ComponentId, Quantity, 0 AS Level
FROM ProductComponents
WHERE ProductId = 'P001'
UNION ALL
SELECT pc.ProductId, pc.ComponentId, pc.Quantity * b.Quantity, b.Level + 1
FROM ProductComponents pc
INNER JOIN BOM b ON pc.ProductId = b.ComponentId
)
SELECT * FROM BOM WHERE Level > 0;PIVOT 行转列
-- 月份作为列
SELECT *
FROM (
SELECT Department, MONTH(SaleDate) AS MonthNum, Amount
FROM Sales
WHERE YEAR(SaleDate) = 2026
) s
PIVOT (
SUM(Amount) FOR MonthNum IN ([1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12])
) p;
-- 动态 PIVOT(需要动态 SQL)
DECLARE @cols NVARCHAR(MAX);
DECLARE @query NVARCHAR(MAX);
SELECT @cols = STRING_AGG(QUOTENAME(Category), ',')
FROM (SELECT DISTINCT Category FROM Products) t;
SET @query = N'
SELECT *
FROM (
SELECT Category, Month, Sales
FROM MonthlySales
) s
PIVOT (SUM(Sales) FOR Category IN (' + @cols + ')) p';
EXEC sp_executesql @query;MERGE 数据同步
-- MERGE — 同步目标表和源表
MERGE INTO Products AS target
USING (
SELECT Id, Name, Price, Category FROM StagingProducts
) AS source
ON target.Id = source.Id
-- 匹配时更新
WHEN MATCHED AND (
target.Name <> source.Name OR
target.Price <> source.Price OR
target.Category <> source.Category
) THEN
UPDATE SET
target.Name = source.Name,
target.Price = source.Price,
target.Category = source.Category,
target.UpdatedAt = GETUTCDATE()
-- 不匹配时插入
WHEN NOT MATCHED THEN
INSERT (Id, Name, Price, Category)
VALUES (source.Id, source.Name, source.Price, source.Category)
-- 目标有但源没有时删除
WHEN NOT MATCHED BY SOURCE THEN
DELETE;临时表与表变量
-- 临时表(# 开头)
CREATE TABLE #TempOrders (
OrderId INT,
TotalAmount DECIMAL(18,2),
OrderDate DATETIME
);
INSERT INTO #TempOrders
SELECT Id, TotalAmount, OrderDate FROM Orders WHERE Status = 'Completed';
SELECT * FROM #TempOrders WHERE TotalAmount > 1000;
DROP TABLE #TempOrders;
-- 全局临时表(## 开头,跨会话)
CREATE TABLE ##GlobalStats (StatName NVARCHAR(100), StatValue INT);
-- 表变量
DECLARE @UserOrders TABLE (
UserId INT,
OrderCount INT,
TotalAmount DECIMAL(18,2)
);
INSERT INTO @UserOrders
SELECT UserId, COUNT(*), SUM(TotalAmount)
FROM Orders
GROUP BY UserId;
SELECT * FROM @UserOrders WHERE OrderCount > 5;性能优化技巧
-- 1. 使用 EXISTS 替代 IN
-- 差
SELECT * FROM Orders WHERE UserId IN (SELECT Id FROM VIPUsers);
-- 好
SELECT * FROM Orders WHERE EXISTS (SELECT 1 FROM VIPUsers WHERE VIPUsers.Id = Orders.UserId);
-- 2. 避免在 WHERE 中使用函数
-- 差
SELECT * FROM Orders WHERE YEAR(CreatedAt) = 2026;
-- 好
SELECT * FROM Orders WHERE CreatedAt >= '2026-01-01' AND CreatedAt < '2027-01-01';
-- 3. 使用 SET NOCOUNT ON
SET NOCOUNT ON; -- 减少 DONE_IN_PROC 消息
-- 4. 批量操作
-- 差:逐行操作
-- 好:批量 INSERT / UPDATE
-- 5. 查看执行计划
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
-- 查看逻辑读和 CPU 时间窗口函数深入
高级窗口函数应用
-- 1. 连续区间问题(Gaps and Islands)
-- 场景:找出每个用户的连续登录天数
WITH login_groups AS (
SELECT
user_id,
login_date,
DATEADD(DAY, -ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY login_date), login_date) AS grp
FROM user_logins
),
continuous_days AS (
SELECT
user_id,
grp,
MIN(login_date) AS start_date,
MAX(login_date) AS end_date,
COUNT(*) AS consecutive_days
FROM login_groups
GROUP BY user_id, grp
)
SELECT * FROM continuous_days WHERE consecutive_days >= 3;
-- 2. 删除重复数据(保留最新一条)
WITH ranked AS (
SELECT
id,
user_id,
email,
updated_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY updated_at DESC) AS rn
FROM users
)
DELETE FROM ranked WHERE rn > 1;
-- 3. 百分位计算
SELECT
department,
employee_name,
salary,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary) OVER (PARTITION BY department) AS median_salary,
PERCENT_RANK() OVER (PARTITION BY department ORDER BY salary) AS pct_rank
FROM employees;
-- 4. 前后行对比
SELECT
product_id,
sales_date,
sales_amount,
LAG(sales_amount, 1) OVER (PARTITION BY product_id ORDER BY sales_date) AS prev_day_sales,
sales_amount - LAG(sales_amount, 1) OVER (PARTITION BY product_id ORDER BY sales_date) AS day_over_day,
LEAD(sales_amount, 7) OVER (PARTITION BY product_id ORDER BY sales_date) AS next_week_sales
FROM daily_sales;
-- 5. NTile 分桶
-- 将用户按消费金额分为 4 个等级
SELECT
user_id,
total_amount,
NTILE(4) OVER (ORDER BY total_amount DESC) AS quartile
FROM user_consumption;
-- quartile = 1 为最高消费群体
-- quartile = 4 为最低消费群体存储过程与触发器
存储过程
-- 分页查询存储过程
CREATE PROCEDURE usp_GetOrdersPaged
@UserId INT = NULL,
@Status NVARCHAR(20) = NULL,
@PageIndex INT = 1,
@PageSize INT = 20,
@TotalCount INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
-- 计算总数
SELECT @TotalCount = COUNT(*)
FROM Orders
WHERE (@UserId IS NULL OR UserId = @UserId)
AND (@Status IS NULL OR Status = @Status);
-- 分页查询
SELECT
Id, OrderNo, UserId, TotalAmount, Status, CreatedAt
FROM Orders
WHERE (@UserId IS NULL OR UserId = @UserId)
AND (@Status IS NULL OR Status = @Status)
ORDER BY CreatedAt DESC
OFFSET (@PageIndex - 1) * @PageSize ROWS
FETCH NEXT @PageSize ROWS ONLY;
END;
GO
-- 调用
DECLARE @total INT;
EXEC usp_GetOrdersPaged @UserId = 1001, @PageIndex = 2, @PageSize = 20, @TotalCount = @total OUTPUT;
SELECT @total AS TotalCount;
-- 带错误处理的存储过程
CREATE PROCEDURE usp_TransferFunds
@FromAccountId INT,
@ToAccountId INT,
@Amount DECIMAL(18,2)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON; -- 遇到错误自动回滚
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN TRY
BEGIN TRANSACTION;
-- 检查余额
DECLARE @balance DECIMAL(18,2);
SELECT @balance = Balance FROM Accounts WHERE Id = @FromAccountId;
IF @balance < @Amount
BEGIN
RAISERROR('余额不足', 16, 1);
RETURN -1;
END
-- 扣款
UPDATE Accounts SET Balance = Balance - @Amount WHERE Id = @FromAccountId;
-- 入账
UPDATE Accounts SET Balance = Balance + @Amount WHERE Id = @ToAccountId;
-- 记录转账流水
INSERT INTO Transfers (FromAccount, ToAccount, Amount, CreatedAt)
VALUES (@FromAccountId, @ToAccountId, @Amount, GETUTCDATE());
COMMIT TRANSACTION;
RETURN 0;
END TRY
BEGIN CATCH
IF @@TRANCOUNT > 0
ROLLBACK TRANSACTION;
-- 记录错误日志
INSERT INTO ErrorLog (ErrorNumber, ErrorSeverity, ErrorState, ErrorProcedure, ErrorLine, ErrorMessage, ErrorTime)
VALUES (ERROR_NUMBER(), ERROR_SEVERITY(), ERROR_STATE(), ERROR_PROCEDURE(), ERROR_LINE(), ERROR_MESSAGE(), GETUTCDATE());
RETURN -1;
END CATCH;
END;
GO触发器
-- 审计触发器
CREATE TABLE AuditLog (
AuditId BIGINT IDENTITY(1,1) PRIMARY KEY,
TableName NVARCHAR(100) NOT NULL,
Operation NVARCHAR(10) NOT NULL,
PrimaryKeyId NVARCHAR(50),
OldData NVARCHAR(MAX),
NewData NVARCHAR(MAX),
ChangedBy NVARCHAR(100),
ChangedAt DATETIME2 DEFAULT SYSUTCDATETIME()
);
CREATE TRIGGER trg_Orders_Audit
ON Orders
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
SET NOCOUNT ON;
-- INSERT 操作
INSERT INTO AuditLog (TableName, Operation, PrimaryKeyId, NewData, ChangedBy)
SELECT 'Orders', 'INSERT', CAST(Id AS NVARCHAR),
(SELECT * FROM inserted i WHERE i.Id = ins.Id FOR JSON PATH),
SYSTEM_USER
FROM inserted ins;
-- DELETE 操作
INSERT INTO AuditLog (TableName, Operation, PrimaryKeyId, OldData, ChangedBy)
SELECT 'Orders', 'DELETE', CAST(Id AS NVARCHAR),
(SELECT * FROM deleted d WHERE d.Id = del.Id FOR JSON PATH),
SYSTEM_USER
FROM deleted del;
-- UPDATE 操作
INSERT INTO AuditLog (TableName, Operation, PrimaryKeyId, OldData, NewData, ChangedBy)
SELECT 'Orders', 'UPDATE', CAST(i.Id AS NVARCHAR),
(SELECT * FROM deleted d WHERE d.Id = i.Id FOR JSON PATH),
(SELECT * FROM inserted i WHERE i.Id = i.Id FOR JSON PATH),
SYSTEM_USER
FROM inserted i;
END;
GO
-- 注意事项:
-- 1. 触发器是隐式执行的,难以调试
-- 2. 触发器中的操作会增加事务时间
-- 3. 批量操作时触发器会逐行触发(SQL Server 2005+ 支持批量)
-- 4. 生产环境建议用 CDC(Change Data Capture)替代触发器TRY_CAST 与错误处理
安全类型转换
-- TRY_CAST / TRY_CONVERT 失败返回 NULL 而不报错
SELECT TRY_CAST('123' AS INT); -- 123
SELECT TRY_CAST('abc' AS INT); -- NULL(不报错)
SELECT TRY_CONVERT(DATE, '2026-04-14'); -- 2026-04-14
SELECT TRY_CONVERT(DATE, 'invalid'); -- NULL
-- 实际应用:清洗脏数据
SELECT
id,
phone,
CASE
WHEN TRY_CAST(phone AS BIGINT) IS NOT NULL THEN 'valid'
ELSE 'invalid'
END AS phone_status
FROM users;
-- TRY_PARSE(依赖文化设置)
SELECT TRY_PARSE('1,234.56' AS DECIMAL(10,2)); -- 1234.56(英文文化)
SELECT TRY_PARSE('1.234,56' AS DECIMAL(10,2) USING 'de-DE'); -- 1234.56(德文文化)
-- IIF 简化 CASE 表达式
SELECT
id,
IIF(status = 1, 'active', 'inactive') AS status_text,
IIF(TotalAmount >= 1000, 'VIP', '普通') AS customer_level
FROM orders;
-- CHOOSE 函数
SELECT
id,
CHOOSE(status, '待支付', '已支付', '已发货', '已完成', '已取消') AS status_text
FROM orders;
-- CONCAT_NULL_YIELDS_NULL 设置
SET CONCAT_NULL_YIELDS_NULL OFF;
SELECT CONCAT('Hello', NULL, 'World'); -- HelloWorld
SET CONCAT_NULL_YIELDS_NULL ON;
SELECT CONCAT('Hello', NULL, 'World'); -- HelloNULLWorld临时表 vs 表变量 vs CTE
选择策略
-- 临时表(#table)
-- 适合:大量数据(> 1000 行)、需要索引、需要统计信息
CREATE TABLE #TempOrders (
Id INT PRIMARY KEY,
UserId INT,
Amount DECIMAL(18,2),
CreatedAt DATETIME
);
CREATE INDEX idx_temp_user ON #TempOrders(UserId);
-- 表变量(@table)
-- 适合:少量数据(< 1000 行)、短生命周期、存储过程内部
DECLARE @UserStats TABLE (
UserId INT PRIMARY KEY,
OrderCount INT,
TotalAmount DECIMAL(18,2)
);
-- 表变量没有统计信息,不适合大查询
-- CTE(Common Table Expression)
-- 适合:一次性使用、提高可读性、递归查询
;WITH OrderStats AS (
SELECT UserId, COUNT(*) AS OrderCount, SUM(Amount) AS TotalAmount
FROM Orders
GROUP BY UserId
)
SELECT * FROM OrderStats WHERE OrderCount > 10;
-- 表值函数
CREATE FUNCTION dbo.fn_GetUserOrders(@UserId INT)
RETURNS @Result TABLE (
OrderId INT,
OrderNo NVARCHAR(32),
Amount DECIMAL(18,2),
OrderDate DATETIME
)
AS
BEGIN
INSERT INTO @Result
SELECT Id, OrderNo, TotalAmount, CreatedAt
FROM Orders WHERE UserId = @UserId;
RETURN;
END;
GO
-- 选择指南:
-- | 场景 | 推荐 | 原因 |
-- |-------------------|-------------|-------------------------|
-- | < 1000 行 | 表变量 | 无日志开销,性能好 |
-- | > 1000 行 | 临时表 | 有统计信息,优化器可选择计划 |
-- | 需要索引 | 临时表 | 表变量不支持创建索引 |
-- | 递归 | CTE | 语法简洁 |
-- | 可复用 | 表值函数 | 可参数化,可复用 |
-- | 跨批处理使用 | 临时表 | 表变量在批处理结束时销毁 |JSON 操作(SQL Server 2016+)
JSON 查询与修改
-- JSON 数据存储
CREATE TABLE products (
id INT PRIMARY KEY,
name NVARCHAR(200),
attributes NVARCHAR(MAX) -- 存储 JSON 字符串
);
INSERT INTO products VALUES
(1, 'Laptop', '{"brand":"Dell","cpu":"i7","ram":"16GB","storage":"512GB SSD"}'),
(2, 'Phone', '{"brand":"Apple","model":"iPhone 15","color":"black"}');
-- JSON_VALUE 提取标量值
SELECT
id,
name,
JSON_VALUE(attributes, '$.brand') AS brand,
JSON_VALUE(attributes, '$.cpu') AS cpu
FROM products;
-- JSON_QUERY 提取对象/数组
SELECT
id,
JSON_QUERY(attributes, '$.storage') AS storage_info
FROM products;
-- OPENJSON 将 JSON 转为表
SELECT *
FROM OPENJSON((SELECT attributes FROM products WHERE id = 1))
WITH (
brand NVARCHAR(50) '$.brand',
cpu NVARCHAR(50) '$.cpu',
ram NVARCHAR(50) '$.ram'
);
-- JSON 修改(SQL Server 2017+)
UPDATE products
SET attributes = JSON_MODIFY(attributes, '$.ram', '32GB')
WHERE id = 1;
-- JSON_MODIFY 添加新属性
UPDATE products
SET attributes = JSON_MODIFY(
JSON_MODIFY(attributes, '$.price', 6999),
'$.discount', 0.95
)
WHERE id = 1;
-- JSON 索引(计算列 + 索引)
ALTER TABLE products
ADD brand AS JSON_VALUE(attributes, '$.brand');
CREATE INDEX idx_products_brand ON products(brand);
SELECT * FROM products WHERE brand = 'Dell';优点
缺点
总结
T-SQL 高级查询是后端开发者的核心竞争力。窗口函数处理排名和累计,CTE 递归处理层级数据,PIVOT 处理行列转换,MERGE 处理数据同步。核心原则:优先用集合操作替代游标,用 CTE 提高可读性,用窗口函数替代自连接。
关键知识点
- 数据库主题一定要同时看数据模型、读写模式和执行代价。
- 很多性能问题不是 SQL 语法问题,而是索引、统计信息、事务和数据分布问题。
- 高可用、备份、迁移和治理与查询优化同样重要。
- 先把数据模型、访问模式和执行代价绑定起来理解。
项目落地视角
- 所有优化前后都保留执行计划、样本 SQL 和关键指标对比。
- 上线前准备回滚脚本、备份点和校验方案。
- 把连接池、锁等待、慢查询和容量增长纳入日常巡检。
- 保留执行计划、样本 SQL、索引定义和优化前后指标。
常见误区
- 脱离真实数据分布讨论索引或分片。
- 只看单条 SQL,不看整条业务链路的事务和锁。
- 把测试环境结论直接等同于生产环境结论。
- 脱离真实数据分布设计索引。
进阶路线
- 继续向执行计划、存储引擎、复制机制和数据治理层深入。
- 把主题与 ORM、缓存、消息队列和归档策略联动起来思考。
- 沉淀成数据库设计规范、SQL 审核规则和变更流程。
- 继续深入存储引擎、复制机制、归档与冷热分层治理。
适用场景
- 当你准备把《T-SQL 高级查询》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合数据建模、查询优化、事务控制、高可用和迁移治理。
- 当系统开始遇到慢查询、锁冲突、热点数据或容量增长时,这类主题价值最高。
落地建议
- 先分析真实查询模式、数据量级和写入特征,再决定索引或分片策略。
- 所有优化结论都结合执行计划、样本数据和监控指标验证。
- 高风险操作前准备备份、回滚脚本与校验 SQL。
排错清单
- 先确认瓶颈在 CPU、I/O、锁等待、网络还是 SQL 本身。
- 检查执行计划是否走错索引、是否发生排序或全表扫描。
- 排查长事务、隐式类型转换、统计信息过期和参数嗅探。
复盘问题
- 如果把《T-SQL 高级查询》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《T-SQL 高级查询》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《T-SQL 高级查询》最大的收益和代价分别是什么?
