一、文章大纲
本文围绕 SQL Server 2019 在珠宝行业中的查询设计模式与高级开发实战展开,内容涵盖从基础数据准备、常用查询优化到多租户架构、增量 ETL 与批处理执行模式等进阶主题。以下是全文结构大纲:
1. 基础数据准备
- 珠宝产品分类表(含层级关系)
- 珠宝产品表、客户表、订单表、订单明细表、销售流水表
- 示例数据插入与推荐索引创建
2. 高效 JOIN 连接查询
- 反模式:SELECT * 与未提前过滤的多表 JOIN
- 优化方案:CTE 提前过滤 + 明确字段选择
- 高级技巧:CROSS APPLY / OUTER APPLY
3. 高效分页查询
- 反模式:OFFSET-FETCH 大偏移量性能问题
- 优化方案:Keyset(游标)分页
4. 搜索优化查询
- 反模式:LIKE '%keyword%' 全表扫描
- 优化方案:前缀匹配 LIKE 'keyword%' 利用索引
- 优化方案:多条件动态搜索(参数化动态 SQL 防注入)
5. 窗口函数
- 排名与 Top-N 分析
- 累计销售额与排名
- 移动平均与 LAG/LEAD 分析
- NTILE 价格分档
6. 递归 CTE
- 构建珠宝产品完整分类路径
- 查找所有子分类(含间接子级)
7. PIVOT / UNPIVOT 透视转换
- PIVOT:按金属类型透视销售汇总
- 条件聚合替代 PIVOT(更灵活)
- UNPIVOT:宽表转长表
8. GROUPING SETS 多级聚合
- 按金属类型与会员等级的多维度销售汇总
9. 索引优化与执行计划分析
- 核心算子识别与优化实战(Key Lookup、Sort 消除)
- 缺失索引分析与索引碎片检查维护
- 查询存储(Query Store)分析与执行计划对比
10. 高级实战:多租户架构与增量 ETL
- RLS 多租户 + 分区表解决数据倾斜
- SCD Type 2 增量 ETL(MERGE + OUTPUT)
- 批处理执行模式(Batch Mode)与自适应联接
二、正文内容
1. 基础数据准备
本文首先创建珠宝行业所需的六张核心表:珠宝产品分类表(含层级关系)、珠宝产品表、客户表、订单表、订单明细表和销售流水表。分类表通过 ParentCategoryID 自引用实现层级结构,产品表记录金属类型、宝石类型、克拉重量、单价和库存等关键属性。随后插入示例数据并创建推荐索引,为后续查询优化演示奠定基础。
2. 高效 JOIN 连接查询
本节先展示反模式:使用 SELECT * 且未提前过滤的多表 JOIN,会导致大量无效数据传输。优化方案采用 CTE 提前过滤,将已完成订单、钻石会员、高价值产品分别提取后再 JOIN,显著减少中间结果集。高级技巧部分演示 CROSS APPLY / OUTER APPLY 的用法,用于查询每件产品最近一笔销售记录以及每位客户的订单聚合统计。
3. 高效分页查询
反模式 OFFSET-FETCH 在大偏移量场景下性能急剧下降,因为数据库需要扫描并丢弃前 N 行。优化方案采用 Keyset(游标)分页,通过记录上一页最后一条记录的排序键值,使用 WHERE 条件直接定位下一页起点,性能稳定且可充分利用索引。
4. 搜索优化查询
LIKE '%keyword%' 无法利用索引导致全表扫描,而前缀匹配 LIKE 'keyword%' 可走索引。对于多条件动态搜索,使用参数化动态 SQL(sp_executesql)既保证灵活性又防止 SQL 注入,同时避免不必要的过滤条件拼接。
5. 窗口函数
窗口函数部分涵盖四大场景:ROW_NUMBER 实现每种宝石价格最高的前 N 件产品;SUM OVER 计算销售人员累计销售额;AVG OVER 与 LAG/LEAD 实现移动平均和同比分析;NTILE 将产品按价格分为四档,便于价格分层管理。
6. 递归 CTE
递归 CTE 用于处理珠宝分类的层级关系。第一个示例构建完整分类路径(如"珠宝首饰 > 戒指 > 钻石戒指"),第二个示例从指定根分类出发查找所有子分类及其产品,注意使用 OPTION (MAXRECURSION) 限制递归深度。
7. PIVOT / UNPIVOT 透视转换
PIVOT 将行数据转为列,示例按金属类型(黄金、铂金、银)透视客户销售额。条件聚合(CASE WHEN + SUM)是更灵活的替代方案,可同时输出多列汇总。UNPIVOT 则反向操作,将宽表(月度列)转为长表(月份行),便于后续分析。
8. GROUPING SETS 多级聚合
GROUPING SETS 在一次查询中生成多个维度的汇总结果:按金属类型+会员等级、仅金属类型、仅会员等级以及总计四个层级,配合 GROUPING_ID 函数区分聚合级别,避免多次 UNION ALL 的性能开销。
9. 索引优化与执行计划分析
本节深入执行计划优化:场景1 通过创建覆盖索引消除 Key Lookup 回表;场景2 利用索引预排序消除 Sort 算子;场景3 演示隐式类型转换导致索引失效的陷阱。同时提供缺失索引 DMV 查询、索引碎片检查维护脚本,以及 Query Store 分析最耗时查询的方法。
10. 高级实战:多租户架构与增量 ETL
高级实战部分构建完整的企业级方案:模块一通过 RLS(行级安全)+ 分区表解决多租户数据倾斜问题,大租户与小租户数据分布在不同文件组;模块二使用 MERGE + OUTPUT 实现 SCD Type 2 缓慢变化维度的增量 ETL,完整记录客户历史轨迹;模块三通过列存储索引触发批处理执行模式(Batch Mode),并演示自适应联接的优化效果。
sql
-- ============================================================
-- SQL Server 2019 珠宝行业查询设计模式 - 完整可运行脚本
-- 包含:建表、插数据、高效JOIN、分页、搜索、窗口函数、递归CTE、透视转换
-- ============================================================
-- ==================== 第一部分:基础数据准备 ====================
-- 1. 创建珠宝产品分类表(含层级关系)
IF OBJECT_ID('JewelryCategories', 'U') IS NOT NULL DROP TABLE JewelryCategories;
CREATE TABLE JewelryCategories (
CategoryID INT PRIMARY KEY IDENTITY(1,1),
CategoryName NVARCHAR(100) NOT NULL,
ParentCategoryID INT NULL,
FOREIGN KEY (ParentCategoryID) REFERENCES JewelryCategories(CategoryID)
);
-- 2. 创建珠宝产品表
IF OBJECT_ID('JewelryProducts', 'U') IS NOT NULL DROP TABLE JewelryProducts;
CREATE TABLE JewelryProducts (
ProductID INT PRIMARY KEY IDENTITY(1,1),
ProductName NVARCHAR(200) NOT NULL,
CategoryID INT NOT NULL,
MetalType NVARCHAR(50), -- 金属类型:黄金/铂金/银
Gemstone NVARCHAR(100), -- 宝石类型:钻石/红宝石/蓝宝石
CaratWeight DECIMAL(10,2), -- 克拉重量
UnitPrice DECIMAL(12,2), -- 单价
StockQuantity INT DEFAULT 0, -- 库存数量
FOREIGN KEY (CategoryID) REFERENCES JewelryCategories(CategoryID)
);
-- 3. 创建客户表
IF OBJECT_ID('JewelryCustomers', 'U') IS NOT NULL DROP TABLE JewelryCustomers;
CREATE TABLE JewelryCustomers (
CustomerID INT PRIMARY KEY IDENTITY(1,1),
CustomerName NVARCHAR(100) NOT NULL,
MembershipLevel NVARCHAR(20), -- 会员等级:普通/银卡/金卡/钻石
RegistrationDate DATE
);
-- 4. 创建订单表
IF OBJECT_ID('JewelryOrders', 'U') IS NOT NULL DROP TABLE JewelryOrders;
CREATE TABLE JewelryOrders (
OrderID INT PRIMARY KEY IDENTITY(1,1),
CustomerID INT NOT NULL,
OrderDate DATETIME NOT NULL,
OrderStatus NVARCHAR(20), -- 订单状态:待支付/已支付/已发货/已完成/已取消
TotalAmount DECIMAL(12,2),
FOREIGN KEY (CustomerID) REFERENCES JewelryCustomers(CustomerID)
);
-- 5. 创建订单明细表
IF OBJECT_ID('JewelryOrderDetails', 'U') IS NOT NULL DROP TABLE JewelryOrderDetails;
CREATE TABLE JewelryOrderDetails (
DetailID INT PRIMARY KEY IDENTITY(1,1),
OrderID INT NOT NULL,
ProductID INT NOT NULL,
Quantity INT NOT NULL,
UnitPrice DECIMAL(12,2) NOT NULL,
LineTotal DECIMAL(12,2),
FOREIGN KEY (OrderID) REFERENCES JewelryOrders(OrderID),
FOREIGN KEY (ProductID) REFERENCES JewelryProducts(ProductID)
);
-- 6. 创建销售流水表(用于窗口函数演示)
IF OBJECT_ID('JewelrySalesLog', 'U') IS NOT NULL DROP TABLE JewelrySalesLog;
CREATE TABLE JewelrySalesLog (
LogID INT PRIMARY KEY IDENTITY(1,1),
ProductID INT NOT NULL,
SaleDate DATE NOT NULL,
SaleAmount DECIMAL(12,2),
SalespersonID INT
);
-- ==================== 插入示例数据 ====================
INSERT INTO JewelryCategories VALUES
('珠宝首饰', NULL),
('戒指', 1), ('项链', 1), ('手镯', 1), ('耳环', 1),
('黄金戒指', 2), ('钻石戒指', 2), ('铂金项链', 3);
INSERT INTO JewelryProducts VALUES
('经典黄金戒指', 6, '黄金', NULL, 0, 3500.00, 50),
('一克拉钻戒', 7, '铂金', '钻石', 1.00, 85000.00, 10),
('蓝宝石吊坠项链', 8, '铂金', '蓝宝石', 2.50, 42000.00, 8),
('纯银手镯', 4, '银', NULL, 0, 1200.00, 200),
('红宝石耳环', 5, '黄金', '红宝石', 0.80, 28000.00, 15);
INSERT INTO JewelryCustomers VALUES
('张三', '钻石', '2020-01-15'),
('李四', '金卡', '2021-03-20'),
('王五', '银卡', '2022-06-10'),
('赵六', '普通', '2023-09-01');
INSERT INTO JewelryOrders VALUES
(1, '2024-01-10', '已完成', 85000.00),
(1, '2024-02-14', '已完成', 42000.00),
(2, '2024-03-08', '已支付', 3500.00),
(3, '2024-01-20', '已完成', 28000.00),
(4, '2024-04-01', '待支付', 1200.00);
INSERT INTO JewelryOrderDetails VALUES
(1, 2, 1, 85000.00, 85000.00),
(2, 3, 1, 42000.00, 42000.00),
(3, 1, 1, 3500.00, 3500.00),
(4, 5, 1, 28000.00, 28000.00),
(5, 4, 1, 1200.00, 1200.00);
INSERT INTO JewelrySalesLog VALUES
(2, '2024-01-10', 85000.00, 101),
(3, '2024-01-15', 42000.00, 102),
(2, '2024-02-14', 85000.00, 101),
(1, '2024-03-08', 3500.00, 103),
(5, '2024-01-20', 28000.00, 101),
(3, '2024-04-01', 42000.00, 102),
(1, '2024-05-10', 3500.00, 103);
-- ==================== 创建推荐索引 ====================
CREATE NONCLUSTERED INDEX IX_JewelryProducts_Price_ID
ON JewelryProducts (UnitPrice DESC, ProductID DESC);
CREATE NONCLUSTERED INDEX IX_JewelryProducts_Search
ON JewelryProducts (MetalType, UnitPrice)
INCLUDE (ProductName, Gemstone, CaratWeight);
CREATE NONCLUSTERED INDEX IX_JewelryOrders_Status
ON JewelryOrders (OrderStatus)
INCLUDE (CustomerID, OrderDate, TotalAmount);
CREATE NONCLUSTERED INDEX IX_JewelrySalesLog_ProductDate
ON JewelrySalesLog (ProductID, SaleDate)
INCLUDE (SaleAmount, SalespersonID);
-- ==================== 第二部分:高效 JOIN 连接查询 ====================
-- 2.1 反模式:SELECT * 与未提前过滤的多表 JOIN
PRINT '===== 反模式:SELECT * 多表JOIN =====';
SELECT *
FROM JewelryOrders o
JOIN JewelryCustomers c ON o.CustomerID = c.CustomerID
JOIN JewelryOrderDetails od ON o.OrderID = od.OrderID
JOIN JewelryProducts p ON od.ProductID = p.ProductID
WHERE o.OrderStatus = '已完成'
AND c.MembershipLevel = '钻石'
AND p.UnitPrice > 10000;
-- 2.2 优化方案:CTE 提前过滤 + 明确字段选择
PRINT '===== 优化方案:CTE提前过滤 =====';
WITH CompletedOrders AS (
SELECT OrderID, CustomerID, OrderDate, TotalAmount
FROM JewelryOrders
WHERE OrderStatus = '已完成'
),
DiamondMembers AS (
SELECT CustomerID, CustomerName, MembershipLevel
FROM JewelryCustomers
WHERE MembershipLevel = '钻石'
),
HighValueProducts AS (
SELECT ProductID, ProductName, MetalType, Gemstone, UnitPrice
FROM JewelryProducts
WHERE UnitPrice > 10000
)
SELECT
co.OrderID,
co.OrderDate,
dm.CustomerName,
hp.ProductName,
hp.MetalType,
hp.Gemstone,
hp.UnitPrice,
co.TotalAmount
FROM CompletedOrders co
INNER JOIN DiamondMembers dm ON co.CustomerID = dm.CustomerID
INNER JOIN JewelryOrderDetails od ON co.OrderID = od.OrderID
INNER JOIN HighValueProducts hp ON od.ProductID = hp.ProductID
ORDER BY co.OrderDate DESC;
-- 2.3 高级技巧:CROSS APPLY / OUTER APPLY
PRINT '===== CROSS APPLY / OUTER APPLY 示例 =====';
-- 查询每件珠宝产品及其最近一笔销售记录
SELECT
p.ProductID,
p.ProductName,
p.UnitPrice,
recent.LastSaleDate,
recent.LastSaleAmount
FROM JewelryProducts p
OUTER APPLY (
SELECT TOP 1 SaleDate AS LastSaleDate, SaleAmount AS LastSaleAmount
FROM JewelrySalesLog sl
WHERE sl.ProductID = p.ProductID
ORDER BY SaleDate DESC
) recent;
-- 查询每位客户的订单总金额(使用 CROSS APPLY 动态聚合)
SELECT
c.CustomerName,
c.MembershipLevel,
agg.TotalSpent,
agg.OrderCount
FROM JewelryCustomers c
CROSS APPLY (
SELECT
SUM(TotalAmount) AS TotalSpent,
COUNT(*) AS OrderCount
FROM JewelryOrders o
WHERE o.CustomerID = c.CustomerID
AND o.OrderStatus = '已完成'
) agg;
-- ==================== 第三部分:高效分页查询 ====================
-- 3.1 反模式:OFFSET-FETCH 大偏移量性能问题
PRINT '===== 反模式:OFFSET-FETCH 大偏移量 =====';
SELECT ProductID, ProductName, UnitPrice
FROM JewelryProducts
ORDER BY UnitPrice DESC
OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY; -- 小偏移量演示
-- 3.2 优化方案:Keyset(游标)分页
PRINT '===== 优化方案:Keyset 分页 =====';
-- 第一页
SELECT TOP 3 ProductID, ProductName, UnitPrice
FROM JewelryProducts
ORDER BY UnitPrice DESC, ProductID DESC;
-- 后续页:假设上一页最后一条是 UnitPrice=3500.00, ProductID=1
-- 实际使用时由应用程序传入上一页的最后记录值
DECLARE @LastPrice DECIMAL(12,2) = 3500.00;
DECLARE @LastID INT = 1;
SELECT TOP 3 ProductID, ProductName, UnitPrice
FROM JewelryProducts
WHERE (UnitPrice < @LastPrice)
OR (UnitPrice = @LastPrice AND ProductID < @LastID)
ORDER BY UnitPrice DESC, ProductID DESC;
-- ==================== 第四部分:搜索优化查询 ====================
-- 4.1 反模式:LIKE '%keyword%' 全表扫描
PRINT '===== 反模式:LIKE %keyword% =====';
SELECT ProductID, ProductName, Gemstone, UnitPrice
FROM JewelryProducts
WHERE ProductName LIKE '%钻石%' OR Gemstone LIKE '%钻石%';
-- 4.2 优化方案:前缀匹配 LIKE 'keyword%' 可利用索引
PRINT '===== 优化方案:前缀匹配 =====';
SELECT ProductID, ProductName, Gemstone, UnitPrice
FROM JewelryProducts
WHERE ProductName LIKE '钻石%';
-- 4.3 优化方案:多条件动态搜索(参数化动态SQL防注入)
PRINT '===== 优化方案:动态搜索 =====';
DECLARE @sql NVARCHAR(MAX) = N'
SELECT ProductID, ProductName, MetalType, Gemstone, UnitPrice
FROM JewelryProducts
WHERE 1=1';
DECLARE @MetalType NVARCHAR(50) = '铂金';
DECLARE @MinPrice DECIMAL(12,2) = 10000;
DECLARE @Gemstone NVARCHAR(100) = NULL;
IF @MetalType IS NOT NULL
SET @sql += N' AND MetalType = @mt';
IF @MinPrice IS NOT NULL
SET @sql += N' AND UnitPrice >= @mp';
IF @Gemstone IS NOT NULL
SET @sql += N' AND Gemstone = @gs';
SET @sql += N' ORDER BY UnitPrice DESC';
EXEC sp_executesql @sql,
N'@mt NVARCHAR(50), @mp DECIMAL(12,2), @gs NVARCHAR(100)',
@mt = @MetalType, @mp = @MinPrice, @gs = @Gemstone;
-- ==================== 第五部分:窗口函数 ====================
-- 5.1 排名与 Top-N 分析
PRINT '===== 窗口函数:每种宝石价格最高的前2件产品 =====';
SELECT ProductID, ProductName, Gemstone, UnitPrice, PriceRank
FROM (
SELECT
ProductID,
ProductName,
Gemstone,
UnitPrice,
ROW_NUMBER() OVER (PARTITION BY Gemstone ORDER BY UnitPrice DESC) AS PriceRank
FROM JewelryProducts
WHERE Gemstone IS NOT NULL
) ranked
WHERE PriceRank <= 2;
-- 5.2 累计销售额与排名
PRINT '===== 窗口函数:销售人员累计销售额 =====';
SELECT
SalespersonID,
SaleDate,
SaleAmount,
SUM(SaleAmount) OVER (
PARTITION BY SalespersonID
ORDER BY SaleDate
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS CumulativeSales
FROM JewelrySalesLog
ORDER BY SalespersonID, SaleDate;
-- 5.3 移动平均与同比分析
PRINT '===== 窗口函数:移动平均与LAG/LEAD =====';
SELECT
ProductID,
SaleDate,
SaleAmount,
AVG(SaleAmount) OVER (
PARTITION BY ProductID
ORDER BY SaleDate
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW
) AS MovingAvg3,
LAG(SaleAmount, 1) OVER (
PARTITION BY ProductID ORDER BY SaleDate
) AS PrevSaleAmount,
LEAD(SaleAmount, 1) OVER (
PARTITION BY ProductID ORDER BY SaleDate
) AS NextSaleAmount
FROM JewelrySalesLog
ORDER BY ProductID, SaleDate;
-- 5.4 NTILE 价格分档
PRINT '===== 窗口函数:NTILE 价格分档 =====';
SELECT
ProductID,
ProductName,
UnitPrice,
NTILE(4) OVER (ORDER BY UnitPrice) AS PriceTier
FROM JewelryProducts;
-- ==================== 第六部分:递归 CTE ====================
-- 6.1 构建珠宝产品完整分类路径
PRINT '===== 递归CTE:分类路径构建 =====';
WITH CategoryHierarchy AS (
-- 锚点成员:顶级分类(无父级)
SELECT
CategoryID,
CategoryName,
ParentCategoryID,
CAST(CategoryName AS NVARCHAR(500)) AS FullPath,
1 AS HierarchyLevel
FROM JewelryCategories
WHERE ParentCategoryID IS NULL
UNION ALL
-- 递归成员:逐级向下查找子分类
SELECT
c.CategoryID,
c.CategoryName,
c.ParentCategoryID,
CAST(ch.FullPath + ' > ' + c.CategoryName AS NVARCHAR(500)) AS FullPath,
ch.HierarchyLevel + 1
FROM JewelryCategories c
INNER JOIN CategoryHierarchy ch ON c.ParentCategoryID = ch.CategoryID
)
SELECT
ch.FullPath AS CategoryPath,
p.ProductName,
p.MetalType,
p.Gemstone,
p.UnitPrice
FROM CategoryHierarchy ch
LEFT JOIN JewelryProducts p ON ch.CategoryID = p.CategoryID
ORDER BY ch.FullPath
OPTION (MAXRECURSION 10);
-- 6.2 查找所有子分类(含间接子级)
PRINT '===== 递归CTE:查找所有子分类产品 =====';
DECLARE @RootCategoryID INT = 1;
WITH SubCategories AS (
SELECT CategoryID, CategoryName, ParentCategoryID
FROM JewelryCategories
WHERE CategoryID = @RootCategoryID
UNION ALL
SELECT c.CategoryID, c.CategoryName, c.ParentCategoryID
FROM JewelryCategories c
INNER JOIN SubCategories sc ON c.ParentCategoryID = sc.CategoryID
)
SELECT
sc.CategoryName,
p.ProductName,
p.UnitPrice
FROM SubCategories sc
LEFT JOIN JewelryProducts p ON sc.CategoryID = p.CategoryID
ORDER BY sc.CategoryName;
-- ==================== 第七部分:PIVOT / UNPIVOT 透视转换 ====================
-- 7.1 PIVOT:按金属类型透视销售汇总
PRINT '===== PIVOT:按金属类型透视 =====';
SELECT *
FROM (
SELECT
c.CustomerName,
p.MetalType,
od.LineTotal AS SaleAmount
FROM JewelryOrderDetails od
JOIN JewelryOrders o ON od.OrderID = o.OrderID
JOIN JewelryCustomers c ON o.CustomerID = c.CustomerID
JOIN JewelryProducts p ON od.ProductID = p.ProductID
WHERE o.OrderStatus = '已完成'
) AS SourceData
PIVOT (
SUM(SaleAmount)
FOR MetalType IN ([黄金], [铂金], [银])
) AS PivotTable;
-- 7.2 条件聚合替代 PIVOT(更灵活)
PRINT '===== 条件聚合替代PIVOT =====';
SELECT
c.CustomerName,
SUM(CASE WHEN p.MetalType = '黄金' THEN od.LineTotal ELSE 0 END) AS GoldSales,
SUM(CASE WHEN p.MetalType = '铂金' THEN od.LineTotal ELSE 0 END) AS PlatinumSales,
SUM(CASE WHEN p.MetalType = '银' THEN od.LineTotal ELSE 0 END) AS SilverSales,
SUM(od.LineTotal) AS TotalSales
FROM JewelryOrderDetails od
JOIN JewelryOrders o ON od.OrderID = o.OrderID
JOIN JewelryCustomers c ON o.CustomerID = c.CustomerID
JOIN JewelryProducts p ON od.ProductID = p.ProductID
WHERE o.OrderStatus = '已完成'
GROUP BY c.CustomerName;
-- 7.3 UNPIVOT 示例:先创建月度汇总表再转换
PRINT '===== UNPIVOT:宽表转长表 =====';
-- 创建月度销售汇总表
IF OBJECT_ID('MonthlySales', 'U') IS NOT NULL DROP TABLE MonthlySales;
CREATE TABLE MonthlySales (
ProductID INT PRIMARY KEY,
JanSales DECIMAL(12,2),
FebSales DECIMAL(12,2),
MarSales DECIMAL(12,2)
);
INSERT INTO MonthlySales VALUES
(1, 3500.00, 0, 3500.00),
(2, 85000.00, 85000.00, 0),
(3, 42000.00, 0, 42000.00);
SELECT ProductID, SaleMonth, SaleAmount
FROM MonthlySales
UNPIVOT (
SaleAmount FOR SaleMonth IN (JanSales, FebSales, MarSales)
) AS UnpivotedData;
-- ==================== 第八部分:GROUPING SETS 多级聚合 ====================
PRINT '===== GROUPING SETS:多维度销售汇总 =====';
SELECT
GROUPING_ID(p.MetalType, c.MembershipLevel) AS GroupLevel,
p.MetalType,
c.MembershipLevel,
COUNT(*) AS OrderCount,
SUM(od.LineTotal) AS TotalSales,
AVG(od.LineTotal) AS AvgOrderValue
FROM JewelryOrderDetails od
JOIN JewelryOrders o ON od.OrderID = o.OrderID
JOIN JewelryCustomers c ON o.CustomerID = c.CustomerID
JOIN JewelryProducts p ON od.ProductID = p.ProductID
WHERE o.OrderStatus = '已完成'
GROUP BY GROUPING SETS (
(p.MetalType, c.MembershipLevel), -- 按金属+会员等级
(p.MetalType), -- 按金属类型小计
(c.MembershipLevel), -- 按会员等级小计
() -- 总计
)
ORDER BY GROUPING_ID(p.MetalType, c.MembershipLevel), p.MetalType, c.MembershipLevel;
-- ==================== 清理(可选)====================
-- 如需删除测试表,取消以下注释:
/*
DROP TABLE JewelryOrderDetails;
DROP TABLE JewelryOrders;
DROP TABLE JewelrySalesLog;
DROP TABLE JewelryProducts;
DROP TABLE JewelryCustomers;
DROP TABLE JewelryCategories;
DROP TABLE MonthlySales;
*/
-- ==================== 第八部分:索引优化与执行计划分析 ====================
-- 开启实际执行计划(在SSMS中也可按 Ctrl+M 开启)
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
GO
-- ==================== 8.1 核心算子识别与优化实战 ====================
-- 场景1:消除 Key Lookup(回表查找)
-- 反模式:索引只覆盖过滤条件,查询时需要回表取其他列
PRINT '===== 场景1:Key Lookup 优化 =====';
-- 模拟未覆盖索引的情况(先删除之前建的宽索引以演示问题)
IF EXISTS (SELECT * FROM sys.indexes WHERE name = 'IX_JewelryOrders_Status')
DROP INDEX IX_JewelryOrders_Status ON JewelryOrders;
-- 只建过滤列的窄索引
CREATE NONCLUSTERED INDEX IX_JewelryOrders_Status_Narrow
ON JewelryOrders (OrderStatus);
-- 执行查询:需要 OrderDate, TotalAmount 等未包含在索引中的列
SELECT OrderID, CustomerID, OrderDate, TotalAmount
FROM JewelryOrders
WHERE OrderStatus = '已完成';
-- 优化:创建覆盖索引,将 SELECT 需要的列放入 INCLUDE
CREATE NONCLUSTERED INDEX IX_JewelryOrders_Status_Covering
ON JewelryOrders (OrderStatus)
INCLUDE (CustomerID, OrderDate, TotalAmount);
-- 再次执行,观察执行计划中 Key Lookup 消失,逻辑读取大幅下降
SELECT OrderID, CustomerID, OrderDate, TotalAmount
FROM JewelryOrders
WHERE OrderStatus = '已完成';
-- 场景2:消除 Sort 算子(利用索引预排序)
PRINT '===== 场景2:Sort 算子优化 =====';
-- 反模式:ORDER BY 列无索引,需要额外排序操作
SELECT ProductID, ProductName, UnitPrice
FROM JewelryProducts
ORDER BY UnitPrice DESC;
-- 优化:创建包含排序列的索引
-- 注意:索引键列顺序应与 ORDER BY 一致
CREATE NONCLUSTERED INDEX IX_JewelryProducts_Price_Sort
ON JewelryProducts (UnitPrice DESC)
INCLUDE (ProductID, ProductName);
-- 再次执行,执行计划中 Sort 算子消失,变为 Index Scan(有序扫描)
SELECT ProductID, ProductName, UnitPrice
FROM JewelryProducts
ORDER BY UnitPrice DESC;
-- 场景3:隐式类型转换导致索引失效
PRINT '===== 场景3:隐式转换优化 =====';
-- 反模式:参数类型与列类型不匹配,导致全表扫描
-- 假设 MetalType 是 NVARCHAR,但传入 VARCHAR 参数
DECLARE @Metal VARCHAR(50) = '黄金'; -- 故意用 VARCHAR
SELECT ProductID, ProductName, UnitPrice
FROM JewelryProducts
WHERE MetalType = @Metal; -- 执行计划会出现黄色警告三角形
-- 优化:确保参数类型与列类型完全一致
DECLARE @MetalCorrect NVARCHAR(50) = '黄金';
SELECT ProductID, ProductName, UnitPrice
FROM JewelryProducts
WHERE MetalType = @MetalCorrect;
-- ==================== 8.2 缺失索引 DMV 查询 ====================
PRINT '===== 缺失索引建议(基于当前缓存计划) =====';
SELECT TOP 10
migs.avg_total_user_cost * migs.avg_user_impact * (migs.user_seeks + migs.user_scans) AS ImprovementMeasure,
OBJECT_NAME(mid.object_id) AS TableName,
mid.equality_columns, -- 等值过滤列
mid.inequality_columns, -- 不等值过滤列
mid.included_columns, -- 建议包含列
migs.user_seeks,
migs.user_scans,
'CREATE INDEX IX_' + OBJECT_NAME(mid.object_id) + '_' +
REPLACE(REPLACE(REPLACE(COALESCE(mid.equality_columns,'') +
COALESCE(mid.inequality_columns,''), ',', '_'), '[', ''), ']', '') +
' ON ' + OBJECT_NAME(mid.object_id) +
' (' + COALESCE(mid.equality_columns,'') +
CASE WHEN mid.equality_columns IS NOT NULL AND mid.inequality_columns IS NOT NULL THEN ', ' ELSE '' END +
COALESCE(mid.inequality_columns,'') + ')' +
CASE WHEN mid.included_columns IS NOT NULL THEN ' INCLUDE (' + mid.included_columns + ')' ELSE '' END AS CreateIndexStatement
FROM sys.dm_db_missing_index_groups mig
JOIN sys.dm_db_missing_index_group_stats migs ON mig.index_group_handle = migs.group_handle
JOIN sys.dm_db_missing_index_details mid ON mig.index_handle = mid.index_handle
ORDER BY ImprovementMeasure DESC;
-- ==================== 8.3 索引碎片检查与维护 ====================
PRINT '===== 索引碎片检查 =====';
SELECT
OBJECT_NAME(ps.object_id) AS TableName,
i.name AS IndexName,
ps.avg_fragmentation_in_percent,
ps.page_count,
CASE
WHEN ps.avg_fragmentation_in_percent > 30 THEN '建议重建索引 (REBUILD)'
WHEN ps.avg_fragmentation_in_percent > 5 THEN '建议重组索引 (REORGANIZE)'
ELSE '无需维护'
END AS Recommendation
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, 'LIMITED') ps
JOIN sys.indexes i ON ps.object_id = i.object_id AND ps.index_id = i.index_id
WHERE ps.avg_fragmentation_in_percent > 5
AND ps.page_count > 100
ORDER BY ps.avg_fragmentation_in_percent DESC;
-- 索引维护示例(按需执行)
-- ALTER INDEX IX_JewelryOrders_Status_Covering ON JewelryOrders REORGANIZE;
-- ALTER INDEX ALL ON JewelryOrders REBUILD WITH (ONLINE = ON); -- 企业版支持联机重建
-- ==================== 8.4 查询存储(Query Store)分析 ====================
-- SQL Server 2019 推荐开启 Query Store 进行长期性能追踪
-- 检查 Query Store 是否开启
SELECT
name,
is_query_store_on,
query_store_retention_interval_in_minutes,
max_storage_size_mb
FROM sys.databases
WHERE name = DB_NAME();
-- 若未开启,可执行以下命令开启(需要 ALTER DATABASE 权限)
-- ALTER DATABASE CURRENT SET QUERY_STORE = ON;
-- ALTER DATABASE CURRENT SET QUERY_STORE (OPERATION_MODE = READ_WRITE);
-- 查询最耗时的前10个查询
PRINT '===== Query Store:最耗时查询 Top 10 =====';
SELECT TOP 10
qsqt.query_sql_text,
qrs.avg_duration / 1000.0 AS AvgDurationMS,
qrs.count_executions,
qrs.last_execution_time
FROM sys.query_store_query_text qsqt
JOIN sys.query_store_query qsq ON qsqt.query_text_id = qsq.query_text_id
JOIN sys.query_store_runtime_stats qrs ON qsq.query_id = qrs.query_id
WHERE qrs.last_execution_time > DATEADD(DAY, -7, GETDATE())
ORDER BY qrs.avg_duration DESC;
-- ==================== 8.5 执行计划对比模板 ====================
PRINT '===== 执行计划对比:优化前 vs 优化后 =====';
-- 优化前:使用 OFFSET-FETCH 分页(大偏移量性能差)
SELECT ProductID, ProductName, UnitPrice
FROM JewelryProducts
ORDER BY UnitPrice DESC
OFFSET 10000 ROWS FETCH NEXT 10 ROWS ONLY;
-- 优化后:使用 Keyset 分页(性能稳定)
DECLARE @LastPrice DECIMAL(12,2) = 0;
DECLARE @LastID INT = 0;
SELECT TOP 10 ProductID, ProductName, UnitPrice
FROM JewelryProducts
WHERE (UnitPrice < @LastPrice)
OR (UnitPrice = @LastPrice AND ProductID < @LastID)
ORDER BY UnitPrice DESC, ProductID DESC;
-- 关闭统计信息
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
GO
/* =====================================================================
SQL Server 2019 珠宝行业高级开发实战完整脚本
业务场景: 珠宝集团 SaaS 平台(多租户)→ 客户维度管理(SCD2)→ 销售分析(批模式+自适应联接)
执行方式: 在 SSMS 中按 F5 整段执行
===================================================================== */
SET NOCOUNT ON;
GO
/* =====================================================================
0. 准备:创建数据库与基础配置
===================================================================== */
USE master;
GO
IF DB_ID('JewelryERP_Advanced') IS NOT NULL
BEGIN
ALTER DATABASE JewelryERP_Advanced SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
DROP DATABASE JewelryERP_Advanced;
END
GO
CREATE DATABASE JewelryERP_Advanced;
GO
USE JewelryERP_Advanced;
GO
-- 设置兼容性级别为 150(SQL Server 2019),启用智能查询处理
ALTER DATABASE CURRENT SET COMPATIBILITY_LEVEL = 150;
GO
PRINT '=== 数据库 JewelryERP_Advanced 已创建,兼容性级别 150 ===';
GO
/* =====================================================================
模块一:RLS 多租户 + 分区表解决数据倾斜
===================================================================== */
PRINT '=== 模块一:RLS + 分区表 ===';
GO
-- 1.1 创建文件组与数据文件(生产环境建议放不同磁盘)
ALTER DATABASE CURRENT ADD FILEGROUP FG_Tenant_Large;
ALTER DATABASE CURRENT ADD FILEGROUP FG_Tenant_Small;
ALTER DATABASE CURRENT ADD FILE
(NAME = 'Data_Large', FILENAME = 'D:\Data\TenantLarge.ndf', SIZE = 100MB, FILEGROWTH = 50MB)
TO FILEGROUP FG_Tenant_Large;
ALTER DATABASE CURRENT ADD FILE
(NAME = 'Data_Small', FILENAME = 'D:\Data\TenantSmall.ndf', SIZE = 10MB, FILEGROWTH = 10MB)
TO FILEGROUP FG_Tenant_Small;
GO
-- 1.2 创建分区函数与分区方案(按 TenantID 分区)
CREATE PARTITION FUNCTION PF_TenantRange (INT)
AS RANGE LEFT FOR VALUES (1); -- <=1 归分区1(大租户),>1 归分区2(小租户)
GO
CREATE PARTITION SCHEME PS_TenantRange
AS PARTITION PF_TenantRange
TO (FG_Tenant_Large, FG_Tenant_Small);
GO
-- 1.3 创建分区表(珠宝订单表)
IF OBJECT_ID('dbo.JewelryOrders_Partitioned', 'U') IS NOT NULL DROP TABLE JewelryOrders_Partitioned;
CREATE TABLE JewelryOrders_Partitioned (
OrderID BIGINT IDENTITY(1,1),
TenantID INT NOT NULL,
CustomerID INT,
OrderDate DATETIME,
TotalAmount DECIMAL(12,2),
Status NVARCHAR(20),
ProductCategory NVARCHAR(50),
CONSTRAINT PK_JewelryOrders_Partitioned PRIMARY KEY CLUSTERED (TenantID, OrderID)
) ON PS_TenantRange(TenantID);
GO
-- 1.4 插入模拟数据
-- 大租户(ID=1):插入 100 万行
INSERT INTO JewelryOrders_Partitioned (TenantID, CustomerID, OrderDate, TotalAmount, Status, ProductCategory)
SELECT TOP 1000000
1,
ABS(CHECKSUM(NEWID())) % 10000 + 1,
DATEADD(MINUTE, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 525600, '2024-01-01'),
ROUND(RAND(CHECKSUM(NEWID())) * 50000 + 500, 2),
CASE ABS(CHECKSUM(NEWID())) % 4 WHEN 0 THEN '已完成' WHEN 1 THEN '已支付' WHEN 2 THEN '已发货' ELSE '待支付' END,
CASE ABS(CHECKSUM(NEWID())) % 5 WHEN 0 THEN '钻戒' WHEN 1 THEN '黄金项链' WHEN 2 THEN '铂金手镯' WHEN 3 THEN '翡翠吊坠' ELSE '珍珠耳环' END
FROM sys.all_objects a, sys.all_objects b;
-- 小租户(ID=2,3,4):各插入 1000 行
INSERT INTO JewelryOrders_Partitioned (TenantID, CustomerID, OrderDate, TotalAmount, Status, ProductCategory)
SELECT TOP 3000
(ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 3) + 2,
ABS(CHECKSUM(NEWID())) % 1000 + 1,
DATEADD(MINUTE, ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) % 525600, '2024-01-01'),
ROUND(RAND(CHECKSUM(NEWID())) * 5000 + 100, 2),
'已完成',
'黄金项链'
FROM sys.all_objects a;
GO
-- 1.5 创建 RLS 安全策略
CREATE SCHEMA Security;
GO
CREATE OR ALTER FUNCTION Security.fn_TenantPredicate(@TenantID INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS Result
WHERE CAST(SESSION_CONTEXT(N'TenantID') AS INT) = @TenantID;
GO
CREATE SECURITY POLICY Security.TenantPartitionPolicy
ADD FILTER PREDICATE Security.fn_TenantPredicate(TenantID)
ON dbo.JewelryOrders_Partitioned
WITH (STATE = ON);
GO
-- 1.6 验证分区消除与 RLS
SET STATISTICS IO ON;
PRINT '--- 大租户查询(应只扫描分区1)---';
EXEC sp_set_session_context N'TenantID', 1;
SELECT COUNT(*) AS LargeTenantOrders FROM JewelryOrders_Partitioned WHERE Status = '已完成';
PRINT '--- 小租户查询(应只扫描分区2)---';
EXEC sp_set_session_context N'TenantID', 2;
SELECT COUNT(*) AS SmallTenantOrders FROM JewelryOrders_Partitioned WHERE Status = '已完成';
SET STATISTICS IO OFF;
GO
-- 查看分区数据分布
SELECT
$PARTITION.PF_TenantRange(TenantID) AS PartitionNumber,
TenantID,
COUNT(*) AS RowCount
FROM JewelryOrders_Partitioned
GROUP BY $PARTITION.PF_TenantRange(TenantID), TenantID
ORDER BY PartitionNumber;
GO
PRINT '=== 模块一完成 ===';
GO
/* =====================================================================
模块二:SCD Type 2 增量 ETL(MERGE + OUTPUT)
===================================================================== */
PRINT '=== 模块二:SCD Type 2 ETL ===';
GO
-- 2.1 创建客户维度表(SCD Type 2)
IF OBJECT_ID('dbo.DimCustomer_SCD2', 'U') IS NOT NULL DROP TABLE DimCustomer_SCD2;
CREATE TABLE DimCustomer_SCD2 (
CustomerKey INT IDENTITY(1,1) PRIMARY KEY,
CustomerID INT NOT NULL,
CustomerName NVARCHAR(100) NOT NULL,
City NVARCHAR(50),
MembershipLevel NVARCHAR(20),
Email NVARCHAR(100),
StartDate DATE NOT NULL,
EndDate DATE NOT NULL,
IsCurrent BIT NOT NULL DEFAULT 1,
DataHash AS CHECKSUM(CustomerName, City, MembershipLevel),
INDEX IX_DimCustomer_Current (CustomerID, IsCurrent)
);
GO
-- 初始化维度数据
INSERT INTO DimCustomer_SCD2 (CustomerID, CustomerName, City, MembershipLevel, Email, StartDate, EndDate, IsCurrent)
VALUES
(1001, '张三', '北京', '金卡', 'zhangsan@example.com', '2023-01-01', '9999-12-31', 1),
(1002, '李四', '上海', '银卡', 'lisi@example.com', '2023-01-01', '9999-12-31', 1),
(1003, '王五', '广州', '普通', 'wangwu@example.com', '2023-01-01', '9999-12-31', 1);
GO
-- 2.2 创建临时表(存放新版本行)
IF OBJECT_ID('dbo.Temp_NewVersions', 'U') IS NOT NULL DROP TABLE Temp_NewVersions;
CREATE TABLE Temp_NewVersions (
CustomerID INT,
CustomerName NVARCHAR(100),
City NVARCHAR(50),
MembershipLevel NVARCHAR(20),
StartDate DATE,
EndDate DATE,
IsCurrent BIT
);
GO
-- 2.3 模拟 CDC 增量数据源
IF OBJECT_ID('dbo.Stg_Customer_Changes', 'U') IS NOT NULL DROP TABLE Stg_Customer_Changes;
CREATE TABLE Stg_Customer_Changes (
CustomerID INT PRIMARY KEY,
CustomerName NVARCHAR(100),
City NVARCHAR(50),
MembershipLevel NVARCHAR(20),
ChangeDate DATE,
DataHash AS CHECKSUM(CustomerName, City, MembershipLevel)
);
-- 增量数据:张三城市变更,李四不变,赵五新增
INSERT INTO Stg_Customer_Changes (CustomerID, CustomerName, City, MembershipLevel, ChangeDate) VALUES
(1001, '张三', '深圳', '金卡', '2024-01-15'),
(1002, '李四', '上海', '银卡', '2024-01-15'),
(1004, '赵五', '成都', '普通', '2024-01-15');
GO
-- 2.4 核心 ETL:MERGE + OUTPUT
TRUNCATE TABLE Temp_NewVersions;
MERGE INTO DimCustomer_SCD2 AS Target
USING Stg_Customer_Changes AS Source
ON Target.CustomerID = Source.CustomerID
AND Target.IsCurrent = 1
WHEN MATCHED AND Target.DataHash <> Source.DataHash THEN
UPDATE SET
Target.EndDate = DATEADD(DAY, -1, Source.ChangeDate),
Target.IsCurrent = 0
WHEN NOT MATCHED BY TARGET THEN
INSERT (CustomerID, CustomerName, City, MembershipLevel, Email, StartDate, EndDate, IsCurrent, DataHash)
VALUES (Source.CustomerID, Source.CustomerName, Source.City, Source.MembershipLevel,
NULL, Source.ChangeDate, '9999-12-31', 1, Source.DataHash)
OUTPUT
INSERTED.CustomerID,
Source.CustomerName,
Source.City,
Source.MembershipLevel,
Source.ChangeDate,
'9999-12-31',
1
INTO Temp_NewVersions (CustomerID, CustomerName, City, MembershipLevel, StartDate, EndDate, IsCurrent);
-- 插入新版本
INSERT INTO DimCustomer_SCD2 (CustomerID, CustomerName, City, MembershipLevel, StartDate, EndDate, IsCurrent)
SELECT CustomerID, CustomerName, City, MembershipLevel, StartDate, EndDate, IsCurrent
FROM Temp_NewVersions;
GO
-- 2.5 验证结果
PRINT '===== 张三的完整历史轨迹 =====';
SELECT CustomerKey, CustomerID, CustomerName, City, MembershipLevel, StartDate, EndDate, IsCurrent
FROM DimCustomer_SCD2
WHERE CustomerID = 1001
ORDER BY StartDate;
PRINT '===== 当前所有客户状态 =====';
SELECT CustomerID, CustomerName, City, MembershipLevel, IsCurrent
FROM DimCustomer_SCD2
WHERE IsCurrent = 1
ORDER BY CustomerID;
GO
-- 2.6 封装为存储过程
CREATE OR ALTER PROCEDURE dbo.usp_SCD2_Merge_ETL
@ChangeDate DATE
AS
BEGIN
SET NOCOUNT ON;
TRUNCATE TABLE Temp_NewVersions;
MERGE INTO DimCustomer_SCD2 AS Target
USING Stg_Customer_Changes AS Source
ON Target.CustomerID = Source.CustomerID
AND Target.IsCurrent = 1
WHEN MATCHED AND Target.DataHash <> Source.DataHash THEN
UPDATE SET
Target.EndDate = DATEADD(DAY, -1, Source.ChangeDate),
Target.IsCurrent = 0
WHEN NOT MATCHED BY TARGET THEN
INSERT (CustomerID, CustomerName, City, MembershipLevel, Email, StartDate, EndDate, IsCurrent, DataHash)
VALUES (Source.CustomerID, Source.CustomerName, Source.City, Source.MembershipLevel,
NULL, Source.ChangeDate, '9999-12-31', 1, Source.DataHash)
OUTPUT
INSERTED.CustomerID, Source.CustomerName, Source.City, Source.MembershipLevel,
Source.ChangeDate, '9999-12-31', 1
INTO Temp_NewVersions (CustomerID, CustomerName, City, MembershipLevel, StartDate, EndDate, IsCurrent);
INSERT INTO DimCustomer_SCD2 (CustomerID, CustomerName, City, MembershipLevel, StartDate, EndDate, IsCurrent)
SELECT CustomerID, CustomerName, City, MembershipLevel, StartDate, EndDate, IsCurrent
FROM Temp_NewVersions;
PRINT 'ETL 完成:影响 ' + CAST(@@ROWCOUNT AS VARCHAR) + ' 行';
END;
GO
PRINT '=== 模块二完成 ===';
GO
/* =====================================================================
模块三:批处理执行模式(Batch Mode)与自适应联接
===================================================================== */
PRINT '=== 模块三:批模式与自适应联接 ===';
GO
-- 3.1 创建销售事实表(行存储)
IF OBJECT_ID('dbo.Fact_Sales_BatchTest', 'U') IS NOT NULL DROP TABLE Fact_Sales_BatchTest;
CREATE TABLE Fact_Sales_BatchTest (
SaleID INT IDENTITY(1,1) PRIMARY KEY,
ProductID INT,
SaleDate DATE,
Amount DECIMAL(12,2),
Quantity INT,
Region NVARCHAR(50)
);
-- 插入 500 万行数据
;WITH Numbers AS (
SELECT TOP 5000000 ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS N
FROM sys.all_objects a, sys.all_objects b, sys.all_objects c
)
INSERT INTO Fact_Sales_BatchTest (ProductID, SaleDate, Amount, Quantity, Region)
SELECT
N % 1000 + 1,
DATEADD(DAY, N % 1095, '2022-01-01'),
ROUND(RAND(CHECKSUM(NEWID())) * 10000 + 100, 2),
N % 20 + 1,
CASE N % 5 WHEN 0 THEN '华东' WHEN 1 THEN '华南' WHEN 2 THEN '华北' WHEN 3 THEN '西南' ELSE '华中' END
FROM Numbers;
GO
-- 3.2 创建行存储索引 + 列存储索引(触发批模式)
CREATE NONCLUSTERED INDEX IX_Fact_Sales_Region
ON Fact_Sales_BatchTest (Region) INCLUDE (Amount, Quantity);
CREATE NONCLUSTERED COLUMNSTORE INDEX NCCI_Fact_Sales
ON Fact_Sales_BatchTest (ProductID, Amount, Quantity, Region);
GO
-- 3.3 创建维度表
IF OBJECT_ID('dbo.Dim_Product', 'U') IS NOT NULL DROP TABLE Dim_Product;
CREATE TABLE Dim_Product (
ProductID INT PRIMARY KEY,
ProductName NVARCHAR(200),
Category NVARCHAR(50)
);
INSERT INTO Dim_Product (ProductID, ProductName, Category)
SELECT TOP 1000
N,
'产品_' + CAST(N AS VARCHAR),
CASE N % 5 WHEN 0 THEN '黄金' WHEN 1 THEN '铂金' WHEN 2 THEN '钻石' WHEN 3 THEN '银' ELSE '玉石' END
FROM (SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS N FROM sys.all_objects a) T;
GO
-- 3.4 执行分析查询(观察批模式与自适应联接)
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
-- 查询1:聚合分析(触发批模式)
PRINT '--- 聚合查询(批模式)---';
SELECT Region, SUM(Amount) AS TotalSales, AVG(Quantity) AS AvgQty
FROM Fact_Sales_BatchTest
GROUP BY Region;
-- 查询2:Join 查询(触发自适应联接)
PRINT '--- Join 查询(自适应联接)---';
SELECT p.Category, SUM(f.Amount) AS TotalSales
FROM Fact_Sales_BatchTest f
JOIN Dim_Product p ON f.ProductID = p.ProductID
WHERE p.Category = '黄金'
GROUP BY p.Category;
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
GO
-- 3.5 通过 DMV 查看执行模式
SELECT TOP 5
qsqt.query_sql_text,
qsp.query_plan,
qrs.avg_duration / 1000.0 AS AvgDurationMS
FROM sys.query_store_query_text qsqt
JOIN sys.query_store_query qsq ON qsqt.query_text_id = qsq.query_text_id
JOIN sys.query_store_plan qsp ON qsq.query_id = qsp.query_id
JOIN sys.query_store_runtime_stats qrs ON qsp.plan_id = qrs.plan_id
WHERE qsqt.query_sql_text LIKE '%Fact_Sales_BatchTest%'
AND qrs.last_execution_time > DATEADD(MINUTE, -10, GETDATE())
ORDER BY qrs.last_execution_time DESC;
GO
PRINT '=== 模块三完成 ===';
GO
/* =====================================================================
收尾:清理与总结
===================================================================== */
PRINT '============================================';
PRINT '所有模块执行完毕!';
PRINT '============================================';
PRINT '关键验证点:';
PRINT '1. 模块一:查看 STATISTICS IO 输出,确认大/小租户 Scan count 均为 1(分区消除生效)';
PRINT '2. 模块二:查看张三的历史轨迹,确认有 2 行记录(旧版本+新版本)';
PRINT '3. 模块三:在 SSMS 中按 Ctrl+M 开启实际执行计划,右键算子属性查看:';
PRINT ' - Actual Execution Mode = Batch(批模式)';
PRINT ' - IsAdaptive = True(自适应联接)';
PRINT '============================================';