一、引言
本文基于一套完整的 SQL Server 脚本,系统梳理珠宝行业数据建模中的核心模式,涵盖范式设计(1NF 到 5NF)、反范式与触发器同步、缓慢变化维度(SCD)、层次模型、多租户策略以及综合业务查询。全文以珠宝业务场景为主线,从原料、款式、加工、库存到经销商销售,帮助读者理解每种建模模式的适用场景与选型思路。
二、范式设计(1NF ~ 5NF)
范式是关系型数据库设计的基础,本节通过珠宝业务中的宝石清单、款式与工艺关系、经销商授权等场景,逐一演示 1NF 到 5NF 的演进过程,并说明每个范式解决的问题。
2.1 第一范式(1NF)
保证列的原子性,消除重复组。以宝石清单为例,每个宝石单独一行,避免在一个字段中存储多个值。
2.2 第二范式(2NF)
在复合主键下消除部分函数依赖。通过拆分主表和关联表,让非键字段完全依赖整个主键。
2.3 第三范式(3NF)与查找表
消除传递依赖,将宝石类型等静态字典拆分为独立的查找表,减少数据冗余。
2.4 BCNF 范式
确保每个决定因子都是候选键。以工序与工艺师的关系为例,修正主键设计以满足 BCNF。
2.5 第四范式(4NF)与连接表
消除多值依赖,将款式的宝石和工艺拆分为两张独立的关联表,避免数据重复。
2.6 第五范式(5NF)
处理三方及以上不可无损拆分的关系,以经销商、款式、材质的三方授权为例,展示投影-连接范式的应用。
三、反范式设计与触发器同步
反范式通过冗余字段提升查询性能,适用于读多写少的报表场景。本节以库存汇总表为例,演示如何冗余款式名和宝石名,并通过触发器在源表更新时自动同步冗余字段。
四、缓慢变化维度(SCD)
维度数据会随时间变化,SCD 提供了三种处理策略。本节以珠宝经销商维度为例,分别演示 Type 1 直接覆盖、Type 2 新增行保留完整历史、Type 3 仅保留上一次值的实现方式。
4.1 SCD Type 1
直接覆盖旧值,不保留历史,适用于不需要追溯的维度。
4.2 SCD Type 2
新增行并记录生效与失效日期,保留完整历史轨迹,是最常用的策略。
4.3 SCD Type 3
通过新增字段保存上一次值,只保留最近一次变更记录。
五、层次模型
珠宝产品分类是一棵典型的树形结构,本节介绍三种主流层次模型:邻接表、嵌套集和闭包表,并说明各自的建表方式与适用场景。
5.1 邻接表
通过父节点外键表达层级关系,增删改简单,适合层级较浅、变动频繁的场景。
5.2 嵌套集
使用左右值表达层级,子树查询极快,但插入和移动节点需要重排左右值,适合读多写少的场景。
5.3 闭包表
通过祖先-后代关系表记录所有路径,查询灵活,适合复杂树形分析。
六、层次模型性能对比
针对查询「戒指」分类下所有子节点的场景,分别使用递归 CTE、BETWEEN 和 JOIN 三种方式实现,并对比各自的查询性能与适用场景。
七、多租户策略
面向珠宝集团 SaaS 场景,多个子品牌或加盟商共享系统。本节采用共享数据库加共享 Schema 加 TenantId 的方案,并通过行级安全(RLS)实现租户数据隔离。
7.1 租户表与业务表设计
通过 TenantId 关联业务数据,并在款式表上建立租户与款式的唯一约束。
7.2 行级安全(RLS)
创建过滤函数和安全策略,根据会话上下文中的 TenantId 自动过滤数据,演示不同租户登录后只能看到自己的数据。
八、综合业务查询示例
本节汇总多个典型查询,包括款式完整 BOM 信息、宝石原料类型查询、SCD Type 2 历史轨迹、反范式库存汇总以及 5NF 三方授权查询,展示各建模模式在实际业务中的联合使用。
九、建模模式选型速查表
以速查表形式汇总各建模模式的适用场景与珠宝行业示例,帮助读者在实际项目中快速做出选型决策。
十、总结
本文通过一套完整的珠宝行业脚本,覆盖了从范式设计到多租户的常见数据建模模式。理解每种模式的适用场景和权衡,是设计高质量数据库的关键。建议读者结合自身业务,按需选择并组合使用这些模式。
sql
/* =====================================================================
珠宝行业数据建模模式完整脚本
数据库: SQL Server 2019+
业务场景: 珠宝原料 -> 款式设计 -> 加工工单 -> 库存 -> 经销商 -> 销售
覆盖: 1NF~5NF / 反范式 / Lookup表 / Junction表 / SCD / 层次模型 / 多租户
执行方式: 在 SSMS 中按 F5 整段执行,或用 sqlcmd -i 本文件
===================================================================== */
SET NOCOUNT ON;
GO
/* =====================================================================
0. 准备:清理并创建数据库
===================================================================== */
USE master;
GO
IF DB_ID('JewelryERP') IS NOT NULL
BEGIN
ALTER DATABASE JewelryERP SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
DROP DATABASE JewelryERP;
END
GO
CREATE DATABASE JewelryERP;
GO
USE JewelryERP;
GO
PRINT '=== 数据库 JewelryERP 已创建 ===';
GO
/* =====================================================================
一、范式 Normalization (1NF ~ 5NF)
===================================================================== */
PRINT '=== 一、范式示例 ===';
GO
/* ---------- 1NF: 列原子性,无重复组 ---------- */
-- 反例(不执行,仅展示): GemMaterials 字段存 '红宝石,钻石' 多值
-- 正例: 每个宝石一行
CREATE TABLE dbo.Style_Gem_1NF
(
Id INT IDENTITY(1,1) PRIMARY KEY,
StyleId INT NOT NULL,
StyleName NVARCHAR(100) NOT NULL,
GemName NVARCHAR(50) NOT NULL
);
GO
INSERT INTO dbo.Style_Gem_1NF (StyleId, StyleName, GemName)
VALUES (1, N'铂金戒指', N'红宝石'),
(1, N'铂金戒指', N'钻石'),
(2, N'黄金吊坠', N'蓝宝石');
GO
PRINT '1NF 表 Style_Gem_1NF 已创建并插入 3 行';
GO
/* ---------- 2NF: 消除部分函数依赖 ---------- */
-- 复合主键 (StyleId, GemId) 时,非键字段必须完全依赖整个主键
-- 先建主表,再建关联表(修正原脚本外键顺序)
CREATE TABLE dbo.Style_2NF
(
StyleId INT PRIMARY KEY,
StyleName NVARCHAR(100) NOT NULL,
StyleCode NVARCHAR(50) NOT NULL
);
GO
CREATE TABLE dbo.Gem_2NF
(
GemId INT PRIMARY KEY,
GemName NVARCHAR(50) NOT NULL
);
GO
CREATE TABLE dbo.Style_Gem_Map_2NF
(
StyleId INT NOT NULL,
GemId INT NOT NULL,
Carat DECIMAL(10,3) NOT NULL,
PRIMARY KEY (StyleId, GemId),
FOREIGN KEY (StyleId) REFERENCES dbo.Style_2NF(StyleId),
FOREIGN KEY (GemId) REFERENCES dbo.Gem_2NF(GemId)
);
GO
INSERT INTO dbo.Style_2NF (StyleId, StyleName, StyleCode)
VALUES (1, N'铂金戒指', 'ST-001'), (2, N'黄金吊坠', 'ST-002');
INSERT INTO dbo.Gem_2NF (GemId, GemName)
VALUES (1, N'红宝石'), (2, N'钻石'), (3, N'蓝宝石');
INSERT INTO dbo.Style_Gem_Map_2NF (StyleId, GemId, Carat)
VALUES (1,1,0.5), (1,2,0.3), (2,3,0.8);
GO
PRINT '2NF 表 Style_2NF / Gem_2NF / Style_Gem_Map_2NF 已创建';
GO
/* ---------- 3NF: 消除传递依赖 + Lookup Table ---------- */
-- GemTypeName 依赖 GemTypeCode(非主键),需拆出查找表
CREATE TABLE dbo.GemType_Lookup_3NF
(
GemTypeCode NVARCHAR(20) PRIMARY KEY,
GemTypeName NVARCHAR(50) NOT NULL,
Remark NVARCHAR(200) NULL
);
GO
CREATE TABLE dbo.GemRaw_3NF
(
GemId INT PRIMARY KEY,
GemCode NVARCHAR(50) NOT NULL,
GemTypeCode NVARCHAR(20) NOT NULL,
WeightCarat DECIMAL(10,3) NOT NULL,
FOREIGN KEY (GemTypeCode) REFERENCES dbo.GemType_Lookup_3NF(GemTypeCode)
);
GO
INSERT INTO dbo.GemType_Lookup_3NF (GemTypeCode, GemTypeName, Remark)
VALUES ('D', N'钻石', N'最硬宝石'),
('R', N'红宝石', N'刚玉族'),
('S', N'蓝宝石', N'刚玉族');
INSERT INTO dbo.GemRaw_3NF (GemId, GemCode, GemTypeCode, WeightCarat)
VALUES (1, 'GEM-001', 'D', 1.0),
(2, 'GEM-002', 'R', 0.8),
(3, 'GEM-003', 'S', 1.2);
GO
PRINT '3NF + Lookup 表 GemType_Lookup_3NF / GemRaw_3NF 已创建';
GO
/* ---------- BCNF: 每个决定因子都必须是候选键 ---------- */
-- 业务规则: 一个工序固定由一位工艺师负责 -> ProcessCode 决定 CraftsmanId
-- 若主键是 (ProcessCode, CraftsmanId),则 ProcessCode 是决定因子但不是候选键,违反BCNF
-- 修正: ProcessCode 单独作主键,CraftsmanId 为普通属性
CREATE TABLE dbo.Process_BCNF
(
ProcessCode NVARCHAR(20) PRIMARY KEY,
ProcessName NVARCHAR(100) NOT NULL,
CraftsmanId INT NOT NULL
);
GO
CREATE TABLE dbo.Process_Workshop_BCNF
(
ProcessCode NVARCHAR(20),
WorkshopId INT,
PRIMARY KEY (ProcessCode, WorkshopId),
FOREIGN KEY (ProcessCode) REFERENCES dbo.Process_BCNF(ProcessCode)
);
GO
INSERT INTO dbo.Process_BCNF (ProcessCode, ProcessName, CraftsmanId)
VALUES ('P01', N'镶嵌', 101), ('P02', N'抛光', 102), ('P03', N'铸造', 103);
INSERT INTO dbo.Process_Workshop_BCNF (ProcessCode, WorkshopId)
VALUES ('P01',1), ('P01',2), ('P02',1), ('P03',3);
GO
PRINT 'BCNF 表 Process_BCNF / Process_Workshop_BCNF 已创建';
GO
/* ---------- 4NF: 消除多值依赖 + Junction Table ---------- */
-- 款式可以有多个宝石、多个工艺;宝石和工艺互相独立,不能放一张表
-- 拆成两张独立关联表(多对多中间表)
CREATE TABLE dbo.Style_Gem_Junction_4NF
(
StyleId INT,
GemId INT,
PRIMARY KEY (StyleId, GemId),
FOREIGN KEY (StyleId) REFERENCES dbo.Style_2NF(StyleId),
FOREIGN KEY (GemId) REFERENCES dbo.Gem_2NF(GemId)
);
GO
CREATE TABLE dbo.Style_Process_Junction_4NF
(
StyleId INT,
ProcessCode NVARCHAR(20),
PRIMARY KEY (StyleId, ProcessCode),
FOREIGN KEY (StyleId) REFERENCES dbo.Style_2NF(StyleId),
FOREIGN KEY (ProcessCode) REFERENCES dbo.Process_BCNF(ProcessCode)
);
GO
INSERT INTO dbo.Style_Gem_Junction_4NF (StyleId, GemId)
VALUES (1,1),(1,2),(2,3);
INSERT INTO dbo.Style_Process_Junction_4NF (StyleId, ProcessCode)
VALUES (1,'P01'),(1,'P02'),(2,'P01'),(2,'P03');
GO
PRINT '4NF + Junction 表 Style_Gem_Junction_4NF / Style_Process_Junction_4NF 已创建';
GO
/* ---------- 5NF: 投影-连接范式,三方关系不可无损拆分 ---------- */
-- 经销商-款式-材质 三方授权: 经销商可销售某款式,仅在指定材质下生效
-- 不能拆成任意两两表还能无损还原
CREATE TABLE dbo.Material_Lookup
(
MaterialId INT PRIMARY KEY,
MaterialName NVARCHAR(50) NOT NULL
);
GO
CREATE TABLE dbo.Dealer_5NF
(
DealerId INT PRIMARY KEY,
DealerName NVARCHAR(100) NOT NULL
);
GO
CREATE TABLE dbo.Dealer_Style_Material_5NF
(
DealerId INT NOT NULL,
StyleId INT NOT NULL,
MaterialId INT NOT NULL,
IsAuthorized BIT NOT NULL DEFAULT 0,
PRIMARY KEY (DealerId, StyleId, MaterialId),
FOREIGN KEY (DealerId) REFERENCES dbo.Dealer_5NF(DealerId),
FOREIGN KEY (StyleId) REFERENCES dbo.Style_2NF(StyleId),
FOREIGN KEY (MaterialId) REFERENCES dbo.Material_Lookup(MaterialId)
);
GO
INSERT INTO dbo.Material_Lookup (MaterialId, MaterialName)
VALUES (1, N'18K白金'), (2, N'18K黄金'), (3, N'铂金PT950');
INSERT INTO dbo.Dealer_5NF (DealerId, DealerName)
VALUES (1, N'深圳水贝经销商A'), (2, N'广州荔湾经销商B');
INSERT INTO dbo.Dealer_Style_Material_5NF (DealerId, StyleId, MaterialId, IsAuthorized)
VALUES (1,1,1,1),(1,1,3,1),(1,2,2,1),(2,1,1,1);
GO
PRINT '5NF 三方关系表 Dealer_Style_Material_5NF 已创建';
GO
/* =====================================================================
二、反范式 Denormalization + 触发器同步
===================================================================== */
PRINT '=== 二、反范式示例 ===';
GO
CREATE TABLE dbo.Inventory_Denormalized
(
InventoryId INT PRIMARY KEY IDENTITY(1,1),
StyleId INT NOT NULL,
StyleName NVARCHAR(100) NOT NULL, -- 冗余字段
GemId INT NOT NULL,
GemName NVARCHAR(50) NOT NULL, -- 冗余字段
StockQty INT NOT NULL,
UnitCost DECIMAL(18,2) NOT NULL,
UpdateTime DATETIME DEFAULT GETDATE()
);
GO
INSERT INTO dbo.Inventory_Denormalized (StyleId, StyleName, GemId, GemName, StockQty, UnitCost)
VALUES (1, N'铂金戒指', 1, N'红宝石', 50, 8800.00),
(1, N'铂金戒指', 2, N'钻石', 30, 12000.00),
(2, N'黄金吊坠', 3, N'蓝宝石', 80, 3500.00);
GO
-- 触发器: 主表款式名变更时同步冗余字段
CREATE TRIGGER dbo.trg_Style_Update_Denorm
ON dbo.Style_2NF
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF UPDATE(StyleName)
BEGIN
UPDATE inv
SET inv.StyleName = inserted.StyleName,
inv.UpdateTime = GETDATE()
FROM dbo.Inventory_Denormalized inv
INNER JOIN inserted ON inv.StyleId = inserted.StyleId;
END
END
GO
-- 验证触发器: 修改款式名
UPDATE dbo.Style_2NF SET StyleName = N'铂金六爪戒指' WHERE StyleId = 1;
GO
PRINT '反范式表 Inventory_Denormalized 已创建,触发器 trg_Style_Update_Denorm 已生效';
PRINT '已验证: StyleId=1 款式名变更后,冗余字段自动同步';
GO
/* =====================================================================
三、Slowly Changing Dimensions (SCD Type 1/2/3)
维度: 珠宝经销商
===================================================================== */
PRINT '=== 三、SCD 缓慢变化维度 ===';
GO
/* ---------- SCD Type 1: 直接覆盖(丢失历史) ---------- */
CREATE TABLE dbo.Dim_Dealer_SCD1
(
DealerKey INT PRIMARY KEY IDENTITY(1,1),
DealerCode NVARCHAR(30) NOT NULL UNIQUE,
DealerName NVARCHAR(100) NOT NULL,
Address NVARCHAR(200) NOT NULL
);
GO
INSERT INTO dbo.Dim_Dealer_SCD1 (DealerCode, DealerName, Address)
VALUES ('D001', N'深圳珠宝经销商A', N'罗湖区水贝');
GO
-- Type1 更新: 直接覆盖
UPDATE dbo.Dim_Dealer_SCD1 SET Address = N'龙岗区布吉' WHERE DealerCode = 'D001';
GO
PRINT 'SCD Type1 表 Dim_Dealer_SCD1 已创建(直接覆盖,旧地址丢失)';
GO
/* ---------- SCD Type 2: 新增行 + 生效/失效日期(最常用,保留完整历史) ---------- */
CREATE TABLE dbo.Dim_Dealer_SCD2
(
DealerSurrogateKey INT PRIMARY KEY IDENTITY(1,1),
DealerCode NVARCHAR(30) NOT NULL,
DealerName NVARCHAR(100) NOT NULL,
Address NVARCHAR(200) NOT NULL,
StartDate DATETIME NOT NULL,
EndDate DATETIME NULL, -- NULL = 当前有效
IsCurrent BIT NOT NULL DEFAULT 1
);
GO
-- 初始版本
INSERT INTO dbo.Dim_Dealer_SCD2 (DealerCode, DealerName, Address, StartDate, EndDate, IsCurrent)
VALUES ('D001', N'深圳珠宝经销商A', N'罗湖区水贝', '2024-01-01', NULL, 1);
GO
-- Type2 更新: 旧记录失效 + 新增版本
BEGIN TRANSACTION;
UPDATE dbo.Dim_Dealer_SCD2
SET EndDate = '2025-06-01', IsCurrent = 0
WHERE DealerCode = 'D001' AND IsCurrent = 1;
INSERT INTO dbo.Dim_Dealer_SCD2 (DealerCode, DealerName, Address, StartDate, EndDate, IsCurrent)
VALUES ('D001', N'深圳珠宝经销商A', N'龙岗区布吉', '2025-06-01', NULL, 1);
COMMIT;
GO
-- 再做一次变更,演示3个版本
BEGIN TRANSACTION;
UPDATE dbo.Dim_Dealer_SCD2
SET EndDate = '2026-01-01', IsCurrent = 0
WHERE DealerCode = 'D001' AND IsCurrent = 1;
INSERT INTO dbo.Dim_Dealer_SCD2 (DealerCode, DealerName, Address, StartDate, EndDate, IsCurrent)
VALUES ('D001', N'深圳珠宝经销商A(旗舰店)', N'福田区CBD', '2026-01-01', NULL, 1);
COMMIT;
GO
PRINT 'SCD Type2 表 Dim_Dealer_SCD2 已创建,包含 3 个历史版本';
GO
/* ---------- SCD Type 3: 新增字段保存上一次值(只保留最近一次历史) ---------- */
CREATE TABLE dbo.Dim_Dealer_SCD3
(
DealerKey INT PRIMARY KEY IDENTITY(1,1),
DealerCode NVARCHAR(30) NOT NULL UNIQUE,
DealerName NVARCHAR(100) NOT NULL,
CurrentAddress NVARCHAR(200) NOT NULL,
PreviousAddress NVARCHAR(200) NULL,
AddressChangeDate DATETIME NULL
);
GO
INSERT INTO dbo.Dim_Dealer_SCD3 (DealerCode, DealerName, CurrentAddress, PreviousAddress, AddressChangeDate)
VALUES ('D001', N'深圳珠宝经销商A', N'龙岗区布吉', N'罗湖区水贝', '2025-06-01');
GO
PRINT 'SCD Type3 表 Dim_Dealer_SCD3 已创建(仅保留上一次地址)';
GO
/* =====================================================================
四、层次模型 Hierarchical Models
业务: 珠宝产品分类树
首饰 > 戒指 > 钻戒 > 18K白金钻戒
===================================================================== */
PRINT '=== 四、层次模型 ===';
GO
/* ---------- 4.1 Adjacency List 邻接表(最常用) ---------- */
CREATE TABLE dbo.ProductCategory_AdjacencyList
(
CategoryId INT PRIMARY KEY IDENTITY(1,1),
CategoryName NVARCHAR(100) NOT NULL,
ParentId INT NULL,
SortOrder INT NOT NULL DEFAULT 0,
FOREIGN KEY (ParentId) REFERENCES dbo.ProductCategory_AdjacencyList(CategoryId)
);
GO
SET IDENTITY_INSERT dbo.ProductCategory_AdjacencyList ON;
INSERT INTO dbo.ProductCategory_AdjacencyList (CategoryId, CategoryName, ParentId, SortOrder)
VALUES
(1, N'首饰', NULL, 1),
(2, N'戒指', 1, 1),
(3, N'吊坠', 1, 2),
(4, N'手链', 1, 3),
(5, N'耳饰', 1, 4),
(6, N'钻戒', 2, 1),
(7, N'素圈戒指', 2, 2),
(8, N'宝石戒指', 2, 3),
(9, N'18K白金钻戒',6, 1),
(10, N'铂金钻戒', 6, 2),
(11, N'钻石吊坠', 3, 1),
(12, N'宝石吊坠', 3, 2),
(13, N'18K金手链', 4, 1),
(14, N'钻石耳钉', 5, 1);
SET IDENTITY_INSERT dbo.ProductCategory_AdjacencyList OFF;
GO
PRINT '邻接表 ProductCategory_AdjacencyList 已创建,14 个节点';
GO
/* ---------- 4.2 Nested Set 嵌套集(左右值模型) ---------- */
-- 对应上面同一棵树的左右值
CREATE TABLE dbo.ProductCategory_NestedSet
(
CategoryId INT PRIMARY KEY,
CategoryName NVARCHAR(100) NOT NULL,
LeftValue INT NOT NULL,
RightValue INT NOT NULL
);
GO
INSERT INTO dbo.ProductCategory_NestedSet (CategoryId, CategoryName, LeftValue, RightValue)
VALUES
(1, N'首饰', 1, 28),
(2, N'戒指', 2, 13),
(3, N'吊坠', 14, 19),
(4, N'手链', 20, 23),
(5, N'耳饰', 24, 27),
(6, N'钻戒', 3, 8),
(7, N'素圈戒指', 9, 10),
(8, N'宝石戒指', 11, 12),
(9, N'18K白金钻戒',4, 5),
(10, N'铂金钻戒', 6, 7),
(11, N'钻石吊坠', 15, 16),
(12, N'宝石吊坠', 17, 18),
(13, N'18K金手链', 21, 22),
(14, N'钻石耳钉', 25, 26);
GO
PRINT '嵌套集表 ProductCategory_NestedSet 已创建,14 个节点';
GO
/* ---------- 4.3 Closure Table 闭包表 ---------- */
CREATE TABLE dbo.ProductCategory_Closure_Main
(
CategoryId INT PRIMARY KEY,
CategoryName NVARCHAR(100) NOT NULL
);
GO
CREATE TABLE dbo.ProductCategory_Closure_Relation
(
AncestorId INT NOT NULL,
DescendantId INT NOT NULL,
Depth INT NOT NULL,
PRIMARY KEY (AncestorId, DescendantId),
FOREIGN KEY (AncestorId) REFERENCES dbo.ProductCategory_Closure_Main(CategoryId),
FOREIGN KEY (DescendantId) REFERENCES dbo.ProductCategory_Closure_Main(CategoryId)
);
GO
INSERT INTO dbo.ProductCategory_Closure_Main (CategoryId, CategoryName)
SELECT CategoryId, CategoryName FROM dbo.ProductCategory_AdjacencyList;
GO
-- 生成闭包关系(自身 + 所有祖先-后代对)
;WITH CategoryTree AS (
SELECT CategoryId, CategoryId AS AncestorId, 0 AS Depth
FROM dbo.ProductCategory_AdjacencyList
UNION ALL
SELECT ct.CategoryId, al.ParentId, ct.Depth + 1
FROM CategoryTree ct
INNER JOIN dbo.ProductCategory_AdjacencyList al ON ct.AncestorId = al.CategoryId
WHERE al.ParentId IS NOT NULL
)
INSERT INTO dbo.ProductCategory_Closure_Relation (AncestorId, DescendantId, Depth)
SELECT AncestorId, CategoryId, Depth
FROM CategoryTree;
GO
PRINT '闭包表 ProductCategory_Closure_Main / Relation 已创建';
GO
/* =====================================================================
五、层次模型性能对比查询
场景: 查询【戒指】分类下所有子节点(含自身)
===================================================================== */
PRINT '=== 五、层次模型性能对比 ===';
GO
-- 方案1: 邻接表 - 递归CTE
PRINT '--- 邻接表: 递归CTE查询戒指下所有子节点 ---';
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
GO
;WITH CategoryTree AS (
SELECT CategoryId, CategoryName, ParentId, CAST(CategoryName AS NVARCHAR(500)) AS Path, 0 AS Lvl
FROM dbo.ProductCategory_AdjacencyList
WHERE CategoryName = N'戒指'
UNION ALL
SELECT c.CategoryId, c.CategoryName, c.ParentId,
CAST(ct.Path + N'/' + c.CategoryName AS NVARCHAR(500)), ct.Lvl + 1
FROM dbo.ProductCategory_AdjacencyList c
INNER JOIN CategoryTree ct ON c.ParentId = ct.CategoryId
)
SELECT CategoryId, CategoryName, Path, Lvl FROM CategoryTree ORDER BY Path;
GO
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
GO
-- 方案2: 嵌套集 - BETWEEN 查询(无需递归)
PRINT '--- 嵌套集: BETWEEN查询戒指下所有子节点 ---';
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
GO
SELECT child.CategoryId, child.CategoryName, child.LeftValue, child.RightValue
FROM dbo.ProductCategory_NestedSet parent
INNER JOIN dbo.ProductCategory_NestedSet child
ON child.LeftValue BETWEEN parent.LeftValue AND parent.RightValue
WHERE parent.CategoryName = N'戒指'
ORDER BY child.LeftValue;
GO
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
GO
-- 方案3: 闭包表 - JOIN 查询
PRINT '--- 闭包表: JOIN查询戒指下所有子节点 ---';
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
GO
SELECT m.CategoryId, m.CategoryName, r.Depth
FROM dbo.ProductCategory_Closure_Relation r
INNER JOIN dbo.ProductCategory_Closure_Main m ON r.DescendantId = m.CategoryId
WHERE r.AncestorId = (SELECT CategoryId FROM dbo.ProductCategory_Closure_Main WHERE CategoryName = N'戒指')
ORDER BY r.Depth, m.CategoryId;
GO
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
GO
-- 性能对比汇总
PRINT '';
PRINT '=== 层次模型选型对比 ===';
PRINT '邻接表: 增删改简单,深树查询需递归,适合层级浅、变动频繁的场景';
PRINT '嵌套集: 子树查询极快(单次BETWEEN),但插入/移动节点需重排左右值,适合读多写少';
PRINT '闭包表: 查询灵活(祖先/后代/路径都快),写入需维护关系表,适合复杂树形分析';
GO
/* =====================================================================
六、Multi-Tenancy 多租户策略
场景: 珠宝集团SaaS,多个子品牌/加盟商共享系统
方案: 共享数据库 + 共享Schema + TenantId + 行级安全RLS
===================================================================== */
PRINT '=== 六、多租户 ===';
GO
CREATE TABLE dbo.Tenant
(
TenantId INT PRIMARY KEY IDENTITY(1,1),
TenantCode NVARCHAR(50) NOT NULL UNIQUE,
TenantName NVARCHAR(100) NOT NULL,
IsActive BIT DEFAULT 1
);
GO
INSERT INTO dbo.Tenant (TenantCode, TenantName, IsActive)
VALUES ('BRAND-A', N'周大福品牌', 1),
('BRAND-B', N'老凤祥品牌', 1),
('BRAND-C', N'周生生品牌', 1);
GO
CREATE TABLE dbo.Style_MultiTenant
(
StyleId INT PRIMARY KEY IDENTITY(1,1),
TenantId INT NOT NULL,
StyleCode NVARCHAR(50) NOT NULL,
StyleName NVARCHAR(100) NOT NULL,
CreateTime DATETIME DEFAULT GETDATE(),
FOREIGN KEY (TenantId) REFERENCES dbo.Tenant(TenantId),
UNIQUE (TenantId, StyleCode)
);
GO
INSERT INTO dbo.Style_MultiTenant (TenantId, StyleCode, StyleName)
VALUES (1, 'ST-A001', N'周大福传承系列戒指'),
(1, 'ST-A002', N'周大福迪士尼吊坠'),
(2, 'ST-B001', N'老凤祥龙凤手镯'),
(2, 'ST-B002', N'老凤祥黄金项链'),
(3, 'ST-C001', N'周生生全爱钻戒指');
GO
PRINT '多租户表 Tenant / Style_MultiTenant 已创建,3 个租户 5 条款式';
GO
/* ---------- 行级安全 RLS (Row-Level Security) ---------- */
-- 创建过滤函数
CREATE FUNCTION dbo.fn_TenantFilter(@TenantId INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS AccessFlag
WHERE @TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS INT);
GO
-- 创建安全策略
CREATE SECURITY POLICY dbo.Tenant_Style_Policy
ADD FILTER PREDICATE dbo.fn_TenantFilter(TenantId) ON dbo.Style_MultiTenant
WITH (STATE = ON);
GO
PRINT '行级安全策略 Tenant_Style_Policy 已启用';
GO
-- 演示: 模拟租户1登录,只能看到自己的款式
PRINT '--- 模拟租户1(周大福)查询,应只看到2条 ---';
EXEC sp_set_session_context N'TenantId', 1;
SELECT StyleId, TenantId, StyleCode, StyleName FROM dbo.Style_MultiTenant;
GO
-- 演示: 模拟租户2登录
PRINT '--- 模拟租户2(老凤祥)查询,应只看到2条 ---';
EXEC sp_set_session_context N'TenantId', 2;
SELECT StyleId, TenantId, StyleCode, StyleName FROM dbo.Style_MultiTenant;
GO
-- 清除会话上下文(恢复管理员视角需禁用策略或设置为NULL)
EXEC sp_set_session_context N'TenantId', NULL;
GO
-- 临时禁用策略以展示全量数据
ALTER SECURITY POLICY dbo.Tenant_Style_Policy WITH (STATE = OFF);
PRINT '--- 全量数据(RLS已临时关闭) ---';
SELECT StyleId, TenantId, StyleCode, StyleName FROM dbo.Style_MultiTenant;
ALTER SECURITY POLICY dbo.Tenant_Style_Policy WITH (STATE = ON);
GO
/* =====================================================================
七、综合业务查询示例
===================================================================== */
PRINT '=== 七、综合业务查询 ===';
GO
-- 1. 款式完整BOM信息(2NF主表 + Junction关联表 联合查询)
PRINT '--- 款式完整BOM信息 ---';
SELECT
s.StyleCode,
s.StyleName,
g.GemName,
m.Carat
FROM dbo.Style_2NF s
INNER JOIN dbo.Style_Gem_Map_2NF m ON s.StyleId = m.StyleId
INNER JOIN dbo.Gem_2NF g ON m.GemId = g.GemId
ORDER BY s.StyleCode, g.GemName;
GO
-- 1b. 宝石原料类型查询(3NF + Lookup Table 联合查询)
PRINT '--- 宝石原料及类型(3NF+Lookup) ---';
SELECT
gr.GemCode,
gr.WeightCarat,
gt.GemTypeName,
gt.Remark
FROM dbo.GemRaw_3NF gr
INNER JOIN dbo.GemType_Lookup_3NF gt ON gr.GemTypeCode = gt.GemTypeCode
ORDER BY gr.GemCode;
GO
-- 2. SCD Type2 历史轨迹查询
PRINT '--- 经销商D001历史变更轨迹 ---';
SELECT
DealerSurrogateKey,
DealerName,
Address,
StartDate,
ISNULL(CONVERT(VARCHAR(10), EndDate, 120), N'至今') AS EndDate,
CASE WHEN IsCurrent = 1 THEN N'当前有效' ELSE N'历史版本' END AS Status
FROM dbo.Dim_Dealer_SCD2
WHERE DealerCode = 'D001'
ORDER BY StartDate;
GO
-- 3. 反范式库存汇总
PRINT '--- 库存汇总(反范式表直接查询,无需JOIN) ---';
SELECT
StyleName,
GemName,
StockQty,
UnitCost,
StockQty * UnitCost AS TotalValue
FROM dbo.Inventory_Denormalized
ORDER BY TotalValue DESC;
GO
-- 4. 5NF 三方授权查询: 哪些经销商被授权销售18K白金的某款式
PRINT '--- 5NF三方授权查询 ---';
SELECT
d.DealerName,
s.StyleName,
m.MaterialName,
CASE WHEN dsm.IsAuthorized = 1 THEN N'已授权' ELSE N'未授权' END AS AuthStatus
FROM dbo.Dealer_Style_Material_5NF dsm
INNER JOIN dbo.Dealer_5NF d ON dsm.DealerId = d.DealerId
INNER JOIN dbo.Style_2NF s ON dsm.StyleId = s.StyleId
INNER JOIN dbo.Material_Lookup m ON dsm.MaterialId = m.MaterialId
ORDER BY d.DealerName, s.StyleName;
GO
/* =====================================================================
八、建模模式选型速查表
===================================================================== */
PRINT '';
PRINT '========================================================';
PRINT ' 珠宝行业数据建模模式选型速查表';
PRINT '========================================================';
PRINT ' 模式 适用场景 珠宝行业示例';
PRINT ' ------------------ ------------------------------------ ------------------------';
PRINT ' 1NF 所有关系表基础 宝石清单每行一颗';
PRINT ' 2NF 复合主键表 款式-宝石关联表';
PRINT ' 3NF OLTP业务系统主力 宝石类型独立Lookup';
PRINT ' BCNF 决定因子非候选键的复杂关系 工序-工艺师-车间';
PRINT ' 4NF 存在独立多值依赖 款式的宝石和工艺分开';
PRINT ' 5NF 三方及以上不可拆关系 经销商-款式-材质授权';
PRINT ' 反范式 报表/汇总/读多写少 库存汇总冗余款式名';
PRINT ' Lookup Table 静态字典/枚举 宝石类型、材质、状态';
PRINT ' Junction Table 多对多关系 款式-宝石、款式-工序';
PRINT ' SCD Type1 不需要历史的维度 内部员工部门';
PRINT ' SCD Type2 需要完整历史轨迹(推荐) 经销商、供应商维度';
PRINT ' SCD Type3 只需保留上一次值 客户最近一次地址';
PRINT ' 邻接表 层级浅、变动频繁 产品分类(3-4层)';
PRINT ' 嵌套集 读多写少、子树查询频繁 珠宝目录导航';
PRINT ' 闭包表 复杂树形分析、路径查询 供应链层级追溯';
PRINT ' 多租户(共享Schema) SaaS多品牌/加盟商(推荐) 珠宝集团ERP';
PRINT ' 多租户(独立库) 高保密大客户/合规要求 银行贵金属部';
PRINT '========================================================';
GO
PRINT '';
PRINT '=== 脚本执行完毕,所有表、数据、查询均已就绪 ===';
GO