sql: JSON and XML Data Handling in SQL using mysql 9.0
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 JSON / XML 半结构化数据处理专题
-- 主题:国际化珠宝集团 OA/ERP/CRM/HRM/MES/SCM/WMS/KMS 八域 JSON/XML
-- + JSON 存储/查询/索引/校验/拆解/修改 + XML 处理边界(MySQL 无原生 XML)
-- + 万亿高并发 + 国际时区 + 多币种结算
-- 运行前提:MySQL 9.0.x,库 JewelryAnalytics 自动建/用;本脚本完全幂等。
-- 方式A(命令行,推荐,避开 Workbench 30s 读超时):
-- mysql -uroot -p --default-character-set=utf8mb4 < MySQL9_JSON_XML_JewelryDemo.sql
-- 方式B(Workbench):整段执行。
-- 幂等说明:每次先 DROP 全部 js_* 实验表/过程再重建并造数,重复运行结果一致。
-- 度量口径:用 performance_schema.session_status 的 INNODB_ROWS_READ 增量,
-- 量化「引擎真正读了多少行」,比 EXPLAIN rows 更接近真实代价。
-- ============================================================================
-- §0.1 MySQL JSON/XML 能力速览(与其它数据库的术语映射)
-- ----------------------------------------------------------------------------
-- SQL Server / 通用概念 MySQL 9.0 等价 / 真相
-- --------------------------- -------------------------------------------------
-- JSON 列类型 JSON 原生类型(校验、二进制存储、内联部分更新)
-- column->'$.path' JSON_EXTRACT 简写;->> 自动去引号
-- JSON_VALUE(col,'$.path') col->>'$.path'
-- JSON_QUERY(col,'$.arr') col->'$.arr'(保留 JSON,不拆引号)
-- 多值数组索引 CREATE INDEX ... ((CAST(col->'$.arr' AS .. ARRAY))) 多值索引
-- OPENJSON / JSON_TABLE JSON_TABLE(col, '$.path[*]' COLUMNS(...)) 炸行
-- ISJSON(col) JSON_VALID(col)
-- JSON 校验(Schema) JSON_SCHEMA_VALID(schema, doc)(8.0.17+)
-- JSON_MODIFY JSON_SET / JSON_INSERT / JSON_REPLACE / JSON_REMOVE
-- XML 类型 / XMLTABLE ❌ MySQL 9.0 无 XML 类型、无 XMLTABLE(实测 1064 不存在)
-- EXTRACTVALUE(XPath标量) ✅ 本机 9.0.1 实测仍在(XPath 1.0 子集,仅抽单值,无集合/无索引)
-- (XMLTABLE 没有;EXTRACTVALUE 有但功能弱)
-- ============================================================================
-- §0.2 反模式速查表(本机已实测)
-- ----------------------------------------------------------------------------
-- 反模式 后果 正确做法
-- 在 JSON 列上直接建二级索引 报错(只能对生成列/标量建索引) 抽生成列再建索引
-- 热路径用 CAST(specs->>'$.x')=v 过滤 全 JSON 扫描(万亿级灾难) 生成列 + 二级索引
-- 数组包含查询用 JSON_SEARCH 全扫 不可走索引,O(n) 多值索引 + MEMBER OF
-- 误以为 XMLTABLE 可用 实测 1064 不存在,EXTRACTVALUE 仅抽单值 XML 存 TEXT,ETL 转 JSON/关系化
-- 用 EXTRACTVALUE 抽数组/多值 仅返回首个匹配,无集合语义 需要集合用 JSON_TABLE 转 JSON 再炸行
-- 用 JSON 存高频聚合字段 行膨胀、Buffer Pool 浪费 抽成关系列或侧表
-- JSON 不校验直接入库 脏数据(缺字段/类型错) JSON_SCHEMA_VALID 入库前校验
-- CONVERT_TZ(ts,'UTC',name) 本机 mysql.time_zone_name=0 → NULL 用 TIMESTAMPADD(MINUTE,偏移,ts)
-- 跨币种 SUM(amount_orig) 不同币种相加无意义 只 SUM(price_usd)
-- ============================================================================
-- §01 度量框架(js_demo_*):量化引擎真实读行,证明取舍
DROP TABLE IF EXISTS js_demo_metric;
CREATE TABLE js_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 js_demo_result;
CREATE TABLE js_demo_result(
check_id VARCHAR(30) PRIMARY KEY,
passed TINYINT NOT NULL,
detail VARCHAR(400)
);
DROP TABLE IF EXISTS js_demo_probe;
CREATE TABLE js_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 js_snap;
DELIMITER //
CREATE PROCEDURE js_snap()
BEGIN
SELECT VARIABLE_VALUE INTO @js_snap_ir FROM performance_schema.session_status WHERE VARIABLE_NAME='INNODB_ROWS_READ';
SELECT VARIABLE_VALUE INTO @js_snap_rn FROM performance_schema.session_status WHERE VARIABLE_NAME='HANDLER_READ_RND_NEXT';
SELECT VARIABLE_VALUE INTO @js_snap_rd FROM performance_schema.session_status WHERE VARIABLE_NAME='HANDLER_READ_RND';
SET @js_snap_ts = UNIX_TIMESTAMP(NOW(6));
END//
DELIMITER ;
DROP PROCEDURE IF EXISTS js_measure;
DELIMITER //
CREATE PROCEDURE js_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)) - @js_snap_ts)*1000);
INSERT INTO js_demo_metric(tag,innodb_rows,rnd_next,rnd,ms) VALUES(p_tag, v_ir-@js_snap_ir, v_rn-@js_snap_rn, v_rd-@js_snap_rd, v_ms)
ON DUPLICATE KEY UPDATE innodb_rows=v_ir-@js_snap_ir, rnd_next=v_rn-@js_snap_rn, rnd=v_rd-@js_snap_rd, ms=v_ms;
END//
DELIMITER ;
DROP PROCEDURE IF EXISTS js_v;
DELIMITER //
CREATE PROCEDURE js_v(IN p_check VARCHAR(30), IN p_pass TINYINT, IN p_detail VARCHAR(400))
BEGIN
INSERT INTO js_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 js_err_probe;
DELIMITER //
CREATE PROCEDURE js_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 @js_probe_sql = p_sql;
PREPARE js_pstmt FROM @js_probe_sql;
IF v_no = 0 THEN
EXECUTE js_pstmt;
DEALLOCATE PREPARE js_pstmt;
END IF;
INSERT INTO js_demo_probe(label,errno,sql_state,msg) VALUES(p_label, v_no, v_st, v_msg);
END//
DELIMITER ;
-- §02 实验台:维度表 + 八域 JSON 仓 + 造数(国际化珠宝集团)
-- 命名统一 js_* 前缀,避免与同库其他主题 demo_*/fts_*/samp_*/gl_*/dim_* 表冲突。
DROP TABLE IF EXISTS js_dim_currency;
CREATE TABLE js_dim_currency(code CHAR(3) PRIMARY KEY, name VARCHAR(40), symbol VARCHAR(8), usd_rate DECIMAL(12,6));
INSERT INTO js_dim_currency VALUES
('CNY','人民币','¥',1/7.20),('USD','美元','$',1.0),('EUR','欧元','€',1.08),
('JPY','日元','¥',0.0067),('HKD','港元','HK$',1/7.80),('GBP','英镑','£',1.27);
DROP TABLE IF EXISTS js_dim_country;
CREATE TABLE js_dim_country(code CHAR(2) PRIMARY KEY, name VARCHAR(40), tz_offset_min INT, currency CHAR(3));
INSERT INTO js_dim_country VALUES
('CN','中国',480,'CNY'),('US','美国',-300,'USD'),('FR','法国',60,'EUR'),
('JP','日本',540,'JPY'),('HK','中国香港',480,'HKD'),('GB','英国',0,'GBP'),
('DE','德国',60,'EUR'),('SG','新加坡',480,'USD');
-- 八域统一商品/物料仓:ERP(商品规格) / MES(工艺参数) / SCM(供货条款) / WMS(批次属性)
-- 均用 JSON 存半结构化规格;生成列抽热点标量建索引;tags 数组用多值索引。
DROP TABLE IF EXISTS js_product;
CREATE TABLE js_product(
id BIGINT PRIMARY KEY,
system_code CHAR(4) NOT NULL, -- OA/ERP/CRM/HRM/MES/SCM/WMS/KMS
sku VARCHAR(20) NOT NULL,
name VARCHAR(240) NOT NULL,
specs JSON NOT NULL, -- 半结构化规格
price_orig DECIMAL(18,2),
currency_code CHAR(3),
price_usd DECIMAL(18,2), -- 已按汇率归一,结算只用它
country_code CHAR(2),
created_ts DATETIME NOT NULL, -- 统一存 UTC
create_offset_min SMALLINT, -- 时区偏移(分钟),冗余存储以便生成列建索引
create_local_date DATE -- 生成列:本地营业日,用于带索引的时区过滤
GENERATED ALWAYS AS (DATE(TIMESTAMPADD(MINUTE, create_offset_min, created_ts))) STORED,
-- 生成列:从 JSON 抽热点标量,可建二级索引(JSON 列本身不能直接建索引)
gen_carat DECIMAL(6,3) GENERATED ALWAYS AS (CAST(specs->>'$.carat' AS DECIMAL(6,3))) STORED,
gen_cert VARCHAR(12) GENERATED ALWAYS AS (specs->>'$.cert') STORED,
KEY ix_system (system_code),
KEY ix_carat (gen_carat),
KEY ix_cert (gen_cert),
KEY ix_local_date (create_local_date),
KEY ix_usd (price_usd)
) ENGINE=InnoDB;
-- 造 100,000 行(250×400),八域、多国、多币种、JSON 规格
INSERT INTO js_product(id,system_code,sku,name,specs,price_orig,currency_code,price_usd,country_code,created_ts,create_offset_min)
WITH RECURSIVE a(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM a WHERE n<250),
b(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM b WHERE n<400),
base AS (SELECT a.n*400 + b.n - 400 AS id FROM a JOIN b)
SELECT
id,
ELT(1+id%8,'OA','ERP','CRM','HRM','MES','SCM','WMS','KMS'),
CONCAT('SKU',LPAD(id,8,'0')),
CONCAT(ELT(1+id%10,'黄金','铂金','钻石','红宝石','蓝宝石','翡翠','珍珠','银','18K金','白金'),
ELT(1+id%8,'戒指','项链','手链','耳环','吊坠','胸针','手镯','脚链')),
JSON_OBJECT(
'carat', ROUND(0.3+(id%50)/10.0,2),
'color', ELT(1+id%6,'D','E','F','G','H','I'),
'clarity', ELT(1+id%7,'IF','VVS1','VVS2','VS1','VS2','SI1','SI2'),
'cert', ELT(1+id%3,'GIA','IGI','NGTC'),
'origin', ELT(1+id%5,'南非','澳大利亚','加拿大','俄罗斯','中国'),
'tags', JSON_ARRAY(
ELT(1+id%6,'婚戒','送礼','经典','时尚','收藏','投资'),
ELT(1+(id+1)%6,'婚戒','送礼','经典','时尚','收藏','投资'),
ELT(1+(id+2)%6,'婚戒','送礼','经典','时尚','收藏','投资')
)
),
ROUND(100 + (id%9000)/10.0, 2),
ELT(1+id%8,'CNY','USD','EUR','JPY','HKD','GBP','EUR','USD'),
ROUND((100 + (id%9000)/10.0) * CASE 1+id%8
WHEN 1 THEN 1/7.20 WHEN 2 THEN 1.0 WHEN 3 THEN 1.08
WHEN 4 THEN 0.0067 WHEN 5 THEN 1/7.80 WHEN 6 THEN 1.27
WHEN 7 THEN 1.08 WHEN 8 THEN 1.0 END, 2),
ELT(1+id%8,'CN','US','FR','JP','HK','GB','DE','SG'),
DATE_ADD('2024-01-01 00:00:00', INTERVAL (id % 365) DAY),
ELT(1+id%8,480,-300,60,540,480,0,60,480)
FROM base;
-- 多值索引(8.0.17+):对 JSON 数组 tags 建索引,支持 MEMBER OF / JSON_CONTAINS / JSON_OVERLAPS 走索引
ALTER TABLE js_product ADD INDEX ix_tags ( (CAST(specs->'$.tags' AS CHAR(32) ARRAY)) );
-- CRM/HRM 客户档案:用 JSON 存偏好/渠道/等级(典型半结构化)
DROP TABLE IF EXISTS js_customer;
CREATE TABLE js_customer(
id BIGINT PRIMARY KEY,
name VARCHAR(80) NOT NULL,
profile JSON NOT NULL
) ENGINE=InnoDB;
INSERT INTO js_customer(id,name,profile)
WITH RECURSIVE a(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM a WHERE n<200)
SELECT
n,
CONCAT('客户', LPAD(n,6,'0')),
JSON_OBJECT(
'tier', ELT(1+n%4,'普通','银卡','金卡','VIP'),
'channels', JSON_ARRAY(ELT(1+n%3,'email','sms','wechat'),
ELT(1+(n+1)%3,'email','sms','wechat'),
ELT(1+(n+2)%3,'email','sms','wechat')),
'prefs', JSON_OBJECT('metal', ELT(1+n%5,'铂金','18K金','黄金','白银','钛'),
'budget', 1000 + (n%20)*500),
'loyalty', 100 + n*7
)
FROM a;
-- KMS/OA 知识库文章:用 JSON 存标签数组;部分遗留文档以 XML 文本存储(见 §09)
DROP TABLE IF EXISTS js_article;
CREATE TABLE js_article(
id BIGINT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
body_json JSON NOT NULL,
KEY ix_article_tags ( (CAST(body_json->'$.tags' AS CHAR(32) ARRAY)) )
) ENGINE=InnoDB;
INSERT INTO js_article(id,title,body_json)
WITH RECURSIVE a(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM a WHERE n<100)
SELECT
n,
CONCAT('知识库文章-', n),
JSON_OBJECT(
'category', ELT(1+n%8,'OA','ERP','CRM','HRM','MES','SCM','WMS','KMS'),
'tags', JSON_ARRAY(ELT(1+n%6,'入库','质检','审批','培训','排产','盘点'),
ELT(1+(n+1)%6,'入库','质检','审批','培训','排产','盘点')),
'views', 50 + n*3
)
FROM a;
-- XML 遗留数据:MySQL 无 XML 类型,只能存为 TEXT/LONGTEXT,解析靠应用层或 ETL
DROP TABLE IF EXISTS js_legacy_xml;
CREATE TABLE js_legacy_xml(
id BIGINT PRIMARY KEY,
domain CHAR(4),
xml_doc LONGTEXT NOT NULL,
created_ts DATETIME NOT NULL,
create_offset_min SMALLINT,
create_local_date DATE GENERATED ALWAYS AS (DATE(TIMESTAMPADD(MINUTE, create_offset_min, created_ts))) STORED,
KEY ix_local (create_local_date)
) ENGINE=InnoDB;
INSERT INTO js_legacy_xml(id,domain,xml_doc,created_ts,create_offset_min) VALUES
(1,'OA','<workflow><id>1</id><title>采购审批</title><amount>5000</amount><approver>张三</approver></workflow>','2024-03-01 09:00:00',480),
(2,'ERP','<invoice><no>INV-2024-001</no><item>钻石戒指</item><qty>12</qty><total>96000</total></invoice>','2024-03-02 10:30:00',-300),
(3,'WMS','<lot><sku>SKU00000001</sku><qty>200</qty><loc>A-01-03</loc><expiry>2026-12-31</expiry></lot>','2024-03-03 14:15:00',60);
SET @total_rows = 100000;
-- ============================================================================
-- §03 JSON 存储与类型校验
-- ============================================================================
SELECT '§03 JSON 存储与类型校验(JSON_TYPE / JSON_VALID)' AS `-- 第3章 --`;
SELECT JSON_TYPE(specs) AS root_type, JSON_TYPE(specs->'$.carat') AS carat_type,
JSON_TYPE(specs->'$.tags') AS tags_type, JSON_VALID(specs) AS is_valid
FROM js_product WHERE id=1;
-- ============================================================================
-- §04 JSON 查询:->> / JSON_EXTRACT / JSON_KEYS / JSON_CONTAINS / JSON_SEARCH
-- ============================================================================
SELECT '§04 JSON 查询(->> / JSON_EXTRACT / JSON_KEYS / JSON_CONTAINS / JSON_SEARCH)' AS `-- 第4章 --`;
-- 4.1 ->> 去引号提取标量
SELECT id, name, specs->>'$.color' AS color, specs->>'$.clarity' AS clarity, specs->>'$.cert' AS cert
FROM js_product WHERE id BETWEEN 1 AND 5;
-- 4.2 JSON_KEYS 看顶层键;JSON_LENGTH 看数组长度
SELECT specs->>'$.cert' AS cert, JSON_KEYS(specs) AS keys_arr, JSON_LENGTH(specs->'$.tags') AS tags_len
FROM js_product WHERE id=1;
-- 4.3 JSON_CONTAINS 判断对象包含;JSON_SEARCH 定位值路径
SELECT id, name FROM js_product
WHERE JSON_CONTAINS(specs, JSON_OBJECT('origin','南非')) LIMIT 5;
SELECT JSON_SEARCH(specs, 'one', 'GIA') AS path_to_gia FROM js_product WHERE id=1;
-- ============================================================================
-- §05 JSON 索引:生成列 + 二级索引(标量)vs 多值索引(数组)
-- ============================================================================
SELECT '§05 JSON 索引:生成列二级索引(标量)vs 多值索引(数组 tags)' AS `-- 第5章 --`;
-- 5.1 生成列索引:高净度大克拉热路径查询(走 ix_carat,只读命中行)
SELECT id, name, gen_carat, gen_cert FROM js_product WHERE gen_carat > 4.8 LIMIT 5;
-- 5.2 多值索引:数组包含(走 ix_tags,避免全 JSON 扫描)
SELECT id, name, specs->>'$.tags' AS tags FROM js_product
WHERE '婚戒' MEMBER OF (specs->'$.tags') LIMIT 5;
-- ============================================================================
-- §06 JSON Schema 校验(JSON_SCHEMA_VALID,8.0.17+):脏数据拦截
-- ============================================================================
SELECT '§06 JSON Schema 校验(JSON_SCHEMA_VALID):入库前拦截脏数据' AS `-- 第6章 --`;
SET @product_schema = '{"type":"object","properties":{"carat":{"type":"number"},"cert":{"type":"string"},"tags":{"type":"array"}},"required":["carat","cert"]}';
-- 合法文档(carat 是数字、cert 是字符串)→ 1
SELECT JSON_SCHEMA_VALID(@product_schema, JSON_OBJECT('carat',1.2,'cert','GIA','tags',JSON_ARRAY('x'))) AS valid_ok;
-- 非法文档(carat 是字符串)→ 0
SELECT JSON_SCHEMA_VALID(@product_schema, JSON_OBJECT('carat','bad','cert','GIA')) AS valid_bad;
-- ============================================================================
-- §07 JSON 拆解:JSON_TABLE 把 tags 数组炸成关系行(半结构化 → 关系)
-- ============================================================================
SELECT '§07 JSON 拆解:JSON_TABLE 把 tags 数组炸成行(半结构化→关系)' AS `-- 第7章 --`;
SELECT j.system_code, t.tag, COUNT(*) AS cnt
FROM js_product j,
JSON_TABLE(j.specs, '$.tags[*]' COLUMNS (tag VARCHAR(32) PATH '$')) AS t
GROUP BY j.system_code, t.tag
ORDER BY cnt DESC
LIMIT 12;
-- ============================================================================
-- §08 JSON 原地修改:JSON_SET / JSON_INSERT / JSON_REPLACE / JSON_REMOVE
-- ============================================================================
SELECT '§08 JSON 修改:JSON_SET / JSON_INSERT / JSON_REPLACE / JSON_REMOVE' AS `-- 第8章 --`;
DROP TABLE IF EXISTS js_tmp_edit;
CREATE TABLE js_tmp_edit(id INT PRIMARY KEY, specs JSON);
INSERT INTO js_tmp_edit VALUES (1, JSON_OBJECT('a',1,'b',2));
UPDATE js_tmp_edit SET specs = JSON_SET(specs,'$.c',3) WHERE id=1; -- 新增/覆盖 c=3
UPDATE js_tmp_edit SET specs = JSON_INSERT(specs,'$.d',4) WHERE id=1; -- 仅当不存在才插入
UPDATE js_tmp_edit SET specs = JSON_REMOVE(specs,'$.b') WHERE id=1; -- 删除 b
SELECT id, specs FROM js_tmp_edit;
-- ============================================================================
-- §09 XML 处理边界:MySQL 9.0 无 XML 类型 / 无 XMLTABLE,但 EXTRACTVALUE(XPath) 仍在
-- ============================================================================
SELECT '§09 XML 处理边界:MySQL 9.0 无 XML 类型/无 XMLTABLE,但 EXTRACTVALUE(XPath) 仍可用' AS `-- 第9章 --`;
-- 9.1 探针:EXTRACTVALUE 在 9.0.1 实测【仍在】(XPath 1.0 子集,仅抽单值);XMLTABLE【已不存在 → 1064】
CALL js_err_probe('EXTRACTVALUE仍在', 'SELECT EXTRACTVALUE(xml_doc,''/workflow/title'') FROM js_legacy_xml');
CALL js_err_probe('XMLTABLE不存在', 'SELECT * FROM XMLTABLE(''/x'' COLUMNS a INT PATH ''/x/a'') AS x');
-- 9.2 实测 EXTRACTVALUE 能抽 XML 单值(轻量级可用,但仅首个匹配、无集合语义、无 XML 索引)
SELECT id, domain,
EXTRACTVALUE(xml_doc,'/workflow/title') AS wf_title,
EXTRACTVALUE(xml_doc,'/invoice/item') AS inv_item
FROM js_legacy_xml;
-- 9.3 字符串函数也可抽(不依赖 XML 解析器,适合固定模板)
SELECT id,
SUBSTRING_INDEX(SUBSTRING_INDEX(xml_doc,'<approver>',-1),'</approver>',1) AS approver_when_has
FROM js_legacy_xml;
-- 9.4 正解:万亿级 XML 应 ETL 转 JSON(入库 JSON 列,享 JSON_TABLE/多值索引)或关系化到侧表;
-- EXTRACTVALUE 只适合偶发、单值抽取,别承载高频 XML 解析。
-- ============================================================================
-- §10 时区 + 多币种:本地营业日生成列过滤;只 SUM(price_usd)
-- ============================================================================
SELECT '§10 国际时区 + 多币种结算(生成列本地日;只 SUM(price_usd))' AS `-- 第10章 --`;
-- 10.1 按本地营业日过滤(带索引,时区无关 UTC 存储)
SELECT create_local_date, COUNT(*) AS cnt
FROM js_product
WHERE create_local_date BETWEEN '2024-01-01' AND '2024-01-31'
GROUP BY create_local_date ORDER BY create_local_date LIMIT 5;
-- 10.2 跨域、跨币种只汇总 USD 归一值(绝不可 SUM(price_orig))
SELECT system_code, COUNT(*) AS orders, SUM(price_usd) AS gmv_usd
FROM js_product GROUP BY system_code ORDER BY gmv_usd DESC;
-- ============================================================================
-- §11 万亿/高并发:索引取舍实测(生成列索引 vs 全 JSON 扫描;多值索引 vs 全扫)
-- ============================================================================
SELECT '§11 万亿/高并发性能感知:JSON 索引实测(INNODB_ROWS_READ 增量)' AS `-- 第11章 --`;
-- 11.1 生成列索引 vs 全 JSON 扫描(标量过滤)
-- 注意:MySQL 会对「与生成列定义逐字相同的表达式」做生成列替换而误用索引,
-- 故「全扫描」版本改用 JSON_EXTRACT(...),表达式文本不同,强制真全扫。
CALL js_snap();
SELECT COUNT(*) INTO @gc_idx FROM js_product WHERE gen_carat > 4.8;
CALL js_measure('GENCOL_index');
CALL js_snap();
SELECT COUNT(*) INTO @gc_full FROM js_product WHERE CAST(JSON_EXTRACT(specs,'$.carat') AS DECIMAL(6,3)) > 4.8;
CALL js_measure('GENCOL_fullscan');
-- 11.2 多值索引 vs JSON_SEARCH 全扫(数组包含)
CALL js_snap();
SELECT COUNT(*) INTO @mv_idx FROM js_product WHERE '婚戒' MEMBER OF (specs->'$.tags');
CALL js_measure('MV_index');
CALL js_snap();
SELECT COUNT(*) INTO @mv_full FROM js_product WHERE JSON_SEARCH(specs,'all','婚戒') IS NOT NULL;
CALL js_measure('MV_fullscan');
SELECT m.tag, m.innodb_rows,
CASE WHEN m.tag LIKE 'GENCOL%' THEN '生成列索引 vs 全JSON扫描'
WHEN m.tag LIKE 'MV%' THEN '多值索引 vs JSON_SEARCH全扫' END AS 对比
FROM js_demo_metric m WHERE m.tag IN ('GENCOL_index','GENCOL_fullscan','MV_index','MV_fullscan')
ORDER BY m.tag;
-- ============================================================================
-- §12 V1–V12 自检(每轮运行结果一致即为幂等)
-- ============================================================================
SELECT '§12 V1–V12 自检(连跑两次应完全一致)' AS `-- 第12章 --`;
-- V1 框架表存在
SELECT COUNT(*) INTO @v1 FROM information_schema.tables
WHERE table_schema='JewelryAnalytics' AND table_name IN ('js_demo_metric','js_demo_result','js_demo_probe');
CALL js_v('V1_框架表存在', IF(@v1=3,1,0), CONCAT('框架表数=',@v1));
-- V2 JSON 相关索引存在(ix_carat/ix_cert/ix_tags)
SELECT COUNT(*) INTO @v2 FROM information_schema.statistics
WHERE table_schema='JewelryAnalytics' AND table_name='js_product'
AND index_name IN ('ix_carat','ix_cert','ix_tags');
CALL js_v('V2_JSON索引存在', IF(@v2=3,1,0), CONCAT('JSON索引数=',@v2));
-- V3 ->> 查询返回 > 0
SELECT COUNT(*) INTO @v3 FROM js_product WHERE specs->>'$.color' = 'D';
CALL js_v('V3_箭头查询>0', IF(@v3>0,1,0), CONCAT('color=D命中=',@v3));
-- V4 生成列索引生效:命中行数相等且生成列读行 << 全 JSON 扫描读行
SELECT IFNULL((SELECT innodb_rows FROM js_demo_metric WHERE tag='GENCOL_index'),0) INTO @ir_gci;
SELECT IFNULL((SELECT innodb_rows FROM js_demo_metric WHERE tag='GENCOL_fullscan'),0) INTO @ir_gcf;
CALL js_v('V4_生成列索引生效', IF(@gc_idx=@gc_full AND @ir_gci>0 AND @ir_gci < @ir_gcf,1,0),
CONCAT('命中=',@gc_idx,' 索引读行=',@ir_gci,' 全扫读行=',@ir_gcf));
-- V5 多值索引生效:命中相等且多值索引读行 < 全扫读行
SELECT IFNULL((SELECT innodb_rows FROM js_demo_metric WHERE tag='MV_index'),0) INTO @ir_mvi;
SELECT IFNULL((SELECT innodb_rows FROM js_demo_metric WHERE tag='MV_fullscan'),0) INTO @ir_mvf;
CALL js_v('V5_多值索引生效', IF(@mv_idx=@mv_full AND @ir_mvi>0 AND @ir_mvi < @ir_mvf,1,0),
CONCAT('命中=',@mv_idx,' 索引读行=',@ir_mvi,' 全扫读行=',@ir_mvf));
-- V6 JSON Schema 校验正确区分合法/非法
SET @s6 = '{"type":"object","properties":{"carat":{"type":"number"},"cert":{"type":"string"}},"required":["carat","cert"]}';
SELECT JSON_SCHEMA_VALID(@s6, JSON_OBJECT('carat',1.2,'cert','GIA')) INTO @v6a;
SELECT JSON_SCHEMA_VALID(@s6, JSON_OBJECT('carat','bad','cert','GIA')) INTO @v6b;
CALL js_v('V6_Schema校验正确', IF(@v6a=1 AND @v6b=0,1,0), CONCAT('合法=',@v6a,' 非法=',@v6b));
-- V7 JSON_TABLE 炸行 > 0
SELECT COUNT(*) INTO @v7 FROM (
SELECT j.id, t.tag FROM js_product j,
JSON_TABLE(j.specs, '$.tags[*]' COLUMNS (tag VARCHAR(32) PATH '$')) AS t
) x;
CALL js_v('V7_JSON_TABLE炸行>0', IF(@v7>0,1,0), CONCAT('炸出行数=',@v7));
-- V8 JSON 修改生效(SET 新增、REMOVE 删除)
SELECT JSON_CONTAINS_PATH(specs,'one','$.c') INTO @v8a FROM js_tmp_edit WHERE id=1;
SELECT JSON_CONTAINS_PATH(specs,'one','$.b') INTO @v8b FROM js_tmp_edit WHERE id=1;
CALL js_v('V8_JSON修改生效', IF(@v8a=1 AND @v8b=0,1,0), CONCAT('含c=',@v8a,' 含b=',@v8b));
-- V9 XML 支持边界:EXTRACTVALUE 仍在(errno=0) 但 XMLTABLE 不存在(errno=1064)
SELECT errno INTO @v9a FROM js_demo_probe WHERE label='EXTRACTVALUE仍在' LIMIT 1;
SELECT errno INTO @v9b FROM js_demo_probe WHERE label='XMLTABLE不存在' LIMIT 1;
CALL js_v('V9_XML支持边界', IF(@v9a=0 AND @v9b=1064,1,0), CONCAT('EXTRACTVALUE errno=',@v9a,' XMLTABLE errno=',@v9b));
-- V10 时区生成列可用 + CONVERT_TZ 本机失效(执行成功 errno=0 但返回 NULL)
SELECT COUNT(*) INTO @v10 FROM js_product WHERE create_local_date >= '2024-01-01';
CALL js_err_probe('CONVERT_TZ本机失效', 'SELECT CONVERT_TZ(created_ts,''UTC'',''Asia/Shanghai'') FROM js_product LIMIT 1');
SELECT errno INTO @v10b FROM js_demo_probe WHERE label='CONVERT_TZ本机失效' LIMIT 1;
SELECT CONVERT_TZ(created_ts,'UTC','Asia/Shanghai') INTO @ctz_test FROM js_product LIMIT 1;
CALL js_v('V10_时区生成列可用', IF(@v10>0 AND @v10b=0 AND @ctz_test IS NULL,1,0),
CONCAT('本地日命中=',@v10,' CONVERT_TZ errno=',@v10b,' 结果=',IF(@ctz_test IS NULL,'NULL','非NULL')));
-- V11 多币种 USD 归一一致(price_usd == price_orig*rate 求和)
SELECT SUM(price_usd) INTO @v11a FROM js_product;
SELECT SUM(ROUND(price_orig * CASE currency_code
WHEN 'CNY' THEN 1/7.20 WHEN 'USD' THEN 1.0 WHEN 'EUR' THEN 1.08
WHEN 'JPY' THEN 0.0067 WHEN 'HKD' THEN 1/7.80 WHEN 'GBP' THEN 1.27 END, 2)) INTO @v11b FROM js_product;
CALL js_v('V11_多币种USD归一一致', IF(ABS(@v11a-@v11b) < 1,1,0), CONCAT('sum_usd=',@v11a,' sum_orig*rate=',@v11b));
-- V12 数据量稳定(幂等)
SELECT COUNT(*) INTO @v12 FROM js_product;
CALL js_v('V12_数据量稳定100000', IF(@v12=@total_rows,1,0), CONCAT('产品行数=',@v12));
-- 汇总
SELECT check_id, passed, detail FROM js_demo_result ORDER BY check_id;
-- ============================================================================
-- §13 结论:MySQL 9.0 JSON/XML 半结构化处理要点
-- ============================================================================
SELECT '§13 JSON/XML 半结构化处理结论' AS `-- 第13章 --`;
SELECT '1) JSON 是原生类型,支持 ->> / JSON_EXTRACT / JSON_TABLE / JSON_SET 等完整函数族。' AS `结论1`;
SELECT '2) JSON 列不能直接建二级索引;抽热点标量到「生成列」再建索引,热路径才不会全 JSON 扫描。' AS `结论2`;
SELECT '3) JSON 数组包含查询用「多值索引」+ MEMBER OF,可走索引;JSON_SEARCH 是全扫描,万亿级禁用。' AS `结论3`;
SELECT '4) JSON_SCHEMA_VALID 在入库前校验结构/类型,拦截脏数据(carat 数字/ cert 字符串)。' AS `结论4`;
SELECT '5) JSON_TABLE 把半结构化数组炸成关系行,是「JSON→关系」分析的正道(替代应用层循环)。' AS `结论5`;
SELECT '6) ⚠️ MySQL 9.0 无 XML 类型、无 XMLTABLE(1064);但 EXTRACTVALUE(XPath) 仍在,仅抽单值、无集合/无索引;XML 重负载仍应 ETL 转 JSON/关系化。' AS `结论6`;
SELECT '7) 万亿级:高频聚合字段不要塞 JSON;抽生成列或侧表,JSON 只存真正半结构化、低频访问的部分。' AS `结论7`;
SELECT '8) 时区用 TIMESTAMPADD(偏移) 生成列(CONVERT_TZ 本机失效);多币种只 SUM(price_usd) 归一结算。' AS `结论8`;
SELECT '① JSON 存半结构化;② 热点抽生成列+索引;③ 数组用多值索引;④ 入库前 JSON_SCHEMA_VALID;⑤ XML 存TEXT走ETL;' AS `清单`;
哲学管理(学)人生, 文学艺术生活, 自动(计算机学)物理(学)工作, 生物(学)化学逆境, 历史(学)测绘(学)时间, 经济(学)数学金钱(理财), 心理(学)医学情绪, 诗词美容情感, 美学建筑(学)家园, 解构建构(分析)整合学习, 智商情商(IQ、EQ)运筹(学)生存.---Geovin Du(涂聚文)
浙公网安备 33010602011771号