高级查询:CTE 与窗口函数
3.1 公用表表达式(CTE)
公用表表达式(Common Table Expression,CTE)是用 WITH 关键字定义的临时结果集,仅在当前查询中可见。与子查询相比,CTE 具有更好的可读性,并且可以被多次引用。
3.1.1 基本 CTE 语法
-- 基本语法
WITH CTE名称 [(列名1, 列名2, ...)] AS (
-- CTE 定义查询
SELECT ...
)
-- 主查询
SELECT * FROM CTE名称;
-- 示例:计算每个部门的薪资统计
WITH DeptSalary AS (
SELECT
DepartmentID,
COUNT(*) AS HeadCount,
AVG(Salary) AS AvgSalary,
MAX(Salary) AS MaxSalary,
MIN(Salary) AS MinSalary,
SUM(Salary) AS TotalSalary
FROM Employees
WHERE IsActive = 1
GROUP BY DepartmentID
)
SELECT
d.DeptName,
ds.HeadCount,
ds.AvgSalary,
ds.MaxSalary,
ds.TotalSalary,
ds.AvgSalary / SUM(ds.AvgSalary) OVER() * 100 AS PctOfTotal -- 窗口函数结合
FROM DeptSalary ds
JOIN Departments d ON ds.DepartmentID = d.DepartmentID
ORDER BY ds.TotalSalary DESC;
3.1.2 多 CTE 链式定义
-- 多个 CTE 顺序定义,后面的可以引用前面的
WITH
-- 第一步:筛选活跃客户
ActiveCustomers AS (
SELECT CustomerID, CustomerName, CityID
FROM Customers
WHERE IsActive = 1
),
-- 第二步:计算每个活跃客户的订单统计
CustomerStats AS (
SELECT
ac.CustomerID,
ac.CustomerName,
ac.CityID,
COUNT(o.OrderID) AS OrderCount,
SUM(o.Amount) AS TotalAmount,
MAX(o.OrderDate) AS LastOrderDate
FROM ActiveCustomers ac
LEFT JOIN Orders o ON ac.CustomerID = o.CustomerID
AND o.Status <> 'Cancelled'
GROUP BY ac.CustomerID, ac.CustomerName, ac.CityID
),
-- 第三步:关联城市并分层
RankedCustomers AS (
SELECT
cs.*,
ci.CityName,
NTILE(4) OVER (ORDER BY cs.TotalAmount DESC) AS ValueTier
FROM CustomerStats cs
LEFT JOIN Cities ci ON cs.CityID = ci.CityID
)
-- 主查询:输出高价值客户
SELECT
CustomerName,
CityName,
OrderCount,
CAST(TotalAmount AS DECIMAL(12,2)) AS TotalAmount,
LastOrderDate,
CASE ValueTier
WHEN 1 THEN '钻石客户(TOP 25%)'
WHEN 2 THEN '黄金客户(25-50%)'
WHEN 3 THEN '白银客户(50-75%)'
ELSE '普通客户(后 25%)'
END AS CustomerGrade
FROM RankedCustomers
ORDER BY TotalAmount DESC;
3.1.3 递归 CTE
递归 CTE 用于处理树形/层级数据(组织架构、商品分类、账户关系等)。
-- 组织架构递归查询
-- 表结构:Employees(EmpID, EmpName, JobTitle, ManagerID, Salary)
WITH OrgTree AS (
-- 锚点成员(Anchor Member):起始节点(最高层级)
SELECT
EmpID,
EmpName,
JobTitle,
ManagerID,
Salary,
0 AS Level, -- 层级深度
CAST(EmpName AS NVARCHAR(500)) AS Path, -- 层级路径
CAST(EmpID AS VARCHAR(MAX)) AS IDPath
FROM Employees
WHERE ManagerID IS NULL -- 顶层(无上司)
UNION ALL
-- 递归成员(Recursive Member):引用自身
SELECT
e.EmpID,
e.EmpName,
e.JobTitle,
e.ManagerID,
e.Salary,
ot.Level + 1,
CAST(ot.Path + N' → ' + e.EmpName AS NVARCHAR(500)),
CAST(ot.IDPath + ',' + CAST(e.EmpID AS VARCHAR) AS VARCHAR(MAX))
FROM Employees e
INNER JOIN OrgTree ot ON e.ManagerID = ot.EmpID
)
-- OPTION (MAXRECURSION N) 控制最大递归深度,默认100,0=无限制
SELECT
REPLICATE(' ', Level) + EmpName AS IndentedName, -- 缩进显示层级
JobTitle,
Level,
Path,
Salary,
SUM(Salary) OVER (PARTITION BY Level) AS LevelTotalSalary
FROM OrgTree
ORDER BY IDPath
OPTION (MAXRECURSION 50);
-- 递归 CTE:生成日期序列(实用技巧)
DECLARE @StartDate DATE = '2024-01-01';
DECLARE @EndDate DATE = '2024-12-31';
WITH DateSeries AS (
SELECT @StartDate AS dt
UNION ALL
SELECT DATEADD(DAY, 1, dt)
FROM DateSeries
WHERE dt < @EndDate
)
SELECT
dt,
DATENAME(WEEKDAY, dt) AS WeekdayName,
DATEPART(WEEK, dt) AS WeekNumber,
IIF(DATEPART(WEEKDAY, dt) IN (1,7), '周末', '工作日') AS DayType
FROM DateSeries
OPTION (MAXRECURSION 0);
-- 递归 CTE:计算阶乘(演示算法)
WITH Factorial AS (
SELECT 1 AS n, CAST(1 AS BIGINT) AS fact
UNION ALL
SELECT n + 1, CAST(fact * (n + 1) AS BIGINT)
FROM Factorial
WHERE n < 20
)
SELECT n, fact AS [n!] FROM Factorial;
3.1.4 CTE 与 UPDATE / DELETE / INSERT
-- CTE 用于 UPDATE(提高可读性)
WITH TopProducts AS (
SELECT ProductID
FROM Products
WHERE CategoryID = 1
AND StockQty > 100
)
UPDATE p
SET p.Price = p.Price * 0.95
FROM Products p
INNER JOIN TopProducts tp ON p.ProductID = tp.ProductID;
-- CTE 用于删除重复行(经典去重技巧)
WITH Duplicates AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY CustomerID, OrderDate, Amount
ORDER BY OrderID
) AS RowNum
FROM Orders
)
DELETE FROM Duplicates WHERE RowNum > 1;
-- 执行前先 SELECT 确认:
-- SELECT * FROM Duplicates WHERE RowNum > 1;
3.2 窗口函数
窗口函数(Window Functions)是 SQL Server 分析类查询的核心。与聚合函数不同,窗口函数不会减少行数,它对每一行计算一个基于”窗口”(相关行集合)的结果。
3.2.1 窗口函数语法解析
函数名() OVER (
[PARTITION BY 分组列] -- 定义窗口的分区(可选,不加则整个结果集为一个窗口)
[ORDER BY 排序列] -- 定义窗口内的排序(部分函数必须)
[ROWS/RANGE 框架定义] -- 定义当前行的计算范围(可选)
)
-- 框架(Frame)常用写法:
-- ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW -- 从第一行到当前行
-- ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING -- 当前行前后各1行
-- ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING -- 整个分区
-- RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING -- 当前行到最后行
3.2.2 排名函数
-- 准备数据
CREATE TABLE #SalesData (
SalesPersonID INT,
SalesPersonName NVARCHAR(50),
Region NVARCHAR(20),
Quarter INT,
SalesAmount DECIMAL(12,2)
);
INSERT INTO #SalesData VALUES
(1,N'张三',N'华北',1,250000),(1,N'张三',N'华北',2,320000),
(2,N'李四',N'华南',1,180000),(2,N'李四',N'华南',2,420000),
(3,N'王五',N'华北',1,320000),(3,N'王五',N'华北',2,320000),
(4,N'赵六',N'华南',1,550000),(4,N'赵六',N'华南',2,280000);
SELECT
SalesPersonName,
Region,
Quarter,
SalesAmount,
-- 全局排名(相同值跳号)
RANK() OVER (ORDER BY SalesAmount DESC) AS GlobalRank,
-- 全局排名(相同值不跳号)
DENSE_RANK() OVER (ORDER BY SalesAmount DESC) AS GlobalDenseRank,
-- 全局唯一排名(ORDER BY 必须唯一,否则结果不确定)
ROW_NUMBER() OVER (ORDER BY SalesAmount DESC, SalesPersonID) AS GlobalRowNum,
-- 分区内排名(按Region分组)
RANK() OVER (PARTITION BY Region ORDER BY SalesAmount DESC) AS RegionRank,
DENSE_RANK() OVER (PARTITION BY Region ORDER BY SalesAmount DESC) AS RegionDenseRank,
-- 分桶:按 SalesAmount 排名分4组
NTILE(4) OVER (ORDER BY SalesAmount DESC) AS Quartile
FROM #SalesData
ORDER BY SalesAmount DESC;
ROW_NUMBER vs RANK vs DENSE_RANK 对比:
| SalesAmount | ROW_NUMBER | RANK | DENSE_RANK |
|---|---|---|---|
| 550000 | 1 | 1 | 1 |
| 420000 | 2 | 2 | 2 |
| 320000 | 3 | 3 | 3 |
| 320000 | 4 | 3 | 3 |
| 320000 | 5 | 3 | 3 |
| 280000 | 6 | 6 | 4 |
3.2.3 聚合窗口函数
SELECT
SalesPersonName,
Region,
Quarter,
SalesAmount,
-- 分区聚合(按 Region)
SUM(SalesAmount) OVER (PARTITION BY Region) AS RegionTotal,
AVG(SalesAmount) OVER (PARTITION BY Region) AS RegionAvg,
COUNT(*) OVER (PARTITION BY Region) AS RegionCount,
MIN(SalesAmount) OVER (PARTITION BY Region) AS RegionMin,
MAX(SalesAmount) OVER (PARTITION BY Region) AS RegionMax,
-- 占比计算(个人/区域总额)
CAST(SalesAmount * 100.0 / SUM(SalesAmount) OVER (PARTITION BY Region)
AS DECIMAL(5,2)) AS PctOfRegion,
-- 累计求和(Running Total),按 Region 分区,按 Quarter 排序
SUM(SalesAmount) OVER (
PARTITION BY Region, SalesPersonID
ORDER BY Quarter
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS RunningTotal,
-- 移动平均(滑动窗口:前1行+当前行+后1行,共3行均值)
AVG(SalesAmount) OVER (
PARTITION BY Region
ORDER BY Quarter
ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING
) AS MovingAvg3
FROM #SalesData
ORDER BY Region, SalesPersonID, Quarter;
3.2.4 偏移函数:LAG / LEAD / FIRST_VALUE / LAST_VALUE
SELECT
SalesPersonName,
Quarter,
SalesAmount,
-- LAG:取前 N 行的值(环比分析)
LAG(SalesAmount, 1, 0) OVER (
PARTITION BY SalesPersonID
ORDER BY Quarter
) AS PrevQuarterAmount,
-- 环比增长率
CASE
WHEN LAG(SalesAmount, 1) OVER (PARTITION BY SalesPersonID ORDER BY Quarter) = 0
THEN NULL
ELSE CAST(
(SalesAmount - LAG(SalesAmount, 1) OVER (PARTITION BY SalesPersonID ORDER BY Quarter))
* 100.0
/ LAG(SalesAmount, 1) OVER (PARTITION BY SalesPersonID ORDER BY Quarter)
AS DECIMAL(5,2))
END AS QoQ_Growth_Pct,
-- LEAD:取后 N 行的值(预测分析)
LEAD(SalesAmount, 1, NULL) OVER (
PARTITION BY SalesPersonID
ORDER BY Quarter
) AS NextQuarterAmount,
-- FIRST_VALUE:分区内第一行的值
FIRST_VALUE(SalesAmount) OVER (
PARTITION BY SalesPersonID
ORDER BY Quarter
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS Q1Amount,
-- LAST_VALUE:分区内最后一行的值
-- 注意:LAST_VALUE 默认框架是 RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
-- 必须指定完整框架才能得到真正的最后一行
LAST_VALUE(SalesAmount) OVER (
PARTITION BY SalesPersonID
ORDER BY Quarter
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS LastQuarterAmount
FROM #SalesData
ORDER BY SalesPersonID, Quarter;
3.3 数据透视(PIVOT / UNPIVOT)
3.3.1 静态 PIVOT
-- 场景:将季度销售数据从行转列
-- 原始格式:
-- SalesPersonName | Quarter | SalesAmount
-- 张三 | 1 | 250000
-- 张三 | 2 | 320000
-- 目标格式:
-- SalesPersonName | Q1 | Q2 | Q3 | Q4
SELECT SalesPersonName, [1] AS Q1, [2] AS Q2, [3] AS Q3, [4] AS Q4
FROM (
SELECT SalesPersonName, Quarter, SalesAmount
FROM #SalesData
) AS SourceData
PIVOT (
SUM(SalesAmount)
FOR Quarter IN ([1], [2], [3], [4])
) AS PivotTable;
3.3.2 动态 PIVOT(列值不确定时)
-- 当 Quarter 的值是动态的时,用动态 SQL 构建 PIVOT
DECLARE @Columns NVARCHAR(MAX) = '';
DECLARE @SQL NVARCHAR(MAX) = '';
-- 动态获取所有季度值
SELECT @Columns += ',' + QUOTENAME(Quarter)
FROM (SELECT DISTINCT Quarter FROM #SalesData) AS q
ORDER BY Quarter;
SET @Columns = STUFF(@Columns, 1, 1, ''); -- 去掉第一个逗号
SET @SQL = N'
SELECT SalesPersonName, ' + @Columns + N'
FROM (
SELECT SalesPersonName, Quarter, SalesAmount
FROM #SalesData
) AS src
PIVOT (
SUM(SalesAmount) FOR Quarter IN (' + @Columns + N')
) AS pvt;';
EXEC sp_executesql @SQL;
3.3.3 UNPIVOT:列转行
-- 准备宽表
CREATE TABLE #WideTable (
ProductName NVARCHAR(50),
Q1_Sales DECIMAL(12,2),
Q2_Sales DECIMAL(12,2),
Q3_Sales DECIMAL(12,2),
Q4_Sales DECIMAL(12,2)
);
INSERT INTO #WideTable VALUES
(N'Surface Pro', 100000, 120000, 90000, 150000),
(N'Xbox', 80000, 75000, 100000, 200000);
-- UNPIVOT:将 Q1~Q4 四列转回行
SELECT ProductName, Quarter, SalesAmount
FROM #WideTable
UNPIVOT (
SalesAmount FOR Quarter IN (Q1_Sales, Q2_Sales, Q3_Sales, Q4_Sales)
) AS UnpivotResult;
-- 用 CROSS APPLY + VALUES 实现(更灵活,不要求类型相同)
SELECT ProductName,
Quarter,
SalesAmount
FROM #WideTable
CROSS APPLY (
VALUES
('Q1', Q1_Sales),
('Q2', Q2_Sales),
('Q3', Q3_Sales),
('Q4', Q4_Sales)
) AS T(Quarter, SalesAmount);
3.4 复杂报表查询实战
3.4.1 销售排行榜(Top N per Group)
-- 每个地区销售额前3名(经典问题)
WITH RankedSales AS (
SELECT
SalesPersonName,
Region,
SUM(SalesAmount) AS TotalSales,
ROW_NUMBER() OVER (
PARTITION BY Region
ORDER BY SUM(SalesAmount) DESC
) AS RegionRank
FROM #SalesData
GROUP BY SalesPersonName, Region
)
SELECT Region, RegionRank, SalesPersonName, TotalSales
FROM RankedSales
WHERE RegionRank <= 3
ORDER BY Region, RegionRank;
3.4.2 同比/环比分析
-- 月度销售同比(Year over Year)分析
WITH MonthlySales AS (
SELECT
YEAR(OrderDate) AS SalesYear,
MONTH(OrderDate) AS SalesMonth,
SUM(Amount) AS MonthlySales
FROM Orders
GROUP BY YEAR(OrderDate), MONTH(OrderDate)
)
SELECT
SalesYear,
SalesMonth,
MonthlySales,
LAG(MonthlySales, 12) OVER (ORDER BY SalesYear, SalesMonth) AS SameMonthLastYear,
CAST(
(MonthlySales - LAG(MonthlySales, 12) OVER (ORDER BY SalesYear, SalesMonth))
* 100.0
/ NULLIF(LAG(MonthlySales, 12) OVER (ORDER BY SalesYear, SalesMonth), 0)
AS DECIMAL(6,2)
) AS YoY_Growth_Pct,
-- 累计(YTD)
SUM(MonthlySales) OVER (
PARTITION BY SalesYear
ORDER BY SalesMonth
ROWS UNBOUNDED PRECEDING
) AS YTD_Sales
FROM MonthlySales
ORDER BY SalesYear, SalesMonth;
3.4.3 漏斗分析(Funnel Analysis)
-- 电商转化漏斗:浏览→加购→结算→支付
WITH FunnelData AS (
SELECT
CAST(EventDate AS DATE) AS EventDay,
SUM(CASE WHEN EventType = 'View' THEN 1 ELSE 0 END) AS ViewCount,
SUM(CASE WHEN EventType = 'AddCart' THEN 1 ELSE 0 END) AS CartCount,
SUM(CASE WHEN EventType = 'Checkout' THEN 1 ELSE 0 END) AS CheckoutCount,
SUM(CASE WHEN EventType = 'Payment' THEN 1 ELSE 0 END) AS PaymentCount
FROM UserEvents
WHERE EventDate >= DATEADD(DAY, -30, GETDATE())
GROUP BY CAST(EventDate AS DATE)
)
SELECT
EventDay,
ViewCount,
CartCount,
CheckoutCount,
PaymentCount,
-- 各步骤转化率
CAST(CartCount * 100.0 / NULLIF(ViewCount, 0) AS DECIMAL(5,2)) AS ViewToCart_Pct,
CAST(CheckoutCount * 100.0 / NULLIF(CartCount, 0) AS DECIMAL(5,2)) AS CartToCheckout_Pct,
CAST(PaymentCount * 100.0 / NULLIF(CheckoutCount, 0) AS DECIMAL(5,2)) AS CheckoutToPay_Pct,
-- 整体转化率
CAST(PaymentCount * 100.0 / NULLIF(ViewCount, 0) AS DECIMAL(5,2)) AS OverallConversion_Pct
FROM FunnelData
ORDER BY EventDay;
3.5 小结
| 特性 | 要点 |
|---|---|
| CTE | 多级链式;递归处理层级数据;可结合 UPDATE/DELETE |
| ROW_NUMBER | 唯一行号,去重删除的利器 |
| RANK/DENSE_RANK | 相同值排名不同:RANK跳号,DENSE_RANK不跳 |
| LAG/LEAD | 环比/同比分析必备;默认偏移1行 |
| LAST_VALUE | 需显式指定 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING |
| PIVOT | 列数已知用静态;列数动态用动态SQL |
下一章:索引原理与执行计划优化
文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有