珠宝行业的数据链路长、业务形态复杂:从宝石原料采购、款式 BOM 设计、加工工单流转,到成品库存、经销商分销与门店销售,每一环都沉淀着大量结构化数据。然而,许多企业在建模时往往陷入两难------业务库要么过度冗余导致更新异常,要么过度范式化拖慢查询;数据仓库又缺乏从 OLTP 到 DW 的完整衔接,维度拉链、增量抽取、数据质量校验等环节常常靠手工拼凑,难以支撑规模化经营分析。本文面向数据库工程师与架构师,以 MySQL 9.0 为底座,从范式建模、反范式设计、SCD 缓慢变化维度、层次模型、多租户、事务与锁,到星型/雪花数据仓库、ETL 增量抽取、数据质量校验、读写分离与 BI 报表,系统梳理一套从 OLTP 到 DW 的完整建模方法论,并附上可直接运行的完整 MySQL 脚本,帮助读者在真实项目中快速落地。
一、文章大纲
本文围绕珠宝行业数据建模的完整实践展开,覆盖从 OLTP 业务库到数据仓库的建模全流程。全文共分为以下章节:
- 1. 范式建模(1NF ~ 5NF):从列原子性到投影-连接范式,逐层消除数据冗余与异常依赖。
- 2. 反范式设计与触发器同步:通过冗余字段减少 JOIN,并用触发器保证数据一致性。
- 3. SCD 缓慢变化维度:对比 Type1 覆盖、Type2 拉链、Type3 保留上一版本三种策略。
- 4. 层次模型:邻接表、嵌套集、闭包表三种树形结构建模与适用场景。
- 5. 多租户架构:共享库 + 共享 Schema + TenantId + 行级安全视图。
- 6. 数据校验、审计日志与乐观锁:CHECK 约束、审计触发器、行版本控制。
- 7. 业务主表设计:销售订单、加工工单、客户等核心业务表。
- 8. 批量测试数据:为各业务表生成可复现的演示数据。
- 9. 存储过程:SCD2 拉链、分类树移动、业务补偿等核心逻辑。
- 10. 索引优化与分区表:提升查询性能与大数据量管理能力。
- 11. 慢查询配置与优化器 Hint:定位并优化慢 SQL。
- 12. 事务隔离级别与锁分析:库存并发扣减场景下的锁与隔离实践。
- 13. 数据仓库建模:星型模型与雪花模型的设计与对比。
- 14. ETL 存储过程:OLTP 到 DW 的增量抽取与 SCD2 自动拉链。
- 15. ETL 数据质量校验:脏数据捕获与错误日志记录。
- 16. 读写分离与主从延迟监控:架构设计与延迟告警。
- 17. 数据库安全:账号权限、行级安全视图、数据脱敏与审计加固。
- 18. 运维脚本:备份、binlog、碎片优化与统计信息更新。
- 19. BI 报表 SQL:销售、库存周转、工单良率、加盟商分析。
- 20. 层次模型性能对比:通过 EXPLAIN 对比三种树形查询方案。
- 附录:DDD 领域模型说明:聚合、实体、值对象与 ER 关系梳理。
以下是代码:
sql
/* ============================================================================
珠宝行业数据建模完整脚本 (MySQL 9.0 / InnoDB / utf8mb4)
============================================================================
数据库: JewelryERP (OLTP业务库) + JewelryDW (数据仓库)
业务场景: 宝石原料 -> 款式BOM -> 加工工单 -> 成品库存 -> 经销商 -> 销售
覆盖模块:
01. 范式建模 1NF ~ 5NF
02. 反范式 Denormalization + 触发器同步
03. SCD 缓慢变化维度 (Type1/2/3)
04. 层次模型 (邻接表 / 嵌套集 / 闭包表)
05. 多租户 Multi-Tenancy
06. 数据校验约束 + 审计日志 + 行版本控制(乐观锁)
07. 业务主表 (销售订单 / 加工工单 / 客户)
08. 批量测试数据
09. 存储过程 (SCD2拉链 / 分类树移动 / 业务补偿)
10. 索引优化 + 分区表
11. 慢查询配置 + 优化器 Hint
12. 事务隔离级别 + 锁分析 (库存并发扣减)
13. 数据仓库 (星型模型 / 雪花模型)
14. ETL 存储过程 (OLTP -> DW 增量抽取, SCD2自动拉链)
15. ETL 数据质量校验 (脏数据捕获)
16. 读写分离 + 主从延迟监控
17. 数据库安全 (账号权限 / 行级安全视图 / 数据脱敏 / 审计加固)
18. 运维脚本 (备份 / binlog / 碎片优化 / 统计信息)
19. BI 报表 SQL (销售 / 库存周转 / 工单良率 / 加盟商分析)
20. 层次模型性能对比 (EXPLAIN)
执行方式: mysql -u root -p < JewelryERP_Full_MySQL9.0.sql
或在 Navicat / MySQL Workbench 中全选执行
============================================================================ */
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
/* ============================================================================
第0部分: 数据库初始化
============================================================================ */
DROP DATABASE IF EXISTS JewelryERP;
CREATE DATABASE JewelryERP DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE JewelryERP;
/* ============================================================================
第1部分: 范式建模 Normalization (1NF ~ 5NF)
============================================================================ */
-- ---------- 1NF: 列原子性,单元格不可拆分,无重复多值 ----------
-- 反例(不执行): GemMaterials 字段存 '钻石,红宝石' 多值,违反1NF
-- 正例: 每个宝石一行
CREATE TABLE Style_Gem_1NF (
Id INT AUTO_INCREMENT PRIMARY KEY,
StyleId INT NOT NULL,
StyleName VARCHAR(100) NOT NULL,
GemName VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='1NF示例: 款式-宝石明细,每行一颗宝石';
INSERT INTO Style_Gem_1NF (StyleId, StyleName, GemName) VALUES
(1, '铂金钻戒', '钻石'),
(1, '铂金钻戒', '红宝石'),
(2, '黄金吊坠', '蓝宝石');
-- ---------- 2NF: 消除部分函数依赖,复合主键时非键字段完全依赖整个主键 ----------
-- 先建主表,再建关联表(外键依赖顺序)
CREATE TABLE Style_2NF (
StyleId INT PRIMARY KEY,
StyleName VARCHAR(100) NOT NULL,
StyleCode VARCHAR(50) NOT NULL UNIQUE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='2NF主表: 珠宝款式';
CREATE TABLE Gem_2NF (
GemId INT PRIMARY KEY,
GemName VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='2NF主表: 宝石名称字典';
CREATE TABLE Style_Gem_Map_2NF (
StyleId INT NOT NULL,
GemId INT NOT NULL,
Carat DECIMAL(10,3) NOT NULL COMMENT '单颗宝石克拉数',
PRIMARY KEY (StyleId, GemId),
FOREIGN KEY (StyleId) REFERENCES Style_2NF(StyleId),
FOREIGN KEY (GemId) REFERENCES Gem_2NF(GemId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='2NF关联表: 款式BOM宝石用料';
-- ---------- 3NF: 消除传递依赖 + Lookup Table 查找表 ----------
-- GemTypeName 依赖 GemTypeCode(非主键),需拆出查找表
CREATE TABLE GemType_Lookup_3NF (
GemTypeCode VARCHAR(20) PRIMARY KEY,
GemTypeName VARCHAR(50) NOT NULL,
Remark VARCHAR(200) NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='3NF Lookup表: 宝石类型字典';
CREATE TABLE GemRaw_3NF (
GemId INT PRIMARY KEY,
GemCode VARCHAR(50) NOT NULL UNIQUE,
GemTypeCode VARCHAR(20) NOT NULL,
WeightCarat DECIMAL(10,3) NOT NULL COMMENT '宝石原石克拉',
FOREIGN KEY (GemTypeCode) REFERENCES GemType_Lookup_3NF(GemTypeCode)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='3NF主表: 宝石原料';
-- ---------- BCNF: 每个决定因子必须是候选键 ----------
-- 业务规则: 一个工序固定由一位工艺师负责 -> ProcessCode 决定 CraftsmanId
CREATE TABLE Process_BCNF (
ProcessCode VARCHAR(20) PRIMARY KEY,
ProcessName VARCHAR(100) NOT NULL COMMENT '加工工序: 起版/镶石/抛光',
CraftsmanId INT NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BCNF: 加工工序,ProcessCode为唯一决定因子';
CREATE TABLE Process_Workshop_BCNF (
ProcessCode VARCHAR(20),
WorkshopId INT,
PRIMARY KEY (ProcessCode, WorkshopId),
FOREIGN KEY (ProcessCode) REFERENCES Process_BCNF(ProcessCode)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='BCNF: 工序-车间多对多';
-- ---------- 4NF: 消除独立多值依赖 + Junction Table 关联表 ----------
-- 款式可以有多个宝石、多个工艺;宝石和工艺互相独立,拆成两张关联表
CREATE TABLE Style_Gem_Junction_4NF (
StyleId INT,
GemId INT,
PRIMARY KEY (StyleId, GemId),
FOREIGN KEY (StyleId) REFERENCES Style_2NF(StyleId),
FOREIGN KEY (GemId) REFERENCES Gem_2NF(GemId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='4NF Junction表: 款式-宝石多对多';
CREATE TABLE Style_Process_Junction_4NF (
StyleId INT,
ProcessCode VARCHAR(20),
PRIMARY KEY (StyleId, ProcessCode),
FOREIGN KEY (StyleId) REFERENCES Style_2NF(StyleId),
FOREIGN KEY (ProcessCode) REFERENCES Process_BCNF(ProcessCode)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='4NF Junction表: 款式-加工工序多对多';
-- ---------- 5NF: 投影-连接范式,三方关系不可无损拆分 ----------
-- 经销商-款式-材质 三方授权: 只能整体存储,不能拆成两两关系
CREATE TABLE Material_Lookup (
MaterialId INT PRIMARY KEY,
MaterialName VARCHAR(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='5NF Lookup表: 材质字典';
CREATE TABLE Dealer_5NF (
DealerId INT PRIMARY KEY,
DealerName VARCHAR(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='5NF主表: 经销商';
CREATE TABLE Dealer_Style_Material_5NF (
DealerId INT NOT NULL,
StyleId INT NOT NULL,
MaterialId INT NOT NULL,
IsAuthorized BOOLEAN NOT NULL DEFAULT FALSE,
PRIMARY KEY (DealerId, StyleId, MaterialId),
FOREIGN KEY (DealerId) REFERENCES Dealer_5NF(DealerId),
FOREIGN KEY (StyleId) REFERENCES Style_2NF(StyleId),
FOREIGN KEY (MaterialId) REFERENCES Material_Lookup(MaterialId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='5NF三方关系: 经销商-款式-材质销售授权';
/* ============================================================================
第2部分: 反范式 Denormalization + 触发器同步
场景: 库存汇总表冗余款式名/宝石名,减少JOIN,牺牲更新一致性换查询速度
============================================================================ */
CREATE TABLE Inventory_Denormalized (
InventoryId INT AUTO_INCREMENT PRIMARY KEY,
StyleId INT NOT NULL,
StyleName VARCHAR(100) NOT NULL COMMENT '冗余字段(反范式): 款式名称',
GemId INT NOT NULL,
GemName VARCHAR(50) NOT NULL COMMENT '冗余字段(反范式): 宝石名称',
StockQty INT NOT NULL,
UnitCost DECIMAL(18,2) NOT NULL,
UpdateTime DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='反范式表: 成品库存汇总,冗余款式名/宝石名';
-- 触发器: 主表款式名变更时同步冗余字段
DELIMITER //
CREATE TRIGGER trg_Style_Update_Denorm
AFTER UPDATE ON Style_2NF
FOR EACH ROW
BEGIN
IF OLD.StyleName <> NEW.StyleName THEN
UPDATE Inventory_Denormalized
SET StyleName = NEW.StyleName, UpdateTime = NOW()
WHERE StyleId = NEW.StyleId;
END IF;
END //
DELIMITER ;
/* ============================================================================
第3部分: SCD 缓慢变化维度 (珠宝经销商维度)
============================================================================ */
-- ---------- SCD Type1: 直接覆盖,丢失历史 ----------
CREATE TABLE Dim_Dealer_SCD1 (
DealerKey INT AUTO_INCREMENT PRIMARY KEY,
DealerCode VARCHAR(30) NOT NULL UNIQUE,
DealerName VARCHAR(100) NOT NULL,
Address VARCHAR(200) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SCD Type1: 经销商维度,直接覆盖';
-- ---------- SCD Type2: 拉链表,新增行+生效失效日期,保留全量历史(DW最常用) ----------
CREATE TABLE Dim_Dealer_SCD2 (
DealerSurrogateKey INT AUTO_INCREMENT PRIMARY KEY COMMENT '代理键',
DealerCode VARCHAR(30) NOT NULL,
DealerName VARCHAR(100) NOT NULL,
Address VARCHAR(200) NOT NULL,
StartDate DATETIME NOT NULL,
EndDate DATETIME NULL COMMENT 'NULL=当前有效',
IsCurrent BOOLEAN NOT NULL DEFAULT TRUE,
INDEX idx_dealer_code (DealerCode),
INDEX idx_dealer_current (DealerCode, IsCurrent)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SCD Type2: 经销商维度拉链表,保留历史版本';
-- ---------- SCD Type3: 仅保存上一版本地址,新增历史字段 ----------
CREATE TABLE Dim_Dealer_SCD3 (
DealerKey INT AUTO_INCREMENT PRIMARY KEY,
DealerCode VARCHAR(30) NOT NULL UNIQUE,
DealerName VARCHAR(100) NOT NULL,
CurrentAddress VARCHAR(200) NOT NULL,
PreviousAddress VARCHAR(200) NULL,
AddressChangeDate DATETIME NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='SCD Type3: 经销商维度,仅保留上一次地址';
/* ============================================================================
第4部分: 层次模型 Hierarchical Models (珠宝产品分类树)
树结构: 首饰 > 戒指/吊坠/耳饰/手链/项链/手镯 > 钻戒/素圈/彩宝...
============================================================================ */
-- ---------- 4.1 Adjacency List 邻接表 (父ID模式,最常用) ----------
CREATE TABLE ProductCategory_AdjacencyList (
CategoryId INT AUTO_INCREMENT PRIMARY KEY,
CategoryName VARCHAR(100) NOT NULL,
ParentId INT NULL,
SortOrder INT NOT NULL DEFAULT 0,
INDEX idx_parent (ParentId),
FOREIGN KEY (ParentId) REFERENCES ProductCategory_AdjacencyList(CategoryId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='层次模型-邻接表: 产品分类父子关系';
-- ---------- 4.2 Nested Set 嵌套集 (左右值模型,适合批量子树查询) ----------
CREATE TABLE ProductCategory_NestedSet (
CategoryId INT AUTO_INCREMENT PRIMARY KEY,
CategoryName VARCHAR(100) NOT NULL,
LeftValue INT NOT NULL,
RightValue INT NOT NULL,
INDEX idx_lr (LeftValue, RightValue)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='层次模型-嵌套集: 左右值模型,子树查询极快';
-- ---------- 4.3 Closure Table 闭包表 (独立存储祖先-后代关系,复杂树形推荐) ----------
CREATE TABLE ProductCategory_Closure_Main (
CategoryId INT AUTO_INCREMENT PRIMARY KEY,
CategoryName VARCHAR(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='层次模型-闭包表主表: 分类节点';
CREATE TABLE ProductCategory_Closure_Relation (
AncestorId INT NOT NULL,
DescendantId INT NOT NULL,
Depth INT NOT NULL,
PRIMARY KEY (AncestorId, DescendantId),
INDEX idx_desc (DescendantId),
FOREIGN KEY (AncestorId) REFERENCES ProductCategory_Closure_Main(CategoryId),
FOREIGN KEY (DescendantId) REFERENCES ProductCategory_Closure_Main(CategoryId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='层次模型-闭包表关系: 全部祖先-后代对';
/* ============================================================================
第5部分: 多租户 Multi-Tenancy (珠宝集团,多个子品牌/加盟商)
方案: 共享数据库 + 共享Schema + TenantId + 行级安全视图
============================================================================ */
CREATE TABLE Tenant (
TenantId INT AUTO_INCREMENT PRIMARY KEY,
TenantCode VARCHAR(50) NOT NULL UNIQUE,
TenantName VARCHAR(100) NOT NULL,
IsActive BOOLEAN DEFAULT TRUE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='多租户: 加盟商/品牌租户';
CREATE TABLE Style_MultiTenant (
StyleId INT AUTO_INCREMENT PRIMARY KEY,
TenantId INT NOT NULL,
StyleCode VARCHAR(50) NOT NULL,
StyleName VARCHAR(100) NOT NULL,
CreateTime DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (TenantId) REFERENCES Tenant(TenantId),
UNIQUE KEY uk_tenant_stylecode (TenantId, StyleCode),
INDEX idx_tenant (TenantId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='多租户: 租户私有款式';
/* ============================================================================
第6部分: 数据校验约束 + 审计日志 + 行版本控制(乐观锁)
============================================================================ */
-- ---------- 6.1 CHECK约束 (MySQL 8.0+ 原生支持) ----------
ALTER TABLE GemRaw_3NF
ADD CONSTRAINT chk_gem_carat CHECK (WeightCarat > 0),
ADD CONSTRAINT chk_gem_code CHECK (GemCode <> '');
ALTER TABLE Inventory_Denormalized
ADD CONSTRAINT chk_stock_qty CHECK (StockQty >= 0),
ADD CONSTRAINT chk_cost CHECK (UnitCost >= 0);
-- 行版本号(乐观锁): 防止并发覆盖
ALTER TABLE Inventory_Denormalized
ADD COLUMN RowVersion INT NOT NULL DEFAULT 1 COMMENT '行版本号,乐观锁';
-- ---------- 6.2 审计日志表 (全业务变更审计: 增删改,记录旧值新值JSON) ----------
CREATE TABLE AuditLog (
AuditId BIGINT AUTO_INCREMENT PRIMARY KEY,
TableName VARCHAR(64) NOT NULL COMMENT '被操作表名',
OperateType ENUM('INSERT','UPDATE','DELETE') NOT NULL,
RecordId VARCHAR(64) NOT NULL COMMENT '业务主键ID',
OldValue JSON NULL COMMENT '修改前JSON',
NewValue JSON NULL COMMENT '修改后JSON',
OperatorAccount VARCHAR(64) NOT NULL COMMENT '操作账号',
OperateIP VARCHAR(45) NULL,
OperateTime DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_audit_table (TableName, OperateTime)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审计日志: 全业务表增删改记录';
-- 审计触发器: 款式表 Style_2NF
DELIMITER //
CREATE TRIGGER trg_Style_Insert_Audit
AFTER INSERT ON Style_2NF FOR EACH ROW
BEGIN
INSERT INTO AuditLog(TableName, OperateType, RecordId, NewValue, OperatorAccount)
VALUES('Style_2NF','INSERT',CAST(NEW.StyleId AS CHAR),
JSON_OBJECT('StyleId',NEW.StyleId,'StyleName',NEW.StyleName,'StyleCode',NEW.StyleCode),
CURRENT_USER());
END //
CREATE TRIGGER trg_Style_Update_Audit
AFTER UPDATE ON Style_2NF FOR EACH ROW
BEGIN
INSERT INTO AuditLog(TableName, OperateType, RecordId, OldValue, NewValue, OperatorAccount)
VALUES('Style_2NF','UPDATE',CAST(NEW.StyleId AS CHAR),
JSON_OBJECT('StyleId',OLD.StyleId,'StyleName',OLD.StyleName,'StyleCode',OLD.StyleCode),
JSON_OBJECT('StyleId',NEW.StyleId,'StyleName',NEW.StyleName,'StyleCode',NEW.StyleCode),
CURRENT_USER());
END //
CREATE TRIGGER trg_Style_Delete_Audit
AFTER DELETE ON Style_2NF FOR EACH ROW
BEGIN
INSERT INTO AuditLog(TableName, OperateType, RecordId, OldValue, OperatorAccount)
VALUES('Style_2NF','DELETE',CAST(OLD.StyleId AS CHAR),
JSON_OBJECT('StyleId',OLD.StyleId,'StyleName',OLD.StyleName,'StyleCode',OLD.StyleCode),
CURRENT_USER());
END //
DELIMITER ;
-- 审计归档表
CREATE TABLE AuditLog_Archive LIKE AuditLog;
-- ---------- 6.3 业务补偿日志表 (订单取消/工单返工/库存回滚) ----------
CREATE TABLE BusinessCompensationLog (
CompId BIGINT AUTO_INCREMENT PRIMARY KEY,
BizType ENUM('OrderCancel','WorkOrderRework','InventoryRollback') NOT NULL,
BizKey VARCHAR(64) NOT NULL COMMENT '业务主键: 订单号/工单号',
BeforeState JSON NULL,
AfterState JSON NULL,
CompQty INT NOT NULL DEFAULT 0 COMMENT '补偿数量(库存增减)',
CompRemark VARCHAR(500) NULL,
Operator VARCHAR(64) NOT NULL,
CompTime DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_bizkey (BizKey),
INDEX idx_biztype (BizType, CompTime)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务补偿日志: 异常处理流水';
/* ============================================================================
第7部分: 业务主表 (销售订单 / 加工工单 / 客户)
============================================================================ */
CREATE TABLE SalesOrder (
OrderId INT AUTO_INCREMENT PRIMARY KEY,
TenantId INT NOT NULL,
OrderNo VARCHAR(50) NOT NULL UNIQUE COMMENT '销售订单编号',
DealerId INT NOT NULL,
OrderAmount DECIMAL(18,2) NOT NULL,
OrderStatus ENUM('Draft','Confirmed','Producing','Shipped','Completed','Cancelled') NOT NULL DEFAULT 'Draft',
CreateTime DATETIME DEFAULT CURRENT_TIMESTAMP,
UpdateTime DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (TenantId) REFERENCES Tenant(TenantId),
CONSTRAINT chk_order_amount CHECK (OrderAmount >= 0),
INDEX idx_tenant_status (TenantId, OrderStatus),
INDEX idx_createtime (CreateTime)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销售订单';
CREATE TABLE WorkOrder (
WorkOrderId INT AUTO_INCREMENT PRIMARY KEY,
StyleId INT NOT NULL,
OrderId INT NULL,
WorkNo VARCHAR(50) UNIQUE NOT NULL COMMENT '加工工单号',
WorkStatus ENUM('Pending','Working','QC','Finished','Reject') NOT NULL DEFAULT 'Pending',
PlanStartDate DATE,
PlanEndDate DATE,
ActualEndDate DATE NULL,
CreateTime DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (StyleId) REFERENCES Style_2NF(StyleId),
FOREIGN KEY (OrderId) REFERENCES SalesOrder(OrderId),
INDEX idx_style (StyleId),
INDEX idx_order (OrderId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='加工工单';
CREATE TABLE Customer (
CustId INT AUTO_INCREMENT PRIMARY KEY,
TenantId INT NOT NULL,
CustName VARCHAR(50),
Mobile VARCHAR(20) NOT NULL COMMENT '客户手机号(敏感字段)',
GemCertNo VARCHAR(64) NULL COMMENT '宝石GIA证书编号(敏感字段)',
INDEX idx_tenant (TenantId)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客户表(含敏感字段,需脱敏)';
-- 数据脱敏视图: 普通账号只能看到脱敏后的手机号/证书号
CREATE VIEW vw_Customer_Masked AS
SELECT
CustId, TenantId, CustName,
CONCAT(LEFT(Mobile,3),'',RIGHT(Mobile,4)) AS Mobile,
CONCAT(LEFT(GemCertNo,6),'**',RIGHT(GemCertNo,3)) AS GemCertNo
FROM Customer;
/* ============================================================================
第8部分: 批量测试数据
============================================================================ */
-- 8.1 基础字典数据
INSERT INTO GemType_Lookup_3NF (GemTypeCode, GemTypeName, Remark) VALUES
('D','钻石','天然钻石GIA证书'),
('R','红宝石','缅甸红宝石'),
('S','蓝宝石','斯里兰卡蓝宝石'),
('E','祖母绿','哥伦比亚祖母绿'),
('P','珍珠','南洋白珍珠');
INSERT INTO GemRaw_3NF (GemId, GemCode, GemTypeCode, WeightCarat) VALUES
(1,'D202609001','D',0.52),
(2,'D202609002','D',1.01),
(3,'R202609001','R',2.10),
(4,'S202609001','S',1.55),
(5,'E202609001','E',0.88),
(6,'P202609001','P',8.20);
INSERT INTO Gem_2NF (GemId, GemName) VALUES
(1,'钻石'),(2,'红宝石'),(3,'蓝宝石'),(4,'祖母绿'),(5,'珍珠');
INSERT INTO Style_2NF (StyleId, StyleName, StyleCode) VALUES
(1,'18K白金经典钻戒','ST001'),
(2,'足金素圈戒指','ST002'),
(3,'红宝石吊坠','ST003'),
(4,'蓝宝石耳钉','ST004'),
(5,'珍珠手链','ST005');
INSERT INTO Style_Gem_Map_2NF (StyleId, GemId, Carat) VALUES
(1,1,0.52),(1,2,1.01),(3,3,2.10),(4,4,1.55),(5,5,8.20);
INSERT INTO Process_BCNF (ProcessCode, ProcessName, CraftsmanId) VALUES
('P01','起版',101),('P02','镶石',102),('P03','抛光',103),('P04','铸造',104);
INSERT INTO Material_Lookup (MaterialId, MaterialName) VALUES
(1,'18K白金'),(2,'18K黄金'),(3,'铂金PT950'),(4,'足金999');
INSERT INTO Dealer_5NF (DealerId, DealerName) VALUES
(1,'深圳水贝经销商A'),(2,'广州荔湾经销商B'),(3,'上海豫园经销商C');
INSERT INTO Dealer_Style_Material_5NF (DealerId, StyleId, MaterialId, IsAuthorized) VALUES
(1,1,1,TRUE),(1,1,3,TRUE),(1,2,4,TRUE),(2,1,1,TRUE),(3,3,2,TRUE);
-- 8.2 库存数据
INSERT INTO Inventory_Denormalized (StyleId, StyleName, GemId, GemName, StockQty, UnitCost) VALUES
(1,'18K白金经典钻戒',1,'钻石',50,8800.00),
(1,'18K白金经典钻戒',2,'红宝石',30,12000.00),
(2,'足金素圈戒指',3,'蓝宝石',80,3500.00),
(3,'红宝石吊坠',3,'蓝宝石',40,6600.00),
(4,'蓝宝石耳钉',4,'祖母绿',60,4200.00),
(5,'珍珠手链',5,'珍珠',100,2800.00);
-- 8.3 SCD经销商数据
INSERT INTO Dim_Dealer_SCD1 (DealerCode, DealerName, Address) VALUES
('D001','深圳珠宝经销商A','罗湖区水贝');
INSERT INTO Dim_Dealer_SCD2 (DealerCode, DealerName, Address, StartDate, EndDate, IsCurrent) VALUES
('D001','深圳珠宝经销商A','罗湖区水贝','2024-01-01','2025-06-01',FALSE),
('D001','深圳珠宝经销商A','龙岗区布吉','2025-06-01','2026-01-01',FALSE),
('D001','深圳珠宝经销商A(旗舰店)','福田区CBD','2026-01-01',NULL,TRUE);
INSERT INTO Dim_Dealer_SCD3 (DealerCode, DealerName, CurrentAddress, PreviousAddress, AddressChangeDate) VALUES
('D001','深圳珠宝经销商A','龙岗区布吉','罗湖区水贝','2025-06-01');
-- 8.4 层次模型数据 (同一棵14节点分类树)
-- 邻接表
INSERT INTO ProductCategory_AdjacencyList (CategoryId, CategoryName, ParentId, SortOrder) VALUES
(1,'首饰',NULL,1),(2,'戒指',1,1),(3,'吊坠',1,2),(4,'手链',1,3),
(5,'耳饰',1,4),(6,'项链',1,5),(7,'手镯',1,6),
(8,'钻戒',2,1),(9,'素圈戒指',2,2),(10,'彩宝戒指',2,3),
(11,'钻石吊坠',3,1),(12,'珍珠吊坠',3,2),
(13,'18K金手链',4,1),(14,'钻石耳钉',5,1);
-- 嵌套集 (深度优先遍历计算左右值)
INSERT INTO ProductCategory_NestedSet (CategoryId, CategoryName, LeftValue, RightValue) VALUES
(1,'首饰',1,28),(2,'戒指',2,13),(3,'吊坠',14,19),(4,'手链',20,23),
(5,'耳饰',24,27),(6,'项链',28,29),(7,'手镯',30,31),
(8,'钻戒',3,8),(9,'素圈戒指',9,10),(10,'彩宝戒指',11,12),
(11,'钻石吊坠',15,16),(12,'珍珠吊坠',17,18),
(13,'18K金手链',21,22),(14,'钻石耳钉',25,26);
-- 闭包表主表
INSERT INTO ProductCategory_Closure_Main (CategoryId, CategoryName)
SELECT CategoryId, CategoryName FROM ProductCategory_AdjacencyList;
-- 闭包表关系 (用递归CTE生成全部祖先-后代对)
INSERT INTO ProductCategory_Closure_Relation (AncestorId, DescendantId, Depth)
WITH RECURSIVE CategoryTree AS (
SELECT CategoryId, CategoryId AS AncestorId, 0 AS Depth
FROM ProductCategory_AdjacencyList
UNION ALL
SELECT ct.CategoryId, al.ParentId, ct.Depth + 1
FROM CategoryTree ct
INNER JOIN ProductCategory_AdjacencyList al ON ct.AncestorId = al.CategoryId
WHERE al.ParentId IS NOT NULL
)
SELECT AncestorId, CategoryId, Depth FROM CategoryTree;
-- 8.5 多租户数据
INSERT INTO Tenant (TenantCode, TenantName, IsActive) VALUES
('T01','水贝直营品牌',TRUE),('T02','加盟商A',TRUE),('T03','加盟商B',TRUE);
INSERT INTO Style_MultiTenant (TenantId, StyleCode, StyleName) VALUES
(1,'S001','18K金钻戒'),(1,'S002','足金素圈'),
(2,'S001','加盟商A专属款'),(2,'S002','银饰吊坠'),
(3,'S001','加盟商B铂金款');
-- 8.6 销售订单 + 加工工单
INSERT INTO SalesOrder (TenantId, OrderNo, DealerId, OrderAmount, OrderStatus) VALUES
(1,'ORD2026090001',1,16800.00,'Confirmed'),
(1,'ORD2026090002',1,8900.00,'Producing'),
(1,'ORD2026090003',2,25600.00,'Shipped'),
(2,'ORD2026090004',1,5200.00,'Completed'),
(3,'ORD2026090005',3,12000.00,'Draft');
INSERT INTO WorkOrder (StyleId, OrderId, WorkNo, WorkStatus, PlanStartDate, PlanEndDate) VALUES
(1,1,'WO202609001','Working','2026-09-01','2026-09-15'),
(2,2,'WO202609002','Pending','2026-09-05','2026-09-20'),
(3,3,'WO202609003','QC','2026-09-03','2026-09-10'),
(4,4,'WO202609004','Finished','2026-08-20','2026-09-01'),
(1,5,'WO202609005','Pending','2026-09-10','2026-09-25');
-- 8.7 客户数据
INSERT INTO Customer (TenantId, CustName, Mobile, GemCertNo) VALUES
(1,'张三','13812345678','GIA20260001'),
(1,'李四','13987654321','GIA20260002'),
(2,'王五','13711112222','GIA20260003');
-- 8.8 4NF关联表数据
INSERT INTO Style_Gem_Junction_4NF (StyleId, GemId) VALUES
(1,1),(1,2),(2,3),(3,3),(4,4),(5,5);
INSERT INTO Style_Process_Junction_4NF (StyleId, ProcessCode) VALUES
(1,'P01'),(1,'P02'),(1,'P03'),(2,'P04'),(2,'P03'),(3,'P01'),(3,'P02');
SET FOREIGN_KEY_CHECKS = 1;
/* ============================================================================
第9部分: 存储过程
============================================================================ */
-- ---------- 9.1 批量生成邻接表子分类测试数据 ----------
DELIMITER //
CREATE PROCEDURE sp_GenerateAdjCategoryData(IN p_parentId INT, IN p_count INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE v_name VARCHAR(128);
WHILE i <= p_count DO
SET v_name = CONCAT('子分类_',p_parentId,'_',i);
INSERT INTO ProductCategory_AdjacencyList(CategoryName, ParentId, SortOrder)
VALUES (v_name, p_parentId, i);
SET i = i + 1;
END WHILE;
END //
-- ---------- 9.2 SCD2 自动拉链存储过程 (经销商维度) ----------
CREATE PROCEDURE sp_SCD2_UpdateDealer(
IN p_DealerCode VARCHAR(30),
IN p_DealerName VARCHAR(100),
IN p_NewAddress VARCHAR(200)
)
BEGIN
DECLARE v_now DATETIME;
SET v_now = NOW();
START TRANSACTION;
-- 旧记录标记过期
UPDATE Dim_Dealer_SCD2
SET EndDate = v_now, IsCurrent = FALSE
WHERE DealerCode = p_DealerCode AND IsCurrent = TRUE;
-- 插入新版本
INSERT INTO Dim_Dealer_SCD2(DealerCode, DealerName, Address, StartDate, EndDate, IsCurrent)
VALUES (p_DealerCode, p_DealerName, p_NewAddress, v_now, NULL, TRUE);
COMMIT;
END //
-- ---------- 9.3 邻接表分类树移动节点 (防循环引用) ----------
CREATE PROCEDURE sp_MoveCategoryNode(IN p_CategoryId INT, IN p_NewParentId INT)
BEGIN
DECLARE v_cnt INT;
-- 防止把父节点移动到自己的子节点(循环引用)
SELECT COUNT(*) INTO v_cnt
FROM ProductCategory_AdjacencyList AS c
WHERE c.CategoryId = p_NewParentId
AND EXISTS (
WITH RECURSIVE tree AS (
SELECT CategoryId FROM ProductCategory_AdjacencyList WHERE CategoryId = p_CategoryId
UNION ALL
SELECT child.CategoryId FROM ProductCategory_AdjacencyList child
INNER JOIN tree ON child.ParentId = tree.CategoryId
)
SELECT 1 FROM tree WHERE tree.CategoryId = p_NewParentId
);
IF v_cnt > 0 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '不能将节点移动到自身子节点,循环引用';
END IF;
UPDATE ProductCategory_AdjacencyList SET ParentId = p_NewParentId WHERE CategoryId = p_CategoryId;
END //
-- ---------- 9.4 订单取消 (库存回滚 + 工单终止 + 补偿日志) ----------
CREATE PROCEDURE sp_CancelSalesOrder(
IN p_OrderNo VARCHAR(50),
IN p_Remark VARCHAR(500),
IN p_Operator VARCHAR(64)
)
BEGIN
DECLARE v_OrderId INT;
DECLARE v_OldStatus ENUM('Draft','Confirmed','Producing','Shipped','Completed','Cancelled');
DECLARE v_InvId INT;
DECLARE v_InvQty INT;
DECLARE v_RowVer INT;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '订单取消失败,事务回滚';
END;
START TRANSACTION;
SELECT OrderId, OrderStatus INTO v_OrderId, v_OldStatus
FROM SalesOrder WHERE OrderNo = p_OrderNo FOR UPDATE;
IF v_OrderId IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '订单不存在';
END IF;
IF v_OldStatus IN ('Shipped','Completed') THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '订单已发货/已完成,不允许取消';
END IF;
UPDATE SalesOrder SET OrderStatus='Cancelled' WHERE OrderId = v_OrderId;
-- 库存回滚(加回)
SELECT InventoryId, StockQty, RowVersion INTO v_InvId, v_InvQty, v_RowVer
FROM Inventory_Denormalized inv
INNER JOIN WorkOrder wo ON wo.StyleId = inv.StyleId
WHERE wo.OrderId = v_OrderId LIMIT 1;
IF v_InvId IS NOT NULL THEN
UPDATE Inventory_Denormalized
SET StockQty = StockQty + 1, RowVersion = RowVersion + 1
WHERE InventoryId = v_InvId AND RowVersion = v_RowVer;
END IF;
UPDATE WorkOrder SET WorkStatus='Reject' WHERE OrderId = v_OrderId;
INSERT INTO BusinessCompensationLog(BizType, BizKey, BeforeState, AfterState, CompQty, CompRemark, Operator)
VALUES ('OrderCancel', p_OrderNo,
JSON_OBJECT('OrderNo',p_OrderNo,'OldStatus',v_OldStatus),
JSON_OBJECT('OrderNo',p_OrderNo,'NewStatus','Cancelled'),
1, p_Remark, p_Operator);
COMMIT;
END //
-- ---------- 9.5 工单返工 (质检不合格,重置工单,原料退回) ----------
CREATE PROCEDURE sp_WorkOrderRework(
IN p_WorkNo VARCHAR(50),
IN p_Remark VARCHAR(500),
IN p_Operator VARCHAR(64)
)
BEGIN
DECLARE v_WorkId INT;
DECLARE v_OldStatus ENUM('Pending','Working','QC','Finished','Reject');
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '工单返工失败,事务回滚';
END;
START TRANSACTION;
SELECT WorkOrderId, WorkStatus INTO v_WorkId, v_OldStatus
FROM WorkOrder WHERE WorkNo = p_WorkNo FOR UPDATE;
IF v_WorkId IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '工单号不存在';
END IF;
IF v_OldStatus <> 'QC' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '只有QC质检状态工单可以返工';
END IF;
UPDATE WorkOrder SET WorkStatus='Working', ActualEndDate=NULL WHERE WorkOrderId = v_WorkId;
-- 宝石原料退回(示例)
UPDATE GemRaw_3NF SET WeightCarat = WeightCarat + 0.1 WHERE GemId = 1;
INSERT INTO BusinessCompensationLog(BizType, BizKey, BeforeState, AfterState, CompQty, CompRemark, Operator)
VALUES ('WorkOrderRework', p_WorkNo,
JSON_OBJECT('WorkNo',p_WorkNo,'OldStatus',v_OldStatus),
JSON_OBJECT('WorkNo',p_WorkNo,'NewStatus','Working'),
1, p_Remark, p_Operator);
COMMIT;
END //
-- ---------- 9.6 通用库存回滚 ----------
CREATE PROCEDURE sp_InventoryRollback(
IN p_InventoryId INT,
IN p_AddQty INT,
IN p_Remark VARCHAR(500),
IN p_Operator VARCHAR(64)
)
BEGIN
DECLARE v_OldQty INT;
DECLARE v_Ver INT;
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存回滚失败';
END;
START TRANSACTION;
SELECT StockQty, RowVersion INTO v_OldQty, v_Ver
FROM Inventory_Denormalized WHERE InventoryId = p_InventoryId FOR UPDATE;
IF v_OldQty IS NULL THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '库存记录不存在';
END IF;
UPDATE Inventory_Denormalized
SET StockQty = StockQty + p_AddQty, RowVersion = RowVersion + 1
WHERE InventoryId = p_InventoryId AND RowVersion = v_Ver;
INSERT INTO BusinessCompensationLog(BizType, BizKey, BeforeState, AfterState, CompQty, CompRemark, Operator)
VALUES ('InventoryRollback', CAST(p_InventoryId AS CHAR),
JSON_OBJECT('InventoryId',p_InventoryId,'OldQty',v_OldQty),
JSON_OBJECT('InventoryId',p_InventoryId,'NewQty',v_OldQty + p_AddQty),
p_AddQty, p_Remark, p_Operator);
COMMIT;
END //
-- ---------- 9.7 批量生成销售订单测试数据(万级) ----------
CREATE PROCEDURE sp_BulkGenSalesOrder(IN p_rows INT)
BEGIN
DECLARE i INT DEFAULT 1;
DECLARE v_orderNo VARCHAR(64);
START TRANSACTION;
WHILE i <= p_rows DO
SET v_orderNo = CONCAT('ORD_BULK_',DATE_FORMAT(NOW(),'%Y%m%d'),'_',i);
INSERT INTO SalesOrder(TenantId, OrderNo, DealerId, OrderAmount, OrderStatus)
VALUES (1, v_orderNo, 1, FLOOR(RAND()50000)+1000, 'Confirmed');
SET i = i + 1;
END WHILE;
COMMIT;
END //
-- ---------- 9.8 定期ANALYZE更新统计信息 ----------
CREATE PROCEDURE sp_AnalyzeAllTables()
BEGIN
ANALYZE TABLE SalesOrder, WorkOrder, Inventory_Denormalized, Style_2NF, GemRaw_3NF, AuditLog;
END //
DELIMITER ;
/ ============================================================================
第10部分: 索引优化 + 分区表
============================================================================ /
-- 10.1 补充索引
CREATE INDEX idx_scd2_dealer_current ON Dim_Dealer_SCD2(DealerCode, IsCurrent);
CREATE INDEX idx_inv_style ON Inventory_Denormalized(StyleId);
CREATE INDEX idx_inv_update ON Inventory_Denormalized(UpdateTime);
CREATE INDEX idx_tenant_style ON Style_MultiTenant(TenantId);
-- 10.2 分区表: 珠宝成品库存流水事实表 (按月份RANGE分区)
CREATE TABLE Jewelry_Inventory_Fact (
FactId BIGINT AUTO_INCREMENT,
TenantId INT NOT NULL,
StyleId INT NOT NULL,
StockChangeQty INT NOT NULL,
OperateType TINYINT NOT NULL COMMENT '1入库,2出库,3盘点调整',
OperateTime DATETIME NOT NULL,
Remark VARCHAR(500),
PRIMARY KEY (FactId, OperateTime)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
PARTITION BY RANGE (TO_DAYS(OperateTime)) (
PARTITION p202601 VALUES LESS THAN (TO_DAYS('2026-02-01')),
PARTITION p202602 VALUES LESS THAN (TO_DAYS('2026-03-01')),
PARTITION p202603 VALUES LESS THAN (TO_DAYS('2026-04-01')),
PARTITION p202604 VALUES LESS THAN (TO_DAYS('2026-05-01')),
PARTITION p202605 VALUES LESS THAN (TO_DAYS('2026-06-01')),
PARTITION p202606 VALUES LESS THAN (TO_DAYS('2026-07-01')),
PARTITION p202607 VALUES LESS THAN (TO_DAYS('2026-08-01')),
PARTITION p202608 VALUES LESS THAN (TO_DAYS('2026-09-01')),
PARTITION p202609 VALUES LESS THAN (TO_DAYS('2026-10-01')),
PARTITION p_future VALUES LESS THAN MAXVALUE
);
/ ============================================================================
第11部分: 慢查询配置 + 优化器 Hint
============================================================================ /
-- 11.1 慢查询日志 (运行时动态设置,无需重启)
-- SET GLOBAL slow_query_log = ON;
-- SET GLOBAL long_query_time = 1.0;
-- SET GLOBAL log_queries_not_using_indexes = ON;
-- 查看: SHOW VARIABLES LIKE 'slow_query%';
-- 11.2 优化器 Hint 示例
-- 强制使用索引
-- SELECT /+ INDEX(s idx_tenant_style) / * FROM Style_MultiTenant s WHERE TenantId=1;
-- 禁止使用索引(测试对比)
-- SELECT /+ NO_INDEX(inv idx_inv_style) / * FROM Inventory_Denormalized inv WHERE StyleId=1;
-- EXPLAIN ANALYZE (真实执行统计)
-- EXPLAIN ANALYZE SELECT * FROM Inventory_Denormalized inv WHERE StyleId=1;
/ ============================================================================
第12部分: 事务隔离级别 + 锁分析 (库存并发扣减)
============================================================================ /
-- 查看当前隔离级别: SELECT @@transaction_isolation;
-- 设置RC(库存业务推荐,减少间隙锁):
-- SET SESSION transaction_isolation = 'READ-COMMITTED';
-- 悲观锁库存扣减 (防止超卖)
-- START TRANSACTION;
-- SELECT InventoryId, StockQty FROM Inventory_Denormalized WHERE InventoryId=1 FOR UPDATE;
-- UPDATE Inventory_Denormalized SET StockQty = StockQty - 1 WHERE InventoryId=1;
-- COMMIT;
-- 乐观锁库存扣减 (RowVersion版本号)
-- UPDATE Inventory_Denormalized SET StockQty=StockQty-1, RowVersion=RowVersion+1
-- WHERE InventoryId=1 AND RowVersion=1;
-- 锁分析查询
-- SELECT * FROM information_schema.innodb_trx;
-- SELECT * FROM information_schema.innodb_lock_waits;
-- SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
/ ============================================================================
第13部分: 数据仓库 JewelryDW (星型模型 / 雪花模型)
============================================================================ /
CREATE DATABASE IF NOT EXISTS JewelryDW DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
USE JewelryDW;
-- ---------- 13.1 星型模型 Star Schema ----------
CREATE TABLE Dim_Date (
DateKey INT PRIMARY KEY COMMENT 'YYYYMMDD数字主键',
FullDate DATE NOT NULL UNIQUE,
Year INT NOT NULL,
Quarter INT NOT NULL,
Month INT NOT NULL,
Day INT NOT NULL,
WeekOfYear INT NOT NULL,
WeekName VARCHAR(20) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DW维度: 时间';
CREATE TABLE Dim_Dealer (
DealerSK INT AUTO_INCREMENT PRIMARY KEY COMMENT '代理键',
DealerCode VARCHAR(30),
DealerName VARCHAR(100),
Address VARCHAR(200),
StartDate DATE,
EndDate DATE,
IsCurrent BOOLEAN,
INDEX idx_dealer_code (DealerCode),
INDEX idx_dealer_current (DealerCode, IsCurrent)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DW维度: 经销商 SCD2';
CREATE TABLE Dim_Style (
StyleSK INT AUTO_INCREMENT PRIMARY KEY,
StyleCode VARCHAR(50),
StyleName VARCHAR(100),
MaterialType VARCHAR(50),
CategoryName VARCHAR(100)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DW维度: 珠宝款式';
CREATE TABLE Fact_Sales (
SalesSK BIGINT AUTO_INCREMENT PRIMARY KEY,
DateKey INT NOT NULL,
DealerSK INT NOT NULL,
StyleSK INT NOT NULL,
SalesQty INT NOT NULL DEFAULT 1,
SalesAmount DECIMAL(18,2) NOT NULL,
FOREIGN KEY (DateKey) REFERENCES Dim_Date(DateKey),
FOREIGN KEY (DealerSK) REFERENCES Dim_Dealer(DealerSK),
FOREIGN KEY (StyleSK) REFERENCES Dim_Style(StyleSK),
INDEX idx_fact_date (DateKey),
INDEX idx_fact_dealer (DealerSK),
INDEX idx_fact_style (StyleSK)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DW事实表: 销售事实(星型)';
-- ---------- 13.2 雪花模型 Snowflake Schema (维度继续拆分子维度) ----------
CREATE TABLE Dim_Category (
CategorySK INT AUTO_INCREMENT PRIMARY KEY,
CategoryName VARCHAR(100),
ParentCategoryName VARCHAR(100)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='雪花子维度: 产品分类';
CREATE TABLE Dim_Material (
MaterialSK INT AUTO_INCREMENT PRIMARY KEY,
MaterialCode VARCHAR(20),
MaterialName VARCHAR(50)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='雪花子维度: 材质';
CREATE TABLE Dim_Style_Snowflake (
StyleSK INT AUTO_INCREMENT PRIMARY KEY,
StyleCode VARCHAR(50),
StyleName VARCHAR(100),
CategorySK INT NOT NULL,
MaterialSK INT NOT NULL,
FOREIGN KEY (CategorySK) REFERENCES Dim_Category(CategorySK),
FOREIGN KEY (MaterialSK) REFERENCES Dim_Material(MaterialSK)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='雪花维度: 款式(关联分类+材质子维度)';
CREATE TABLE Fact_Sales_Snowflake (
SalesSK BIGINT AUTO_INCREMENT PRIMARY KEY,
DateKey INT NOT NULL,
DealerSK INT NOT NULL,
StyleSK INT NOT NULL,
SalesQty INT NOT NULL,
SalesAmount DECIMAL(18,2) NOT NULL,
FOREIGN KEY (DateKey) REFERENCES Dim_Date(DateKey),
FOREIGN KEY (DealerSK) REFERENCES Dim_Dealer(DealerSK),
FOREIGN KEY (StyleSK) REFERENCES Dim_Style_Snowflake(StyleSK)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='DW事实表: 销售事实(雪花)';
-- ---------- 13.3 ETL控制表 + 错误日志表 ----------
CREATE TABLE ETL_Control (
ETLId INT AUTO_INCREMENT PRIMARY KEY,
ETLName VARCHAR(100) NOT NULL,
LastExtractTime DATETIME NOT NULL,
UpdateTime DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ETL控制: 记录最后抽取时间';
INSERT INTO ETL_Control (ETLName, LastExtractTime) VALUES ('FactSales','2026-01-01 00:00:00');
CREATE TABLE ETL_ErrorLog (
ErrorId BIGINT AUTO_INCREMENT PRIMARY KEY,
ETLTaskName VARCHAR(100) NOT NULL,
SourceTable VARCHAR(64) NOT NULL,
BizKey VARCHAR(100) NULL,
ErrorField VARCHAR(64) NULL,
ErrorMsg VARCHAR(500) NOT NULL,
RawData JSON NULL,
CaptureTime DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_etl_err_task (ETLTaskName, CaptureTime)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='ETL错误日志: 脏数据捕获';
-- 主从监控表(在从库使用)
CREATE TABLE IF NOT EXISTS ReplicationMonitorLog (
MonitorId BIGINT AUTO_INCREMENT PRIMARY KEY,
DelaySeconds INT NULL,
IO_Running VARCHAR(20),
SQL_Running VARCHAR(20),
ErrorMsg VARCHAR(500) NULL,
MonitorTime DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_monitor_time (MonitorTime)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='主从复制延迟监控';
/ ============================================================================
第14部分: ETL 存储过程 (OLTP JewelryERP -> JewelryDW 增量抽取)
============================================================================ */
USE JewelryDW;
DELIMITER //
-- 14.1 预生成日期维度
CREATE PROCEDURE sp_LoadDimDate(IN p_startDate DATE, IN p_endDate DATE)
BEGIN
DECLARE v_curr DATE;
DECLARE v_key INT;
SET v_curr = p_startDate;
START TRANSACTION;
WHILE v_curr <= p_endDate DO
SET v_key = CAST(DATE_FORMAT(v_curr,'%Y%m%d') AS UNSIGNED);
INSERT IGNORE INTO Dim_Date(DateKey, FullDate, Year, Quarter, Month, Day, WeekOfYear, WeekName)
VALUES(v_key, v_curr, YEAR(v_curr), QUARTER(v_curr), MONTH(v_curr),
DAY(v_curr), WEEK(v_curr), DAYNAME(v_curr));
SET v_curr = DATE_ADD(v_curr, INTERVAL 1 DAY);
END WHILE;
COMMIT;
END //
-- 14.2 SCD2 经销商维度增量拉链 (自动对比变更,新增版本)
CREATE PROCEDURE sp_ETL_SCD2_DimDealer()
BEGIN
DECLARE v_etl_date DATE;
SET v_etl_date = CURDATE();
CREATE TEMPORARY TABLE tmp_oltp_dealer (
DealerCode VARCHAR(30), DealerName VARCHAR(100), Address VARCHAR(200),
PRIMARY KEY(DealerCode)
);
INSERT INTO tmp_oltp_dealer(DealerCode, DealerName, Address)
SELECT DealerCode, DealerName, Address FROM JewelryERP.Dim_Dealer_SCD2 WHERE IsCurrent = TRUE;
-- 标记旧版本过期(数据发生变更的)
UPDATE JewelryDW.Dim_Dealer dw
INNER JOIN tmp_oltp_dealer src ON dw.DealerCode = src.DealerCode
SET dw.EndDate = v_etl_date, dw.IsCurrent = FALSE
WHERE dw.IsCurrent = TRUE
AND (dw.DealerName <> src.DealerName OR dw.Address <> src.Address);
-- 插入新增/变更后的新版本
INSERT INTO JewelryDW.Dim_Dealer(DealerCode, DealerName, Address, StartDate, EndDate, IsCurrent)
SELECT src.DealerCode, src.DealerName, src.Address, v_etl_date, NULL, TRUE
FROM tmp_oltp_dealer src
LEFT JOIN JewelryDW.Dim_Dealer dw ON src.DealerCode = dw.DealerCode AND dw.IsCurrent = TRUE
WHERE dw.DealerSK IS NULL;
DROP TEMPORARY TABLE tmp_oltp_dealer;
END //
-- 14.3 增量加载款式维度
CREATE PROCEDURE sp_ETL_DimStyle()
BEGIN
INSERT IGNORE INTO JewelryDW.Dim_Style(StyleCode, StyleName, CategoryName)
SELECT s.StyleCode, s.StyleName, c.CategoryName
FROM JewelryERP.Style_2NF s
LEFT JOIN JewelryERP.ProductCategory_AdjacencyList c ON 1=1
WHERE s.StyleCode NOT IN (SELECT StyleCode FROM JewelryDW.Dim_Style);
END //
-- 14.4 增量抽取销售事实表
CREATE PROCEDURE sp_ETL_FactSalesIncrement()
BEGIN
DECLARE v_last_extract DATETIME;
SELECT LastExtractTime INTO v_last_extract FROM JewelryDW.ETL_Control WHERE ETLName='FactSales';
CREATE TEMPORARY TABLE tmp_sales_incr (
OrderNo VARCHAR(50), OrderAmount DECIMAL(18,2), CreateTime DATETIME,
DealerCode VARCHAR(30), StyleCode VARCHAR(50), DateKey INT
);
INSERT INTO tmp_sales_incr
SELECT o.OrderNo, o.OrderAmount, o.CreateTime, d.DealerCode, s.StyleCode,
DATE_FORMAT(o.CreateTime,'%Y%m%d')
FROM JewelryERP.SalesOrder o
LEFT JOIN JewelryERP.Dim_Dealer_SCD2 d ON o.DealerId = d.DealerSurrogateKey
LEFT JOIN JewelryERP.Style_2NF s ON 1=1
WHERE o.CreateTime > v_last_extract;
INSERT INTO JewelryDW.Fact_Sales(DateKey, DealerSK, StyleSK, SalesQty, SalesAmount)
SELECT t.DateKey, dwd.DealerSK, dws.StyleSK, 1, t.OrderAmount
FROM tmp_sales_incr t
INNER JOIN JewelryDW.Dim_Dealer dwd ON t.DealerCode = dwd.DealerCode AND dwd.IsCurrent = TRUE
INNER JOIN JewelryDW.Dim_Style dws ON t.StyleCode = dws.StyleCode;
UPDATE JewelryDW.ETL_Control SET LastExtractTime = NOW() WHERE ETLName='FactSales';
DROP TEMPORARY TABLE tmp_sales_incr;
END //
-- 14.5 ETL总入口
CREATE PROCEDURE sp_ETL_All()
BEGIN
START TRANSACTION;
CALL sp_ETL_SCD2_DimDealer();
CALL sp_ETL_DimStyle();
CALL sp_ETL_FactSalesIncrement();
COMMIT;
END //
DELIMITER ;
/* ============================================================================
第15部分: ETL 数据质量校验 (脏数据捕获写入ETL_ErrorLog)
============================================================================ */
USE JewelryDW;
DELIMITER //
CREATE PROCEDURE sp_ETL_DataQuality_Check_FactSales()
BEGIN
CREATE TEMPORARY TABLE tmp_etl_src_sales (
OrderNo VARCHAR(50), OrderAmount DECIMAL(18,2), CreateTime DATETIME,
DealerCode VARCHAR(30), StyleCode VARCHAR(50), RawJSON JSON
);
INSERT INTO tmp_etl_src_sales
SELECT o.OrderNo, o.OrderAmount, o.CreateTime, d.DealerCode, s.StyleCode,
JSON_OBJECT('OrderNo',o.OrderNo,'OrderAmount',o.OrderAmount,'CreateTime',o.CreateTime)
FROM JewelryERP.SalesOrder o
LEFT JOIN JewelryERP.Dim_Dealer_SCD2 d ON o.DealerId = d.DealerSurrogateKey
LEFT JOIN JewelryERP.Style_2NF s ON 1=1;
-- 规则1: 订单号为空
INSERT INTO ETL_ErrorLog(ETLTaskName, SourceTable, BizKey, ErrorField, ErrorMsg, RawData)
SELECT 'FactSales','SalesOrder',OrderNo,'OrderNo','订单编号为空',RawJSON
FROM tmp_etl_src_sales WHERE OrderNo IS NULL OR OrderNo = '';
-- 规则2: 订单金额负数
INSERT INTO ETL_ErrorLog(ETLTaskName, SourceTable, BizKey, ErrorField, ErrorMsg, RawData)
SELECT 'FactSales','SalesOrder',OrderNo,'OrderAmount','订单金额小于0',RawJSON
FROM tmp_etl_src_sales WHERE OrderAmount < 0;
-- 规则3: 创建时间为未来时间
INSERT INTO ETL_ErrorLog(ETLTaskName, SourceTable, BizKey, ErrorField, ErrorMsg, RawData)
SELECT 'FactSales','SalesOrder',OrderNo,'CreateTime','订单创建时间不能大于当前时间',RawJSON
FROM tmp_etl_src_sales WHERE CreateTime > NOW();
-- 规则4: 款式编码为空
INSERT INTO ETL_ErrorLog(ETLTaskName, SourceTable, BizKey, ErrorField, ErrorMsg, RawData)
SELECT 'FactSales','SalesOrder',OrderNo,'StyleCode','款式编码为空',RawJSON
FROM tmp_etl_src_sales WHERE StyleCode IS NULL OR StyleCode='';
-- 规则5: 经销商编码为空
INSERT INTO ETL_ErrorLog(ETLTaskName, SourceTable, BizKey, ErrorField, ErrorMsg, RawData)
SELECT 'FactSales','SalesOrder',OrderNo,'DealerCode','经销商编码为空',RawJSON
FROM tmp_etl_src_sales WHERE DealerCode IS NULL OR DealerCode='';
-- 干净数据供后续ETL加载
CREATE TEMPORARY TABLE tmp_etl_clean_sales AS
SELECT * FROM tmp_etl_src_sales
WHERE OrderNo IS NOT NULL AND OrderNo <> ''
AND OrderAmount >= 0 AND CreateTime <= NOW()
AND StyleCode IS NOT NULL AND StyleCode <> ''
AND DealerCode IS NOT NULL AND DealerCode <> '';
DROP TEMPORARY TABLE tmp_etl_src_sales;
END //
DELIMITER ;
/* ============================================================================
第16部分: 读写分离 + 主从延迟监控
============================================================================ */
-- 架构: Master主库承担OLTP写入; Slave从库承担报表查询+ETL抽取
-- 主库my.cnf: server-id=1, log_bin=mysql-bin, binlog_format=ROW
-- 从库my.cnf: server-id=2, read_only=ON, super_read_only=ON
-- 业务层路由: 写操作->Master; 非实时报表/ETL->Slave; 库存实时扣减->Master
USE JewelryDW;
DELIMITER //
CREATE PROCEDURE sp_MonitorReplicationDelay(IN p_Threshold INT)
BEGIN
DECLARE v_delay INT;
DECLARE v_io_run VARCHAR(20);
DECLARE v_sql_run VARCHAR(20);
DECLARE v_err VARCHAR(500);
SELECT Seconds_Behind_Master, Slave_IO_Running, Slave_SQL_Running,
CONCAT(Last_IO_Error, ' ', Last_SQL_Error)
INTO v_delay, v_io_run, v_sql_run, v_err
FROM INFORMATION_SCHEMA.REPLICATION_STATUS;
INSERT INTO ReplicationMonitorLog(DelaySeconds, IO_Running, SQL_Running, ErrorMsg)
VALUES (v_delay, v_io_run, v_sql_run, v_err);
IF v_delay > p_Threshold THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = CONCAT('主从复制延迟告警,延迟:',v_delay,'秒,阈值:',p_Threshold);
END IF;
END //
DELIMITER ;
/* ============================================================================
第17部分: 数据库安全 (账号权限 / 行级安全视图 / 数据脱敏 / 审计加固)
============================================================================ /
-- 注意: 以下账号创建需在root账号下执行,脚本中默认注释,按需取消注释
-- CREATE USER 'etl_user'@'%' IDENTIFIED BY 'StrongETL@2026';
-- GRANT SELECT ON JewelryERP. TO 'etl_user'@'%';
-- GRANT INSERT,UPDATE,SELECT ON JewelryDW.* TO 'etl_user'@'%';
-- CREATE USER 'report_user'@'%' IDENTIFIED BY 'Report@2026Read';
-- GRANT SELECT ON JewelryERP.* TO 'report_user'@'%';
-- CREATE USER 'erp_app'@'%' IDENTIFIED BY 'ERPApp@2026';
-- GRANT SELECT,INSERT,UPDATE,DELETE ON JewelryERP.* TO 'erp_app'@'%';
-- REVOKE DROP,ALTER ON JewelryERP.* FROM 'erp_app'@'%';
-- FLUSH PRIVILEGES;
-- 行级安全: MySQL通过视图+会话变量实现(MySQL无原生CREATE POLICY)
-- 业务层设置 SET @current_tenant_id = 1; 后查询视图自动过滤
USE JewelryERP;
CREATE OR REPLACE VIEW vw_SalesOrder_Tenant AS
SELECT * FROM SalesOrder WHERE TenantId = @current_tenant_id;
CREATE OR REPLACE VIEW vw_Inventory_Tenant AS
SELECT inv.* FROM Inventory_Denormalized inv
INNER JOIN Style_MultiTenant sm ON inv.StyleId = sm.StyleId
WHERE sm.TenantId = @current_tenant_id;
-- 审计加固: 审计日志表禁止业务账号UPDATE/DELETE
-- REVOKE UPDATE,DELETE ON JewelryERP.AuditLog FROM 'erp_app'@'%';
-- GRANT INSERT,SELECT ON JewelryERP.AuditLog TO 'erp_app'@'%';
/* ============================================================================
第18部分: 运维脚本 (备份 / binlog / 碎片优化 / 统计信息)
============================================================================ /
-- 18.1 mysqldump全量备份 (Shell脚本,需在Linux命令行执行)
-- #!/bin/bash
-- BACKUP_DATE=$(date +%Y%m%d_%H%M%S)
-- BACKUP_DIR="/data/backup/jewelry"
-- mkdir -p ${BACKUP_DIR}
-- mysqldump -ubackup_user -pBackupPass --single-transaction --routines --triggers --events JewelryERP
-- > ${BACKUP_DIR}/JewelryERP_full_${BACKUP_DATE}.sql
-- find ${BACKUP_DIR} -name "JewelryERP_full_.sql" -mtime +7 -delete
-- 18.2 表碎片优化 (业务低峰期执行,会锁表)
-- OPTIMIZE TABLE SalesOrder;
-- OPTIMIZE TABLE Inventory_Denormalized;
-- OPTIMIZE TABLE AuditLog;
-- OPTIMIZE TABLE BusinessCompensationLog;
-- 18.3 查看表碎片
-- SELECT TABLE_NAME, DATA_LENGTH/1024/1024 AS DATA_MB,
-- INDEX_LENGTH/1024/1024 AS IDX_MB, DATA_FREE/1024/1024 AS FREE_MB
-- FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA='JewelryERP';
-- 18.4 定时事件: 每日凌晨更新统计信息
SET GLOBAL event_scheduler = ON;
CREATE EVENT IF NOT EXISTS evt_analyze_table_stats
ON SCHEDULE EVERY 1 DAY STARTS '2026-09-16 01:00:00'
DO CALL JewelryERP.sp_AnalyzeAllTables();
-- 18.5 定时事件: 每日凌晨ETL
CREATE EVENT IF NOT EXISTS evt_etl_daily
ON SCHEDULE EVERY 1 DAY STARTS '2026-09-16 02:00:00'
DO CALL JewelryDW.sp_ETL_All();
/* ============================================================================
第19部分: BI 报表 SQL (销售分析 / 库存周转 / 工单良率 / 加盟商分析)
============================================================================ /
USE JewelryERP;
-- 报表1: 按月+款式统计销售金额、订单数量
-- SELECT DATE_FORMAT(o.CreateTime,'%Y-%m') AS OrderMonth, s.StyleName,
-- COUNT(o.OrderId) AS OrderCount, SUM(o.OrderAmount) AS TotalSalesAmount
-- FROM SalesOrder o
-- LEFT JOIN WorkOrder wo ON o.OrderId = wo.OrderId
-- LEFT JOIN Style_2NF s ON wo.StyleId = s.StyleId
-- WHERE o.OrderStatus NOT IN ('Cancelled')
-- GROUP BY OrderMonth, StyleName ORDER BY OrderMonth DESC, TotalSalesAmount DESC;
-- 报表2: 库存周转天数分析
-- SELECT s.StyleName, inv.StockQty, inv.UnitCost,
-- inv.StockQty * inv.UnitCost AS StockTotalCost,
-- COALESCE(SUM(o.OrderAmount),0) AS SalesAmount_30d,
-- CASE WHEN COALESCE(SUM(o.OrderAmount),0)>0
-- THEN (inv.StockQtyinv.UnitCost)/SUM(o.OrderAmount)30 ELSE 999 END AS TurnoverDays
-- FROM Inventory_Denormalized inv
-- LEFT JOIN Style_2NF s ON inv.StyleId=s.StyleId
-- LEFT JOIN WorkOrder wo ON wo.StyleId=s.StyleId
-- LEFT JOIN SalesOrder o ON o.OrderId=wo.OrderId AND o.CreateTime>=DATE_SUB(NOW(),INTERVAL 30 DAY)
-- GROUP BY inv.StyleId,s.StyleName,inv.StockQty,inv.UnitCost;
-- 报表3: 加工工单良率
-- SELECT DATE_FORMAT(wo.CreateTime,'%Y-%m') AS WorkMonth, s.StyleName,
-- COUNT(wo.WorkOrderId) AS TotalCnt,
-- SUM(CASE WHEN wo.WorkStatus='Finished' THEN 1 ELSE 0 END) AS FinishedCnt,
-- SUM(CASE WHEN wo.WorkStatus='Reject' THEN 1 ELSE 0 END) AS RejectCnt,
-- ROUND(SUM(CASE WHEN wo.WorkStatus='Finished' THEN 1 ELSE 0 END)100.0/COUNT(wo.WorkOrderId),2) AS YieldRatePct
-- FROM WorkOrder wo INNER JOIN Style_2NF s ON wo.StyleId=s.StyleId
-- GROUP BY WorkMonth,StyleName ORDER BY WorkMonth DESC,YieldRatePct DESC;
-- 报表4: 加盟商多维度经营分析
-- SELECT t.TenantName, COUNT(o.OrderId) AS TotalOrderCount,
-- SUM(o.OrderAmount) AS TotalSalesAmount,
-- SUM(CASE WHEN o.OrderStatus='Cancelled' THEN 1 ELSE 0 END) AS CancelCnt,
-- ROUND(SUM(CASE WHEN o.OrderStatus='Cancelled' THEN 1 ELSE 0 END)100.0/COUNT(o.OrderId),2) AS CancelRatePct
-- FROM SalesOrder o INNER JOIN Tenant t ON o.TenantId=t.TenantId
-- WHERE o.CreateTime>=DATE_SUB(NOW(),INTERVAL 90 DAY)
-- GROUP BY t.TenantName ORDER BY TotalSalesAmount DESC;
/ ============================================================================
第20部分: 层次模型性能对比 (EXPLAIN)
场景: 查询"戒指"分类下所有子节点
============================================================================ /
USE JewelryERP;
-- 方案1: 邻接表 - 递归CTE
-- EXPLAIN
-- WITH RECURSIVE CategoryTree AS (
-- SELECT CategoryId,CategoryName,ParentId,CAST(CategoryName AS CHAR(500)) AS Path,0 AS Lvl
-- FROM ProductCategory_AdjacencyList WHERE CategoryName='戒指'
-- UNION ALL
-- SELECT c.CategoryId,c.CategoryName,c.ParentId,CONCAT(ct.Path,'/',c.CategoryName),ct.Lvl+1
-- FROM ProductCategory_AdjacencyList c INNER JOIN CategoryTree ct ON c.ParentId=ct.CategoryId
-- )
-- SELECT * FROM CategoryTree ORDER BY Path;
-- 方案2: 嵌套集 - BETWEEN查询(无递归)
-- EXPLAIN
-- SELECT child. FROM ProductCategory_NestedSet parent
-- INNER JOIN ProductCategory_NestedSet child ON child.LeftValue BETWEEN parent.LeftValue AND parent.RightValue
-- WHERE parent.CategoryName='戒指' ORDER BY child.LeftValue;
-- 方案3: 闭包表 - JOIN查询
-- EXPLAIN
-- SELECT m., r.Depth FROM ProductCategory_Closure_Relation r
-- INNER JOIN ProductCategory_Closure_Main m ON r.DescendantId=m.CategoryId
-- WHERE r.AncestorId=(SELECT CategoryId FROM ProductCategory_Closure_Main WHERE CategoryName='戒指')
-- ORDER BY r.Depth, m.CategoryId;
/ ============================================================================
附录: DDD 领域模型说明
聚合(Aggregate):
产品聚合 Product: 聚合根Style_2NF, 实体Style_Gem_Map_2NF/ProductCategory,
值对象: 材质/宝石参数(克拉/颜色/净度)
宝石原料聚合 GemRaw: 聚合根GemRaw_3NF, 实体GemType_Lookup_3NF(查找表)
销售订单聚合 SalesOrder: 聚合根SalesOrder, 值对象: 订单金额/状态/编号
加工工单聚合 WorkOrder: 聚合根WorkOrder, 值对象: 工单号/计划日期/质检状态
库存聚合 Inventory: 聚合根Inventory_Denormalized, 值对象: 库存数量/成本/RowVersion
多租户聚合 Tenant: 聚合根Tenant, 实体Style_MultiTenant
审计聚合 AuditLog: 跨领域事件记录
实体(Entity): Style, SalesOrder, GemRaw, WorkOrder (有唯一业务ID,生命周期可变更)
值对象(Value Object): 克拉重量、金额、日期、地址 (无独立ID,依附聚合根)
ER关系:
Tenant 1--* Style_MultiTenant
Style_2NF 1--* Style_Gem_Map_2NF
GemRaw_3NF 1--* Style_Gem_Map_2NF
GemType_Lookup_3NF 1--* GemRaw_3NF
ProductCategory_AdjacencyList 自引用 1--* (父子分类)
SalesOrder 1--* WorkOrder
Style_2NF 1--* WorkOrder
Dim_Dealer_SCD2 1--* SalesOrder
AuditLog: 所有主表INSERT/UPDATE/DELETE触发写入
范式策略:
OLTP JewelryERP: 主业务表3NF,BOM/字典用Lookup/Junction表
库存汇总表反范式Denormalization,触发器同步冗余字段
层次分类: 邻接表用于业务端(频繁增删),嵌套集/闭包表用于只读分析
DW JewelryDW: 星型模型优先,SCD2拉链维度,事实表增量ETL
============================================================================ */
SELECT '=== 珠宝行业数据建模完整脚本执行完毕 ===' AS Result;
二、 SCD 三种策略对比选型
为帮助读者在珠宝经销商维度建模时快速决策,下表从实现方式、历史保留能力、存储开销、查询复杂度、适用场景和典型示例六个维度,对 SCD Type1、Type2、Type3 三种策略进行横向对比:
| 策略名称 | 实现方式 | 历史保留能力 | 存储开销 | 查询复杂度 | 适用场景 | 典型示例(经销商维度) |
|---|---|---|---|---|---|---|
| Type1 覆盖 | 直接 UPDATE 原记录,用新值覆盖旧值 | 不保留历史,旧值永久丢失 | 最低(单行存储) | 最低(无需额外过滤条件) | 历史无分析价值、仅关注当前状态的场景 | 经销商A地址从「罗湖区水贝」直接改为「龙岗区布吉」,旧地址不可追溯 |
| Type2 拉链 | 新增一行记录,通过 StartDate/EndDate/IsCurrent 标记版本有效期 | 完整保留全部历史版本,可回溯任意时点状态 | 最高(每次变更新增一行,随变更次数线性增长) | 较高(需按 IsCurrent=TRUE 或时间范围过滤) | 需要历史分析、审计追溯、DW 事实表关联历史状态的场景(DW 最常用) | 经销商A地址变更三次,拉链表保留三行:水贝(2024-01-01~2025-06-01)、布吉(2025-06-01~2026-01-01)、福田CBD(2026-01-01至今,IsCurrent=TRUE) |
| Type3 保留上一版本 | 在原记录上新增 PreviousAddress、AddressChangeDate 等历史字段,仅保存上一次变更 | 仅保留上一版本,更早历史仍会丢失 | 中等(单行 + 少量历史字段) | 较低(当前值和上一值在同一行,无需 JOIN) | 只需对比「当前 vs 上一次」、不需要完整历史链的场景 | 经销商A记录中 CurrentAddress='龙岗区布吉',PreviousAddress='罗湖区水贝',AddressChangeDate='2025-06-01',更早地址不可查 |
选型建议:若业务仅需当前状态且历史无价值,选 Type1;若需完整历史追溯(如 DW 事实表按时间切片分析),选 Type2;若只需对比最近两次变更,选 Type3。珠宝行业经销商分销分析通常推荐 Type2 拉链方案。