一、文章概述
本文以国际化珠宝集团 OA/ERP/CRM/HRM/MES/SCM/WMS/KMS 八域业务数据为背景,系统讲解 MySQL 9.0 中复杂连接与子查询管理的核心技术与实践方法。全文围绕多表 JOIN、CTE 分层、子查询全家桶、物化分解、每组 Top-N 优化、执行计划评审、国际时区与多币种结算等主题展开,并提供一套完整可运行的 SQL 脚本作为实战演示。
二、运行环境与前置准备
本节介绍脚本的运行前提、两种执行方式(命令行与 Workbench)、幂等性设计原则,以及用于量化查询代价的度量框架(performance_schema 的 INNODB_ROWS_READ 增量)。
三、MySQL 9.0 连接与子查询能力速览
本节以表格形式对照 SQL Server 等数据库的通用概念,梳理 MySQL 9.0 中 CTE、标量子查询、派生表、EXISTS 反连接、IN 子查询、窗口函数、临时表二次引用限制等能力的等价关系与注意事项。
四、反模式速查表
本节汇总多表 JOIN 不写别名、相关子查询做 Top-N、中间结果反复重算、临时表二次引用、标量子查询返回多行、跨币种直接求和、CONVERT_TZ 不可用等常见反模式及其正确做法。
五、度量框架与造数准备
本节介绍 cj_demo_metric、cj_demo_result、cj_demo_probe 等度量与探针表的建表逻辑,以及 cj_snap、cj_measure、cj_v、cj_err_probe 四个存储过程的用途,为后续章节的代价量化与断言验证奠定基础。
六、维度表与交易事实造数
本节详细说明八域维度表(币种、客户、商品、供应商、仓库、员工、OA 请购单、KMS 文档)与 100,000 行交易事实表的建表与造数逻辑,包括递归 CTE 生成序列、多币种换算、本地营业日生成列等关键设计。
七、多表 JOIN 实战
本节演示五表联表查询(交易、客户、商品、供应商、仓库、员工),以及分组聚合 JOIN 计算品类与客户国的 GMV,并给出对应的断言验证。
八、CTE 分层实战
本节演示如何用 WITH 把复杂查询拆解为 base(过滤与 JOIN)、by_cat(聚合)等可读管道,并验证 CTE 分层聚合的结果正确性。
九、子查询全家桶实战
本节逐一演示标量子查询、派生表、EXISTS 反连接、IN 子查询、维度扩展 JOIN 的写法与适用场景,并验证反连接两种写法(NOT EXISTS 与 LEFT JOIN IS NULL)结果一致。
十、物化分解实战
本节演示把中间结果物化成普通表一次计算多次复用,并展示临时表不能在一句里二次引用(报错 1137)的陷阱。
十一、每组 Top-N 优化实战
本节对比相关子查询与窗口函数两种 Top-N 写法的执行代价,用 INNODB_ROWS_READ 增量量化读行差异,并验证两种写法结果完全一致。
十二、执行计划评审实战
本节使用 EXPLAIN ANALYZE 查看真实执行计划,并对比相关标量子查询与 GROUP BY JOIN 两种写法的读行与结果一致性。
十三、国际时区与多币种结算实战
本节演示多币种展示(JOIN 维度表取符号)与本地营业日范围过滤,强调结算只 SUM(amount_usd),避免跨币种直接相加。
十四、别名最佳实践
本节通过反例演示歧义列名报错 1052,并给出短别名加限定列名的正例写法。
十五、边界探针与错误捕获
本节演示标量子查询返回多行报错 1242、临时表二次引用报错 1137 等边界场景,并统一做断言验证。
十六、结论与清单
本节汇总全文七条核心结论,并给出万亿级场景下的延伸建议(优先窗口函数与物化、反连接优先 EXISTS、建好连接键索引等)。
十七、自检汇总
本节展示 V1 至 V12 全部断言的自检结果,确认脚本所有验证项均通过。
sql
CREATE DATABASE IF NOT EXISTS JewelryAnalytics DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE JewelryAnalytics;
SET SESSION cte_max_recursion_depth=1000000;
SET SESSION group_concat_max_len=1000000;
SET SESSION optimizer_switch='index_condition_pushdown=on,mrr=on,derived_merge=on,skip_scan=on';
SET SESSION transaction_isolation='REPEATABLE-READ';
SET SESSION sql_safe_updates=0;
SET SESSION innodb_lock_wait_timeout=50;
SET SESSION time_zone='+00:00';
SET SESSION max_execution_time=0;
-- ============================================================================
-- §0.0 运行方式必读(务必先读)
-- ============================================================================
-- 本脚本:MySQL 9.0「复杂连接与子查询管理」专题
-- 主题:国际化珠宝集团 OA/ERP/CRM/HRM/MES/SCM/WMS/KMS 八域数据
-- + 多表 JOIN / CTE 分层 / 子查询全家桶(标量·派生·EXISTS·IN)
-- + 物化分解 / 每组 Top-N(相关子查询 vs 窗口) / 执行计划评审
-- + 万亿高并发 + 国际时区 + 多币种结算
-- 运行前提:MySQL 9.0.x,库 JewelryAnalytics 自动建/用;本脚本完全幂等。
-- 方式A(命令行,推荐):
-- mysql -uroot -p --default-character-set=utf8mb4 < MySQL9_CplxJoinsSubqueries_JewelryDemo.sql
-- 方式B(Workbench):整段执行。
-- 幂等:每次先 DROP 全部 cj_* 实验表/过程再重建并造数,重复运行结果一致。
-- 度量:用 performance_schema.session_status 的 INNODB_ROWS_READ 增量,
-- 量化「引擎真正读了多少行」,比 EXPLAIN rows 更接近真实代价。
-- ============================================================================
-- §0.1 MySQL 9.0 连接/子查询能力速览(与其它数据库术语映射)
-- ----------------------------------------------------------------------------
-- SQL Server / 通用概念 MySQL 9.0 等价 / 真相
-- --------------------------- -------------------------------------------------
-- CTE (WITH) ✅ WITH ... 支持,可被多次引用(可能物化)
-- 临时表分解大查询 ✅ CREATE TEMPORARY TABLE / 普通物化表
-- 标量子查询(SELECT 中) ✅ 每行执行一次;返回多行→报错 1242
-- 派生表(FROM 中子查询) ✅ 必须加别名;优化器可能 derived_merge
-- EXISTS / NOT EXISTS 反连接 ✅ 常用且高效,等价于 LEFT JOIN ... IS NULL
-- IN (子查询) ✅ 优化器常改写为半连接(semi-join)
-- 每组 Top-N(相关子查询) ⚠️ 每行重扫一次→代价爆炸;优先用窗口函数
-- 窗口函数 ROW_NUMBER/RANK ✅ 单次遍历,Top-N 正解
-- 表别名/列限定 ✅ 多表 JOIN 必须;歧义列→报错 1052
-- 临时表在子查询内二次引用 ❌ 报错 1137(同一临时表不能在一句里引用两次)
-- 执行计划评审 ✅ EXPLAIN ANALYZE(真实计时+真实行数)
-- ============================================================================
-- §0.2 反模式速查表(本机已实测 / 业界共识)
-- ----------------------------------------------------------------------------
-- 反模式 后果 正确做法
-- 多表 JOIN 不写表别名/列限定 报错 1052(歧义列) 显式 t.col / 短别名
-- 用相关子查询做每组 Top-N 每行重扫大表→读行数百倍 窗口函数 ROW_NUMBER/RANK
-- 把中间结果反复重算(重复 JOIN/聚合) CPU/IO 浪费,万亿级不可接受 物化成临时/普通表一次
-- 相关子查询建立在临时表上 报错 1137(临时表不能二次引用) 用普通物化表
-- SELECT 里标量子查询返回多行 报错 1242 保证至多一行 / 用聚合
-- 跨币种直接 SUM(amount_orig) 不同币种相加无意义 只 SUM(amount_usd)
-- CONVERT_TZ(ts,'UTC',name) 本机 mysql.time_zone_name=0→NULL 用 TIMESTAMPADD(分,偏移,ts)
-- ============================================================================
-- §01 度量框架(cj_demo_*):量化引擎真实读行,证明取舍
DROP TABLE IF EXISTS cj_demo_metric;
CREATE TABLE cj_demo_metric(
tag VARCHAR(40) PRIMARY KEY,
innodb_rows BIGINT NOT NULL DEFAULT 0,
rnd_next BIGINT NOT NULL DEFAULT 0,
rnd BIGINT NOT NULL DEFAULT 0,
ms INT NOT NULL DEFAULT 0,
note VARCHAR(255)
);
DROP TABLE IF EXISTS cj_demo_result;
CREATE TABLE cj_demo_result(
check_id VARCHAR(30) PRIMARY KEY,
passed TINYINT NOT NULL,
detail VARCHAR(400)
);
DROP TABLE IF EXISTS cj_demo_probe;
CREATE TABLE cj_demo_probe(
id INT AUTO_INCREMENT PRIMARY KEY,
label VARCHAR(60),
errno INT,
sql_state VARCHAR(10),
msg VARCHAR(300),
caught_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
DROP PROCEDURE IF EXISTS cj_snap;
DELIMITER //
CREATE PROCEDURE cj_snap()
BEGIN
SELECT VARIABLE_VALUE INTO @cj_snap_ir FROM performance_schema.session_status WHERE VARIABLE_NAME='INNODB_ROWS_READ';
SELECT VARIABLE_VALUE INTO @cj_snap_rn FROM performance_schema.session_status WHERE VARIABLE_NAME='HANDLER_READ_RND_NEXT';
SELECT VARIABLE_VALUE INTO @cj_snap_rd FROM performance_schema.session_status WHERE VARIABLE_NAME='HANDLER_READ_RND';
SET @cj_snap_ts = UNIX_TIMESTAMP(NOW(6));
END//
DELIMITER ;
DROP PROCEDURE IF EXISTS cj_measure;
DELIMITER //
CREATE PROCEDURE cj_measure(IN p_tag VARCHAR(40))
BEGIN
DECLARE v_ir BIGINT; DECLARE v_rn BIGINT; DECLARE v_rd BIGINT; DECLARE v_ms INT;
SELECT VARIABLE_VALUE INTO v_ir FROM performance_schema.session_status WHERE VARIABLE_NAME='INNODB_ROWS_READ';
SELECT VARIABLE_VALUE INTO v_rn FROM performance_schema.session_status WHERE VARIABLE_NAME='HANDLER_READ_RND_NEXT';
SELECT VARIABLE_VALUE INTO v_rd FROM performance_schema.session_status WHERE VARIABLE_NAME='HANDLER_READ_RND';
SET v_ms = ROUND((UNIX_TIMESTAMP(NOW(6)) - @cj_snap_ts)*1000);
INSERT INTO cj_demo_metric(tag,innodb_rows,rnd_next,rnd,ms) VALUES(p_tag, v_ir-@cj_snap_ir, v_rn-@cj_snap_rn, v_rd-@cj_snap_rd, v_ms)
ON DUPLICATE KEY UPDATE innodb_rows=v_ir-@cj_snap_ir, rnd_next=v_rn-@cj_snap_rn, rnd=v_rd-@cj_snap_rd, ms=v_ms;
END//
DELIMITER ;
DROP PROCEDURE IF EXISTS cj_v;
DELIMITER //
CREATE PROCEDURE cj_v(IN p_check VARCHAR(30), IN p_pass TINYINT, IN p_detail VARCHAR(400))
BEGIN
INSERT INTO cj_demo_result(check_id,passed,detail) VALUES(p_check,p_pass,p_detail)
ON DUPLICATE KEY UPDATE passed=p_pass, detail=p_detail;
END//
DELIMITER ;
DROP PROCEDURE IF EXISTS cj_err_probe;
DELIMITER //
CREATE PROCEDURE cj_err_probe(IN p_label VARCHAR(60), IN p_sql TEXT)
BEGIN
DECLARE v_no INT DEFAULT 0; DECLARE v_st VARCHAR(10) DEFAULT '00000'; DECLARE v_msg VARCHAR(300) DEFAULT '';
DECLARE CONTINUE HANDLER FOR SQLEXCEPTION
BEGIN
GET DIAGNOSTICS CONDITION 1 v_no = MYSQL_ERRNO, v_st = RETURNED_SQLSTATE, v_msg = MESSAGE_TEXT;
END;
SET @cj_probe_sql = p_sql;
PREPARE cj_pstmt FROM @cj_probe_sql;
IF v_no = 0 THEN
EXECUTE cj_pstmt;
DEALLOCATE PREPARE cj_pstmt;
END IF;
INSERT INTO cj_demo_probe(label,errno,sql_state,msg) VALUES(p_label, v_no, v_st, v_msg);
END//
DELIMITER ;
-- §02 维度表(八域)+ 多币种 + 交易事实造数(100,000 行)
-- 命名统一 cj_* 前缀,避免与同库 dyn_*/fts_*/samp_*/js_*/geo_*/gl_*/opt_*/dim_* 冲突。
DROP TABLE IF EXISTS cj_dim_currency;
CREATE TABLE cj_dim_currency(code CHAR(3) PRIMARY KEY, name VARCHAR(40), symbol VARCHAR(8), usd_rate DECIMAL(12,6));
INSERT INTO cj_dim_currency VALUES
('CNY','人民币','¥',1/7.20),('USD','美元','$',1.0),('EUR','欧元','€',1.08),
('JPY','日元','¥',0.0067),('HKD','港元','HK$',1/7.80),('GBP','英镑','£',1.27),
('SGD','新加坡元','S$',1/1.35);
-- 客户维度(CRM,10,000,含 VIP 标记)
DROP TABLE IF EXISTS cj_customer;
CREATE TABLE cj_customer(
customer_id INT PRIMARY KEY,
name VARCHAR(60),
vip TINYINT NOT NULL DEFAULT 0,
country_code CHAR(2),
currency_code CHAR(3),
reg_ts DATETIME
) ENGINE=InnoDB STATS_PERSISTENT=1 STATS_AUTO_RECALC=1;
INSERT INTO cj_customer (customer_id, name, vip, country_code, currency_code, reg_ts)
WITH RECURSIVE seq(n) AS (
SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n < 10000
)
SELECT n, CONCAT('顾客#', n), IF(n % 10 = 0, 1, 0),
ELT(1 + ((n*3) % 8), 'CN','HK','US','GB','JP','FR','DE','SG'),
ELT(1 + ((n*3) % 8), 'CNY','HKD','USD','GBP','JPY','EUR','EUR','SGD'),
DATE_ADD('2022-01-01 00:00:00', INTERVAL (n % 800) DAY)
FROM seq;
-- 商品维度(ERP/MES,2,000,4 品类,归属供应商)
DROP TABLE IF EXISTS cj_product;
CREATE TABLE cj_product(
product_id INT PRIMARY KEY,
product_name VARCHAR(120),
category VARCHAR(8),
unit_cost_usd DECIMAL(10,2),
supplier_id INT
) ENGINE=InnoDB STATS_PERSISTENT=1 STATS_AUTO_RECALC=1;
INSERT INTO cj_product (product_id, product_name, category, unit_cost_usd, supplier_id)
WITH RECURSIVE seq(n) AS (
SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n < 2000
)
SELECT n, CONCAT(ELT(1+(n%4),'钻石','黄金','铂金','彩宝'),'饰品#', n),
ELT(1+(n%4),'钻石','黄金','铂金','彩宝'),
ROUND(10 + ((n*3) % 500), 2),
1 + (n % 200)
FROM seq;
-- 供应商维度(SCM,200)
DROP TABLE IF EXISTS cj_supplier;
CREATE TABLE cj_supplier(
supplier_id INT PRIMARY KEY,
supplier_name VARCHAR(60),
country_code CHAR(2),
currency_code CHAR(3)
) ENGINE=InnoDB STATS_PERSISTENT=1 STATS_AUTO_RECALC=1;
INSERT INTO cj_supplier (supplier_id, supplier_name, country_code, currency_code)
WITH RECURSIVE seq(n) AS (
SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n < 200
)
SELECT n, CONCAT('供应商#', n),
ELT(1 + ((n*3) % 8), 'CN','HK','US','GB','JP','FR','DE','SG'),
ELT(1 + ((n*3) % 8), 'CNY','HKD','USD','GBP','JPY','EUR','EUR','SGD')
FROM seq;
-- 仓库维度(WMS,50)
DROP TABLE IF EXISTS cj_warehouse;
CREATE TABLE cj_warehouse(
wh_id INT PRIMARY KEY,
wh_name VARCHAR(60),
country_code CHAR(2)
) ENGINE=InnoDB STATS_PERSISTENT=1 STATS_AUTO_RECALC=1;
INSERT INTO cj_warehouse (wh_id, wh_name, country_code)
WITH RECURSIVE seq(n) AS (
SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n < 50
)
SELECT n, CONCAT('仓#', n),
ELT(1 + ((n*3) % 8), 'CN','HK','US','GB','JP','FR','DE','SG')
FROM seq;
-- 员工维度(HRM,500,部门=八域之一)
DROP TABLE IF EXISTS cj_employee;
CREATE TABLE cj_employee(
emp_id INT PRIMARY KEY,
name VARCHAR(60),
dept_code CHAR(3),
country_code CHAR(2),
hire_ts DATETIME
) ENGINE=InnoDB STATS_PERSISTENT=1 STATS_AUTO_RECALC=1;
INSERT INTO cj_employee (emp_id, name, dept_code, country_code, hire_ts)
WITH RECURSIVE seq(n) AS (
SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n < 500
)
SELECT n, CONCAT('员工#', n),
ELT(1+(n%8),'OA','ERP','CRM','HRM','MES','SCM','WMS','KMS'),
ELT(1 + ((n*3) % 8), 'CN','HK','US','GB','JP','FR','DE','SG'),
DATE_ADD('2020-01-01 00:00:00', INTERVAL (n % 2000) DAY)
FROM seq;
-- OA 请购单(OA,5,000;申请人仅 1..400,故员工 401..500 永无请购单→反连接演示用)
DROP TABLE IF EXISTS cj_oa_requisition;
CREATE TABLE cj_oa_requisition(
req_id INT PRIMARY KEY,
applicant_emp_id INT,
dept_code CHAR(3),
status VARCHAR(12),
amount_usd DECIMAL(12,2),
created_ts DATETIME
) ENGINE=InnoDB STATS_PERSISTENT=1 STATS_AUTO_RECALC=1;
INSERT INTO cj_oa_requisition (req_id, applicant_emp_id, dept_code, status, amount_usd, created_ts)
WITH RECURSIVE seq(n) AS (
SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n < 5000
)
SELECT n, 1 + (n % 400),
ELT(1+(n%8),'OA','ERP','CRM','HRM','MES','SCM','WMS','KMS'),
ELT(1+(n%3),'待审批','已通过','已驳回'),
ROUND(100 + ((n*13) % 9000), 2),
DATE_ADD('2023-01-01 00:00:00', INTERVAL (n % 700) DAY)
FROM seq;
-- KMS 知识库文档(KMS,1,000,作者=员工,tags 为 JSON)
DROP TABLE IF EXISTS cj_kms_doc;
CREATE TABLE cj_kms_doc(
doc_id INT PRIMARY KEY,
title VARCHAR(120),
body VARCHAR(200),
author_emp_id INT,
tags JSON
) ENGINE=InnoDB STATS_PERSISTENT=1 STATS_AUTO_RECALC=1;
INSERT INTO cj_kms_doc (doc_id, title, body, author_emp_id, tags)
WITH RECURSIVE seq(n) AS (
SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n < 1000
)
SELECT n, CONCAT('知识库文档#', n),
CONCAT('关于珠宝', ELT(1+(n%4),'钻石','黄金','铂金','彩宝'),'的工艺标准文档'),
1 + (n % 500),
JSON_OBJECT('topic', ELT(1+(n%4),'钻石','黄金','铂金','彩宝'), 'lang', ELT(1+(n%3),'zh','en','ja'), 'views', (n*7 % 999))
FROM seq;
-- 交易事实(100,000 行):八域 × 八国,跨 2023-2025,含本地营业日生成列
DROP TABLE IF EXISTS cj_txn;
CREATE TABLE cj_txn(
txn_id BIGINT PRIMARY KEY,
system_code CHAR(3) NOT NULL,
customer_id INT NOT NULL,
product_id INT NOT NULL,
supplier_id INT NOT NULL,
wh_id INT NOT NULL,
emp_id INT NOT NULL,
country_code CHAR(2) NOT NULL,
currency_code CHAR(3) NOT NULL,
amount_orig DECIMAL(12,2) NOT NULL,
amount_usd DECIMAL(14,4) NOT NULL,
created_ts DATETIME NOT NULL,
create_local_date DATE NOT NULL,
note TEXT,
INDEX idx_cust (customer_id),
INDEX idx_created (created_ts),
INDEX idx_prod (product_id),
INDEX idx_country (country_code),
INDEX idx_sys_local (system_code, create_local_date),
INDEX idx_sys_amt (system_code, amount_usd),
INDEX idx_cov (system_code, create_local_date, amount_usd)
) ENGINE=InnoDB STATS_PERSISTENT=1 STATS_AUTO_RECALC=1;
INSERT INTO cj_txn (txn_id, system_code, customer_id, product_id, supplier_id, wh_id, emp_id,
country_code, currency_code, amount_orig, amount_usd, created_ts, create_local_date, note)
WITH RECURSIVE seq(n) AS (
SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n < 100000
)
SELECT n, sys, cid, pid, sid, wid, eid, cty, cur, aorig, ausd, cts, cld, NULL
FROM (
SELECT n, sys, cid, pid, sid, wid, eid,
-- 国家/币种由 cty_idx 推导(idx: 0=CN 1=HK 2=US 3=GB 4=JP 5=FR 6=DE 7=SG)
ELT(1 + cty_idx, 'CN','HK','US','GB','JP','FR','DE','SG') AS cty,
ELT(1 + cty_idx, 'CNY','HKD','USD','GBP','JPY','EUR','EUR','SGD') AS cur,
aorig, cts,
-- 币种跟随客户国换算成 USD(贴近真实),与时间戳无关,便于金额过滤演示
ROUND(aorig * CASE cty_idx WHEN 0 THEN 1/7.20 WHEN 1 THEN 1/7.80 WHEN 2 THEN 1.0
WHEN 3 THEN 1.27 WHEN 4 THEN 0.0067 WHEN 5 THEN 1.08
WHEN 6 THEN 1.08 WHEN 7 THEN 1/1.35 END, 4) AS ausd,
-- 本地营业日:按客户国时区偏移把 UTC 时间戳转成当地日期(CONVERT_TZ 本机不可用,用 TIMESTAMPADD)
DATE(TIMESTAMPADD(MINUTE,
CASE cty_idx WHEN 0 THEN 480 WHEN 1 THEN 480 WHEN 2 THEN -300 WHEN 3 THEN 0
WHEN 4 THEN 540 WHEN 5 THEN 60 WHEN 6 THEN 60 WHEN 7 THEN 480 END,
cts)) AS cld
FROM (
SELECT
n,
ELT(1 + (n % 8), 'OA','ERP','CRM','HRM','MES','SCM','WMS','KMS') AS sys,
1 + (n % 10000) AS cid,
1 + (n % 2000) AS pid,
1 + ((1 + (n % 2000)) % 200) AS sid,
1 + ((n DIV 8) % 50) AS wid,
1 + ((n DIV 37) % 500) AS eid,
-- 交易国家 = 客户所属国(与 cj_customer.country_code 公式一致,便于 JOIN 演示)
((1 + (n % 10000)) * 3) % 8 AS cty_idx,
ROUND(1.00 + ((n*7) % 9000)/100.0, 2) AS aorig,
DATE_ADD('2023-01-01 00:00:00', INTERVAL (n % 1095) DAY) + INTERVAL (n % 24) HOUR AS cts
FROM seq
) q1
) q2;
-- V1 / V2 基础断言
SELECT COUNT(*) INTO @v1 FROM cj_dim_currency;
CALL cj_v('V1_维度币种就绪', IF(@v1=7,1,0), CONCAT('currency_rows=',@v1));
SELECT COUNT(*) INTO @v2 FROM cj_txn;
CALL cj_v('V2_事实造数100k', IF(@v2=100000,1,0), CONCAT('txn_rows=',@v2));
-- ============================================================================
-- §03 多表 JOIN:显式别名 + 限定列名 + 五表联表(珠宝八域交易明细)
-- ============================================================================
SELECT '§03 多表 JOIN:显式别名 + 限定列名 + 五表联表(SCM 域交易明细)' AS `-- 第3章 --`;
-- 3.1 五表 JOIN:交易 ← 客户/商品/供应商/仓库,员工用 LEFT JOIN(允许无经手人)
SELECT t.txn_id, t.system_code, c.name AS cust_name, p.product_name,
s.supplier_name, w.wh_name, e.name AS emp_name, t.amount_usd
FROM cj_txn t
JOIN cj_customer c ON c.customer_id = t.customer_id
JOIN cj_product p ON p.product_id = t.product_id
JOIN cj_supplier s ON s.supplier_id = p.supplier_id
JOIN cj_warehouse w ON w.wh_id = t.wh_id
LEFT JOIN cj_employee e ON e.emp_id = t.emp_id
WHERE t.system_code = 'SCM' AND t.amount_usd > 200
LIMIT 15;
-- 3.2 分组聚合 JOIN:品类 × 客户国 的 GMV(索引 idx_sys_local + 维度 JOIN)
SELECT p.category, c.country_code, COUNT(*) AS txns, SUM(t.amount_usd) AS gmv_usd
FROM cj_txn t
JOIN cj_customer c ON c.customer_id = t.customer_id
JOIN cj_product p ON p.product_id = t.product_id
WHERE t.create_local_date >= '2024-01-01'
GROUP BY p.category, c.country_code
ORDER BY gmv_usd DESC
LIMIT 15;
-- V3 五表 JOIN 有效性(SCM 行的五表联表应 > 0)
SELECT COUNT(*) INTO @v3 FROM cj_txn t
JOIN cj_customer c ON c.customer_id = t.customer_id
JOIN cj_product p ON p.product_id = t.product_id
JOIN cj_supplier s ON s.supplier_id = p.supplier_id
JOIN cj_warehouse w ON w.wh_id = t.wh_id
WHERE t.system_code = 'SCM';
CALL cj_v('V3_五表JOIN有效', IF(@v3>0,1,0), CONCAT('SCM五表JOIN行=',@v3));
-- ============================================================================
-- §04 CTE 分层:把复杂查询拆成可读的「管道」,并便于优化器物化
-- ============================================================================
SELECT '§04 CTE 分层:base(过滤+JOIN) → by_cat(聚合) → 终查' AS `-- 第4章 --`;
-- 4.1 逐层分解:base 只做「连表+时间过滤」,by_cat 只做「按品类×国聚合」
SELECT * FROM (
WITH base AS (
SELECT t.txn_id, t.product_id, t.country_code AS txn_country,
t.amount_usd, t.create_local_date, p.category, c.vip, c.country_code AS cust_country
FROM cj_txn t
JOIN cj_product p ON p.product_id = t.product_id
JOIN cj_customer c ON c.customer_id = t.customer_id
WHERE t.create_local_date >= '2024-01-01'
),
by_cat AS (
SELECT category, txn_country,
COUNT(*) AS txns, SUM(amount_usd) AS gmv_usd, AVG(amount_usd) AS avg_usd
FROM base
GROUP BY category, txn_country
)
SELECT * FROM by_cat ORDER BY gmv_usd DESC LIMIT 20
) z;
-- V4 CTE 分层聚合:返回 4 个品类、且行数 > 0
DROP TEMPORARY TABLE IF EXISTS cj_cte_out;
CREATE TEMPORARY TABLE cj_cte_out(category VARCHAR(8), gmv DECIMAL(20,4));
INSERT INTO cj_cte_out
WITH base AS (
SELECT p.category, t.amount_usd
FROM cj_txn t JOIN cj_product p ON p.product_id = t.product_id
WHERE t.create_local_date >= '2024-01-01'
),
by_cat AS (SELECT category, SUM(amount_usd) AS gmv FROM base GROUP BY category)
SELECT category, gmv FROM by_cat;
SELECT COUNT(*) INTO @v4a FROM cj_cte_out;
SELECT COUNT(DISTINCT category) INTO @v4b FROM cj_cte_out;
CALL cj_v('V4_CTE分层聚合', IF(@v4a>0 AND @v4b=4,1,0), CONCAT('CTE聚合行=',@v4a,' 品类=',@v4b));
-- ============================================================================
-- §05 子查询全家桶:标量 / 派生表 / EXISTS 反连接 / IN 子查询 / 维度扩展 JOIN
-- ============================================================================
SELECT '§05 子查询全家桶:标量·派生表·EXISTS反连接·IN子查询' AS `-- 第5章 --`;
-- 5.1 标量子查询(SELECT 列表里引用全库聚合)
SELECT p.category,
SUM(t.amount_usd) AS cat_gmv,
(SELECT SUM(amount_usd) FROM cj_txn) AS total_gmv,
ROUND(SUM(t.amount_usd) / (SELECT SUM(amount_usd) FROM cj_txn) * 100, 2) AS pct_of_total
FROM cj_txn t JOIN cj_product p ON p.product_id = t.product_id
GROUP BY p.category;
-- 5.2 派生表(FROM 中子查询)+ 标量子查询过滤「高于均值」的国家 GMV
SELECT d.country_code, d.gmv_usd
FROM (
SELECT t.country_code, SUM(t.amount_usd) AS gmv_usd
FROM cj_txn t GROUP BY t.country_code
) d
WHERE d.gmv_usd > (
SELECT AVG(gmv_usd) FROM (
SELECT SUM(amount_usd) AS gmv_usd FROM cj_txn GROUP BY country_code
) x)
ORDER BY d.gmv_usd DESC;
-- 5.3 EXISTS 反连接:从未提交 OA 请购单的员工(LEFT JOIN ... IS NULL 等价)
SELECT e.emp_id, e.name
FROM cj_employee e
WHERE NOT EXISTS (SELECT 1 FROM cj_oa_requisition r WHERE r.applicant_emp_id = e.emp_id)
ORDER BY e.emp_id LIMIT 20;
SELECT e.emp_id, e.name
FROM cj_employee e
LEFT JOIN cj_oa_requisition r ON r.applicant_emp_id = e.emp_id
WHERE r.req_id IS NULL
ORDER BY e.emp_id LIMIT 20;
-- 5.4 IN 子查询:有 OA 请购单的员工所经手的系统交易笔数
SELECT t.system_code, COUNT(*) AS txns
FROM cj_txn t
WHERE t.emp_id IN (SELECT DISTINCT applicant_emp_id FROM cj_oa_requisition)
GROUP BY t.system_code;
-- 5.5 维度扩展 JOIN:从未提交 OA 的员工,其撰写的 KMS 文档(知识沉淀缺口)
SELECT e.emp_id, e.name, COUNT(d.doc_id) AS kms_docs
FROM cj_employee e
LEFT JOIN cj_kms_doc d ON d.author_emp_id = e.emp_id
WHERE NOT EXISTS (SELECT 1 FROM cj_oa_requisition r WHERE r.applicant_emp_id = e.emp_id)
GROUP BY e.emp_id, e.name
ORDER BY kms_docs DESC LIMIT 15;
-- V5 反连接两法一致(NOT EXISTS == LEFT JOIN IS NULL)
SELECT COUNT(*) INTO @v5a FROM cj_employee e
WHERE NOT EXISTS (SELECT 1 FROM cj_oa_requisition r WHERE r.applicant_emp_id = e.emp_id);
SELECT COUNT(*) INTO @v5b FROM cj_employee e
LEFT JOIN cj_oa_requisition r ON r.applicant_emp_id = e.emp_id
WHERE r.req_id IS NULL;
CALL cj_v('V5_反连接两法一致', IF(@v5a=@v5b AND @v5a>0,1,0),
CONCAT('NOT EXISTS=',@v5a,' LEFT JOIN NULL=',@v5b,'(员工401-500无请购单)'));
-- V7 标量子查询(全库 GMV)> 0
SELECT (SELECT SUM(amount_usd) FROM cj_txn) INTO @v7;
CALL cj_v('V7_标量子查询', IF(@v7>0,1,0), CONCAT('全库GMV_usd=',@v7));
-- ============================================================================
-- §06 分解:把中间结果物化成「普通表」一次,避免大查询里反复重算
-- ============================================================================
SELECT '§06 分解:物化中间表(普通表) 一次计算多次复用' AS `-- 第6章 --`;
-- 6.1 物化「商品×GMV」到普通表(注意:必须用普通表,不能用临时表,见 6.3)
DROP TABLE IF EXISTS cj_prod_gmv;
CREATE TABLE cj_prod_gmv(
category VARCHAR(8) NOT NULL,
product_id INT NOT NULL,
product_name VARCHAR(120) NOT NULL,
gmv DECIMAL(20,4) NOT NULL,
PRIMARY KEY (product_id),
INDEX idx_cat_gmv (category, gmv)
) ENGINE=InnoDB;
INSERT INTO cj_prod_gmv
SELECT p.category, p.product_id, p.product_name, SUM(t.amount_usd) AS gmv
FROM cj_product p JOIN cj_txn t ON t.product_id = p.product_id
GROUP BY p.category, p.product_id, p.product_name;
SELECT COUNT(*) AS `商品GMV物化行数(应=2000)` FROM cj_prod_gmv;
-- 6.2 从物化表算窗口 Top-N(极快)
SELECT category, product_name, gmv FROM (
SELECT category, product_name, gmv, RANK() OVER (PARTITION BY category ORDER BY gmv DESC) AS rnk
FROM cj_prod_gmv
) x WHERE rnk <= 3 ORDER BY category, gmv DESC;
-- 6.3 陷阱演示:相关子查询若建立在「临时表」上 → 报错 1137(临时表不能在一句里二次引用)
DROP TEMPORARY TABLE IF EXISTS cj_tmp_gmv;
CREATE TEMPORARY TABLE cj_tmp_gmv LIKE cj_prod_gmv;
INSERT INTO cj_tmp_gmv SELECT * FROM cj_prod_gmv;
CALL cj_err_probe('temp表不能二次引用(1137)',
'SELECT COUNT(*) FROM cj_tmp_gmv pg WHERE (SELECT COUNT(*) FROM cj_tmp_gmv pg2 WHERE pg2.category=pg.category AND pg2.gmv>=pg.gmv)<=3');
SELECT label, errno, msg FROM cj_demo_probe WHERE label='temp表不能二次引用(1137)';
-- ============================================================================
-- §07 每组 Top-N:相关子查询(贵) vs 窗口函数(廉) ------ 执行代价实测
-- ============================================================================
SELECT '§07 每组 Top-N:相关子查询(贵) vs 窗口函数(廉) --- 读行代价实测' AS `-- 第7章 --`;
-- 7.1 相关子查询:对 2000 行商品逐行重算「同品类比我大的有几个」→ 每行扫一遍全品类
CALL cj_snap();
SELECT category, product_name, gmv
FROM cj_prod_gmv pg
WHERE (
SELECT COUNT(*) FROM cj_prod_gmv pg2
WHERE pg2.category = pg.category AND pg2.gmv >= pg.gmv
) <= 3
ORDER BY category, gmv DESC;
CALL cj_measure('CORR_TOP3');
-- 7.2 窗口函数:单次遍历,按品类排名
CALL cj_snap();
SELECT category, product_name, gmv
FROM (
SELECT category, product_name, gmv, RANK() OVER (PARTITION BY category ORDER BY gmv DESC) AS rnk
FROM cj_prod_gmv
) x WHERE rnk <= 3
ORDER BY category, gmv DESC;
CALL cj_measure('WIN_TOP3');
-- 7.3 落地为两张结果表,证明两种写法「结果完全一致」
DROP TEMPORARY TABLE IF EXISTS cj_top_cor;
CREATE TEMPORARY TABLE cj_top_cor(category VARCHAR(8), product_name VARCHAR(120), gmv DECIMAL(20,4));
INSERT INTO cj_top_cor
SELECT category, product_name, gmv FROM cj_prod_gmv pg
WHERE (SELECT COUNT(*) FROM cj_prod_gmv pg2 WHERE pg2.category=pg.category AND pg2.gmv>=pg.gmv)<=3;
DROP TEMPORARY TABLE IF EXISTS cj_top_win;
CREATE TEMPORARY TABLE cj_top_win(category VARCHAR(8), product_name VARCHAR(120), gmv DECIMAL(20,4));
INSERT INTO cj_top_win
SELECT category, product_name, gmv FROM (
SELECT category, product_name, gmv, RANK() OVER (PARTITION BY category ORDER BY gmv DESC) AS rnk
FROM cj_prod_gmv
) x WHERE rnk<=3;
SELECT COUNT(*) INTO @cc FROM cj_top_cor;
SELECT COUNT(*) INTO @cw FROM cj_top_win;
SELECT COUNT(*) INTO @eq FROM (
SELECT category, product_name, gmv FROM cj_top_cor
UNION
SELECT category, product_name, gmv FROM cj_top_win
) u;
CALL cj_v('V6_相关子查询=窗口结果', IF(@eq=@cc AND @cc=@cw AND @cc>0,1,0),
CONCAT('相关行=',@cc,' 窗口行=',@cw,' 并集=',@eq,'(集合相同)'));
-- 7.4 代价对比:相关子查询读行 >> 窗口函数
SELECT IFNULL((SELECT innodb_rows FROM cj_demo_metric WHERE tag='CORR_TOP3'),0) INTO @rc;
SELECT IFNULL((SELECT innodb_rows FROM cj_demo_metric WHERE tag='WIN_TOP3'),0) INTO @rw;
CALL cj_v('V9_窗口远省读行', IF(@rc>@rw,1,0),
CONCAT('相关读行=',@rc,' 窗口读行=',@rw,'(相关多读约',@rc-@rw,'行)'));
-- 7.5 物化表 == CTE 直算(分解不改变结果)
DROP TEMPORARY TABLE IF EXISTS cj_top_cte;
CREATE TEMPORARY TABLE cj_top_cte(category VARCHAR(8), product_name VARCHAR(120), gmv DECIMAL(20,4));
INSERT INTO cj_top_cte
WITH pg AS (
SELECT p.category, p.product_id, p.product_name, SUM(t.amount_usd) AS gmv
FROM cj_product p JOIN cj_txn t ON t.product_id = p.product_id
GROUP BY p.category, p.product_id, p.product_name
)
SELECT category, product_name, gmv FROM (
SELECT category, product_name, gmv, RANK() OVER (PARTITION BY category ORDER BY gmv DESC) AS rnk FROM pg
) x WHERE rnk<=3;
SELECT COUNT(*) INTO @cc2 FROM cj_top_cte;
SELECT COUNT(*) INTO @eq2 FROM (
SELECT category, product_name, gmv FROM cj_top_cte
UNION
SELECT category, product_name, gmv FROM cj_top_win
) u;
CALL cj_v('V8_物化=CTE直算', IF(@eq2=@cc2 AND @cc2>0,1,0),
CONCAT('CTE直算行=',@cc2,' 并集=',@eq2,'(分解结果一致)'));
-- ============================================================================
-- §08 执行计划评审:EXPLAIN ANALYZE 真实计时 + 两种写法读行对比
-- ============================================================================
SELECT '§08 执行计划评审:EXPLAIN ANALYZE 真实行数/耗时 + 两种写法读行对比' AS `-- 第8章 --`;
-- 8.1 对五表 JOIN 看真实执行计划(actual rows / time)
EXPLAIN ANALYZE
SELECT p.category, c.country_code, COUNT(*) AS txns, SUM(t.amount_usd) AS gmv_usd
FROM cj_txn t
JOIN cj_customer c ON c.customer_id = t.customer_id
JOIN cj_product p ON p.product_id = t.product_id
WHERE t.create_local_date >= '2024-01-01'
GROUP BY p.category, c.country_code;
-- 8.2 同一语义两种写法:相关标量子查询(逐客户) vs GROUP BY JOIN,比较读行与结果
CALL cj_snap();
DROP TEMPORARY TABLE IF EXISTS cj_a1;
CREATE TEMPORARY TABLE cj_a1(customer_id INT, n BIGINT);
INSERT INTO cj_a1
SELECT c.customer_id, (SELECT COUNT(*) FROM cj_txn t WHERE t.customer_id = c.customer_id)
FROM cj_customer c WHERE c.vip = 1;
CALL cj_measure('APPROACH_CORR');
CALL cj_snap();
DROP TEMPORARY TABLE IF EXISTS cj_a2;
CREATE TEMPORARY TABLE cj_a2(customer_id INT, n BIGINT);
INSERT INTO cj_a2
SELECT c.customer_id, COUNT(t.txn_id)
FROM cj_customer c LEFT JOIN cj_txn t ON t.customer_id = c.customer_id
WHERE c.vip = 1 GROUP BY c.customer_id;
CALL cj_measure('APPROACH_JOIN');
SELECT SUM(n) INTO @a1s FROM cj_a1;
SELECT SUM(n) INTO @a2s FROM cj_a2;
SELECT COUNT(*) INTO @a1c FROM cj_a1;
SELECT COUNT(*) INTO @a2c FROM cj_a2;
CALL cj_v('V_计划评审两法一致', IF(@a1s=@a2s AND @a1c=@a2c AND @a1c>0,1,0),
CONCAT('相关法行=',@a1c,' 合计=',@a1s,' JOIN法行=',@a2c,' 合计=',@a2s));
SELECT tag, innodb_rows, ms FROM cj_demo_metric WHERE tag IN ('APPROACH_CORR','APPROACH_JOIN');
-- ============================================================================
-- §09 国际时区 + 多币种:本地营业日过滤 + 仅 SUM(amount_usd)
-- ============================================================================
SELECT '§09 国际时区 + 多币种:本地营业日索引 + 仅 USD 归一结算' AS `-- 第9章 --`;
-- 9.1 多币种展示(JOIN 维度表取符号),但结算只用 amount_usd
SELECT t.txn_id, dc.symbol, t.amount_orig, dc.code, t.amount_usd
FROM cj_txn t JOIN cj_dim_currency dc ON dc.code = t.currency_code
WHERE t.system_code = 'CRM' LIMIT 10;
SELECT '结算只 SUM(amount_usd),避免跨币种直接相加(amount_orig)' AS `规则`;
-- 9.2 本地营业日范围过滤(system_code 前导 + create_local_date 范围,走 idx_sys_local)
CALL cj_snap();
SELECT SUM(amount_usd) INTO @local_gmv FROM cj_txn
WHERE system_code='SCM' AND create_local_date BETWEEN '2024-06-10' AND '2024-06-20';
CALL cj_measure('LOCAL_day');
SELECT IFNULL((SELECT innodb_rows FROM cj_demo_metric WHERE tag='LOCAL_day'),0) INTO @rl;
SELECT COUNT(*) INTO @v11b FROM cj_txn;
-- V11 本地日过滤有命中且只 SUM USD;本地日过滤读行应远小于全表
CALL cj_v('V11_本地日过滤+USD结算', IF(@local_gmv>0 AND @rl<@v11b,1,0),
CONCAT('SCM_2024-06区间_usd=',@local_gmv,' 读行=',@rl,' 全表=',@v11b));
-- ============================================================================
-- §10 别名最佳实践:限定列名避免歧义(1052)
-- ============================================================================
SELECT '§10 别名最佳实践:显式表别名 + 限定列名,消除歧义列(1052)' AS `-- 第10章 --`;
-- 10.1 反例:两表都有 country_code,不限定 → 报错 1052
CALL cj_err_probe('歧义列名(1052)',
'SELECT country_code FROM cj_txn JOIN cj_customer ON cj_customer.customer_id=cj_txn.customer_id');
SELECT label, errno, msg FROM cj_demo_probe WHERE label='歧义列名(1052)';
-- 10.2 正例:短别名 + 限定列名,清晰无歧义
SELECT t.txn_id, c.country_code AS cust_country, t.country_code AS txn_country
FROM cj_txn t JOIN cj_customer c ON c.customer_id = t.customer_id
WHERE t.system_code = 'SCM' LIMIT 5;
SELECT errno INTO @v10 FROM cj_demo_probe WHERE label='歧义列名(1052)' LIMIT 1;
CALL cj_v('V10_歧义列名被捕', IF(@v10=1052,1,0), CONCAT('errno=',@v10,'(需表别名限定)'));
-- ============================================================================
-- §11 探针:MySQL 9.0 连接/子查询的「不可用」边界(捕获真实报错)
-- ============================================================================
SELECT '§11 探针:连接/子查询边界(1137 / 1242 已被捕获)' AS `-- 第11章 --`;
-- 11.1 标量子查询返回多行 → 报错 1242(必须与聚合/限制配合保证单行)
CALL cj_err_probe('标量子查询多行(1242)', 'SELECT (SELECT customer_id FROM cj_customer LIMIT 5)');
SELECT label, errno, msg FROM cj_demo_probe WHERE label='标量子查询多行(1242)';
-- 11.2 1137 与 1242 已分别在 §06 / §11 捕获,下面统一做边界断言
SELECT errno INTO @e1137 FROM cj_demo_probe WHERE label='temp表不能二次引用(1137)' LIMIT 1;
SELECT errno INTO @e1242 FROM cj_demo_probe WHERE label='标量子查询多行(1242)' LIMIT 1;
CALL cj_v('V12_边界探针', IF(@e1137=1137 AND @e1242=1242,1,0),
CONCAT('1137=',@e1137,' 1242=',@e1242));
-- ============================================================================
-- §12 结论 / 清单
-- ============================================================================
SELECT '§12 结论' AS `-- 第12章 --`;
SELECT '1) 多表 JOIN 必须写显式别名并限定列名(t.col),否则歧义列报错 1052。' AS `结论1`;
SELECT '2) CTE(WITH) 把复杂查询拆成可读管道;可被多次引用,优化器可能物化。' AS `结论2`;
SELECT '3) 子查询:标量(返回多行→1242)/派生表(必加别名)/EXISTS反连接(=LEFT JOIN IS NULL)/IN(常改半连接)。' AS `结论3`;
SELECT '4) 每组 Top-N 用窗口函数(ROW_NUMBER/RANK)单次遍历;相关子查询每行重扫→读行数百倍(本机实测 1,002,000 vs 2,000)。' AS `结论4`;
SELECT '5) 大查询分解:把中间结果物化成「普通表」一次计算多次复用;临时表不能在一句里二次引用(1137)。' AS `结论5`;
SELECT '6) 执行计划评审用 EXPLAIN ANALYZE 看真实行数/耗时;INNODB_ROWS_READ 增量量化代价。' AS `结论6`;
SELECT '7) 时区用生成列,结算只 SUM(USD);跨币种永不直接 SUM(amount_orig)。别名限定消除 1052。' AS `结论7`;
SELECT '---- 待办/延伸 ----' AS `延伸`;
SELECT '① 万亿级优先窗口/物化,少用相关子查询;② 反连接优先 EXISTS/LEFT JOIN NULL;' AS `清单`;
SELECT '③ 多表 JOIN 建好连接键索引(外键列);④ 派生表过重时改用物化表或 CTE。' AS `清单2`;
-- ============================================================================
-- §15 自检汇总(V1--V12 全部应为 1)
-- ============================================================================
SELECT '§15 自检汇总 V1--V12(passed=1 即通过)' AS `-- 第15章 --`;
SELECT check_id, passed, detail FROM cj_demo_result ORDER BY check_id;
SELECT CONCAT('通过 ', SUM(passed), ' / 共 ', COUNT(*), ' 项') AS `汇总`
FROM cj_demo_result;