sql:SQL Views and Materialized Views using mysql 9.0

1. 引言:为什么需要视图与物化视图

在万亿数据、高并发的国际化业务场景下,SQL 查询的抽象能力与性能取舍直接决定系统架构的成败。本文以国际化珠宝集团 OA / ERP / CRM / HRM / MES / SCM / WMS / KMS 八域业务为背景,通过 MySQL 9.0 完整可运行的演示脚本,系统讲解视图(View)与物化视图(Materialized View)的原理、用法与性能差异。

2. 核心概念:视图与物化视图的本质区别

视图是存储在数据库中的虚拟表,本质是一条被命名的 SQL 查询,不占用物理存储;物化视图则是把查询结果实际落盘到物理表,以空间换时间。理解两者的差异,是做出正确技术选型的前提。

  • 视图(View):每次查询时实时执行底层 SQL,数据永远最新,但复杂查询可能带来性能开销。
  • 物化视图(Materialized View):预计算并存储结果,查询极快,但存在数据新鲜度问题,需要刷新机制。
  • MySQL 现状:MySQL 原生不支持物化视图,业界通常用「普通表 + 刷新过程」模拟实现。

3. 演示环境与数据准备

本文演示脚本完全幂等,可重复运行。环境要求 MySQL 9.0,数据库名 JewelryAnalytics。脚本包含度量框架、维度表与事实表造数,覆盖时区、多币种等国际化场景。

sql 复制代码
-- 度量框架:量化引擎真实读行,证明视图/物化视图的取舍
DROP TABLE IF EXISTS dyn_demo_metric;
CREATE TABLE dyn_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,
  tmp_tables BIGINT NOT NULL DEFAULT 0,
  ms INT NOT NULL DEFAULT 0,
  note VARCHAR(255)
);

4. 标准视图:抽象查询逻辑

视图最核心的价值在于封装复杂的 JOIN 与聚合逻辑,让上层应用只面对简洁的接口。本节演示国家级 GMV 聚合视图与三表 JOIN 明细视图。

sql 复制代码
-- 国家级 GMV 聚合视图(屏蔽折算与分组细节)
DROP VIEW IF EXISTS v_sales_by_country;
CREATE VIEW v_sales_by_country AS
  SELECT country_code,
         COUNT(*)                         AS txn_cnt,
         ROUND(SUM(amount_usd),2)         AS gmv_usd,
         ROUND(AVG(amount_usd),4)         AS avg_usd
  FROM vw_txn
  GROUP BY country_code;

5. 安全视图:列级与聚合级权限控制

视图是数据库安全体系的重要工具。通过只暴露必要列、屏蔽敏感字段,可以实现细粒度的访问控制,让不同角色看到不同粒度的数据。

sql 复制代码
-- 对外只读视图:仅暴露客户主键与姓名,屏蔽国家/币种/等级等敏感列
DROP VIEW IF EXISTS v_customer_public;
CREATE VIEW v_customer_public AS
  SELECT customer_id, cust_name FROM vw_customer;

6. 可更新视图与 WITH CHECK OPTION

MySQL 支持对满足条件的视图进行 INSERT / UPDATE / DELETE。WITH CHECK OPTION 可以阻止越权写入,是行级写权限边界的重要保障。同时需要理解多表 JOIN 视图与聚合视图为何不可更新。

sql 复制代码
-- 单表可更新视图:仅暴露中国(CN)交易,WITH CHECK OPTION 阻止越权写入
DROP VIEW IF EXISTS v_apac_txn;
CREATE VIEW v_apac_txn AS
  SELECT txn_id, customer_id, product_id, system_code, country_code,
         currency_code, amount_orig, amount_usd, created_ts, create_local_date
  FROM vw_txn
  WHERE country_code = 'CN'
  WITH CHECK OPTION;

7. 视图算法:MERGE vs TEMPTABLE

MySQL 视图有两种执行算法:MERGE 算法把视图定义合并进外层查询,可下推谓词、走索引;TEMPTABLE 算法先把整个视图结果物化到临时表,再对临时表应用外层谓词,开销更大。通过度量框架可以量化两者的真实差异。

sql 复制代码
-- MERGE:优化器把视图定义合并进外层查询,可下推谓词、走索引
DROP VIEW IF EXISTS v_merge_txn;
CREATE ALGORITHM=MERGE VIEW v_merge_txn AS
  SELECT txn_id, system_code, amount_usd, create_local_date FROM vw_txn;
-- TEMPTABLE:先把整个视图结果物化到临时表,再对临时表应用外层谓词(开销更大)
DROP VIEW IF EXISTS v_tt_txn;
CREATE ALGORITHM=TEMPTABLE VIEW v_tt_txn AS
  SELECT txn_id, system_code, amount_usd, create_local_date FROM vw_txn;

8. 物化视图:用普通表 + 刷新过程模拟

MySQL 没有原生物化视图,业界标准做法是用普通表 + 存储过程模拟。本节演示全量刷新、触发器增量刷新与事件调度刷新三种维护策略,并量化物化视图在查询性能上的巨大优势。

sql 复制代码
-- 物化表:按「本地营业日 × 国家」预聚合 GMV(已折算 USD,跨币种可直接相加)
DROP TABLE IF EXISTS mv_daily_gmv;
CREATE TABLE mv_daily_gmv(
  biz_date     DATE NOT NULL,
  country_code CHAR(2) NOT NULL,
  gmv_usd      DECIMAL(16,4) NOT NULL,
  txn_cnt      INT NOT NULL,
  refreshed_at DATETIME NOT NULL,
  PRIMARY KEY (biz_date, country_code),
  INDEX ix_mv_country (country_code)
);

9. 嵌套视图与组合复用

视图可以建立在视图之上,形成多层抽象。这种组合复用能力让复杂的数据逻辑可以被逐层拆解、逐层封装,极大提升 SQL 的可维护性。

sql 复制代码
-- 在聚合视图之上再建「Top 国家」视图,证明视图可组合复用
DROP VIEW IF EXISTS v_top_countries;
CREATE VIEW v_top_countries AS
  SELECT country_code, gmv_usd
  FROM v_sales_by_country
  ORDER BY gmv_usd DESC
  LIMIT 3;

10. 边界探针:视图的固有限制

视图并非万能。视图不能带参数、不能自引用(递归视图不被允许)。理解这些边界,才能在设计阶段做出正确的技术决策,避免把视图用在错误的场景。

sql 复制代码
-- 视图是固定查询,无法像过程那样传参;参数化查询请见动态SQL专题
SELECT '视图是固定查询,无法像过程那样传参;参数化查询请见动态SQL专题' AS `边界说明`;
-- 递归层级请用 CTE(WITH RECURSIVE),视图不可自引用
SELECT '递归层级请用 CTE(WITH RECURSIVE),视图不可自引用' AS `边界说明`;

11. 性能对比与自检汇总

脚本内置 V1-V12 共 12 项自动化自检,覆盖视图分组、JOIN 行数、安全视图列数、CHECK OPTION 拦截、多表/聚合视图不可写、TEMPTABLE 建临时表、物化视图填充、物化查询读行更少、币种一致汇总、物化视图快照性、嵌套视图复用等关键断言。全部通过即证明演示结论成立。

sql 复制代码
SELECT check_id, passed, detail FROM dyn_demo_result ORDER BY check_id;
SELECT IF(COUNT(*)=SUM(passed),'全部通过 ✅','存在失败 ❌') AS `V1-V12 总判定`,
       COUNT(*) AS 检查数, SUM(passed) AS 通过数
FROM dyn_demo_result;

12. 总结与最佳实践

视图与物化视图是 SQL 数据抽象与性能优化的两大利器。视图适合逻辑复用与安全控制,物化视图适合高频聚合查询的性能加速。在实际架构中,应根据数据新鲜度要求、查询频率与写放大成本,权衡选择合适方案。

  • 逻辑复用优先:复杂 JOIN / 聚合逻辑用视图封装,提升可维护性。
  • 安全控制用视图:列级、行级权限通过视图 + WITH CHECK OPTION 实现。
  • 性能敏感用物化:高频聚合查询用物化表 + 定时刷新,读行数可降低几个数量级。
  • 注意新鲜度:物化视图是快照,需根据业务容忍度设计刷新策略。
  • 避免写放大:万亿高并发场景慎用触发器增量刷新,优先分区增量刷新。
sql 复制代码
-- =====================================================================
-- MySQL 9.0 演示:SQL Views and Materialized Views(视图与物化视图)
-- 场景:国际化珠宝集团 ------ OA / ERP / CRM / HRM / MES / SCM / WMS / KMS 八域
-- 约束:万亿数据高并发 · 国际时区时间差 · 国际多币种结算
-- 命名前缀:vw_* 基表 / v_* 视图 / mv_* 物化表(避免与同库其它演示表冲突)
-- 本脚本完全幂等:DROP IF EXISTS + 重建,连跑两次 EXIT=0 且结果逐行一致。
-- =====================================================================
SET NAMES utf8mb4;
SET SESSION cte_max_recursion_depth = 1000000;
SET SESSION sql_mode = 'STRICT_ALL_TABLES,NO_ENGINE_SUBSTITUTION';
 
-- §01  度量框架(dyn_demo_*):量化引擎真实读行,证明视图/物化视图的取舍
DROP TABLE IF EXISTS dyn_demo_metric;
CREATE TABLE dyn_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,
  tmp_tables BIGINT NOT NULL DEFAULT 0,
  ms INT NOT NULL DEFAULT 0,
  note VARCHAR(255)
);
DROP TABLE IF EXISTS dyn_demo_result;
CREATE TABLE dyn_demo_result(
  check_id VARCHAR(30) PRIMARY KEY,
  passed TINYINT NOT NULL,
  detail VARCHAR(400)
);
DROP TABLE IF EXISTS dyn_demo_probe;
CREATE TABLE dyn_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 dyn_snap;
DELIMITER //
CREATE PROCEDURE dyn_snap()
BEGIN
  SELECT VARIABLE_VALUE INTO @dyn_snap_ir FROM performance_schema.session_status WHERE VARIABLE_NAME='INNODB_ROWS_READ';
  SELECT VARIABLE_VALUE INTO @dyn_snap_rn FROM performance_schema.session_status WHERE VARIABLE_NAME='HANDLER_READ_RND_NEXT';
  SELECT VARIABLE_VALUE INTO @dyn_snap_rd FROM performance_schema.session_status WHERE VARIABLE_NAME='HANDLER_READ_RND';
  SELECT VARIABLE_VALUE INTO @dyn_snap_tmp FROM performance_schema.session_status WHERE VARIABLE_NAME='CREATED_TMP_TABLES';
  SET @dyn_snap_ts = UNIX_TIMESTAMP(NOW(6));
END//
DELIMITER ;
DROP PROCEDURE IF EXISTS dyn_measure;
DELIMITER //
CREATE PROCEDURE dyn_measure(IN p_tag VARCHAR(40))
BEGIN
  DECLARE v_ir BIGINT; DECLARE v_rn BIGINT; DECLARE v_rd BIGINT; DECLARE v_tmp 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';
  SELECT VARIABLE_VALUE INTO v_tmp FROM performance_schema.session_status WHERE VARIABLE_NAME='CREATED_TMP_TABLES';
  SET v_ms = ROUND((UNIX_TIMESTAMP(NOW(6)) - @dyn_snap_ts)*1000);
  INSERT INTO dyn_demo_metric(tag,innodb_rows,rnd_next,rnd,tmp_tables,ms) VALUES(p_tag, v_ir-@dyn_snap_ir, v_rn-@dyn_snap_rn, v_rd-@dyn_snap_rd, v_tmp-@dyn_snap_tmp, v_ms)
    ON DUPLICATE KEY UPDATE innodb_rows=v_ir-@dyn_snap_ir, rnd_next=v_rn-@dyn_snap_rn, rnd=v_rd-@dyn_snap_rd, tmp_tables=v_tmp-@dyn_snap_tmp, ms=v_ms;
END//
DELIMITER ;
DROP PROCEDURE IF EXISTS dyn_v;
DELIMITER //
CREATE PROCEDURE dyn_v(IN p_check VARCHAR(30), IN p_pass TINYINT, IN p_detail VARCHAR(400))
BEGIN
  INSERT INTO dyn_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 dyn_err_probe;
DELIMITER //
CREATE PROCEDURE dyn_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 @dyn_probe_sql = p_sql;
  PREPARE dyn_pstmt FROM @dyn_probe_sql;
  IF v_no = 0 THEN
    EXECUTE dyn_pstmt;
    DEALLOCATE PREPARE dyn_pstmt;
  END IF;
  INSERT INTO dyn_demo_probe(label,errno,sql_state,msg) VALUES(p_label, v_no, v_st, v_msg);
END//
DELIMITER ;
 
SELECT '§01 框架就绪' AS `-- 第1章 --`;
 
-- §02  维度表(时区 / 多币种)+ 八域基础表造数
-- 时区偏移(分钟):CONVERT_TZ 本机恒 NULL,统一用 TIMESTAMPADD 计算本地营业日
DROP TABLE IF EXISTS vw_dim_country;
CREATE TABLE vw_dim_country(code CHAR(2) PRIMARY KEY, name VARCHAR(40), tz_offset_min INT, currency CHAR(3));
INSERT INTO vw_dim_country VALUES
 ('CN','中国',480,'CNY'),('HK','中国香港',480,'HKD'),('US','美国',-300,'USD'),
 ('GB','英国',0,'GBP'),('JP','日本',540,'JPY'),('FR','法国',60,'EUR'),
 ('DE','德国',60,'EUR'),('SG','新加坡',480,'SGD');
 
DROP TABLE IF EXISTS vw_dim_currency;
CREATE TABLE vw_dim_currency(code CHAR(3) PRIMARY KEY, name VARCHAR(40), usd_rate DECIMAL(12,6));
INSERT INTO vw_dim_currency VALUES
 ('CNY','人民币',1/7.20),('HKD','港元',1/7.80),('USD','美元',1.0),
 ('GBP','英镑',1.27),('JPY','日元',0.0067),('EUR','欧元',1.08),('SGD','新加坡元',1/1.35);
 
-- CRM 客户维度(10,000):国家公式 ((id*3)%8)+1 与交易表保持一致,便于 JOIN
DROP TABLE IF EXISTS vw_customer;
CREATE TABLE vw_customer(
  customer_id INT PRIMARY KEY,
  cust_name VARCHAR(60),
  country_code CHAR(2),
  currency_code CHAR(3),
  tier VARCHAR(6)
);
INSERT INTO vw_customer
WITH RECURSIVE seq(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n<10000)
SELECT
  n,
  CONCAT('客户#', n),
  ELT(((n*3)%8)+1,'CN','HK','US','GB','JP','FR','DE','SG'),
  ELT(((n*3)%8)+1,'CNY','HKD','USD','GBP','JPY','EUR','EUR','SGD'),
  ELT(1+(n%3),'金','银','钻')
FROM seq;
 
-- HRM 员工维度(500)
DROP TABLE IF EXISTS vw_employee;
CREATE TABLE vw_employee(emp_id INT PRIMARY KEY, emp_name VARCHAR(60), dept VARCHAR(20), country_code CHAR(2));
INSERT INTO vw_employee
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%8),'CN','HK','US','GB','JP','FR','DE','SG')
FROM seq;
 
-- MES/WMS 商品维度(2,000)
DROP TABLE IF EXISTS vw_product;
CREATE TABLE vw_product(product_id INT PRIMARY KEY, prod_name VARCHAR(60), category VARCHAR(20), cost_usd DECIMAL(10,2));
INSERT INTO vw_product
WITH RECURSIVE seq(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n<2000)
SELECT n, CONCAT(CASE WHEN n%5=0 THEN '钻石' ELSE '黄金' END,'饰品#',n),
       ELT(1+(n%4),'戒指','项链','手镯','耳环'), ROUND(50+((n*11)%5000)/10.0,2)
FROM seq;
 
-- ERP/CRM 交易事实(100,000 行):国家跟随客户,币种跟随国家,金额折算 USD
DROP TABLE IF EXISTS vw_txn;
CREATE TABLE vw_txn(
  txn_id INT PRIMARY KEY,
  customer_id INT NOT NULL,
  product_id INT NOT NULL,
  system_code CHAR(4) 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,
  INDEX ix_vw_txn_sys (system_code)
);
INSERT INTO vw_txn (txn_id, customer_id, product_id, system_code, country_code, currency_code, amount_orig, amount_usd, created_ts, create_local_date)
WITH RECURSIVE seq(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM seq WHERE n<100000)
SELECT n, cid, pid, sys, cty, cur, aorig, ausd, cts, cld
FROM (
  SELECT
    n, cid, pid, sys, cty, cur, aorig, cts,
    ROUND(aorig * CASE cur WHEN 'CNY' THEN 1/7.20 WHEN 'HKD' THEN 1/7.80 WHEN 'USD' THEN 1.0
                           WHEN 'GBP' THEN 1.27  WHEN 'JPY' THEN 0.0067 WHEN 'EUR' THEN 1.08
                           WHEN 'SGD' THEN 1/1.35 END, 4) AS ausd,
    DATE(TIMESTAMPADD(MINUTE,
          CASE cty WHEN 'CN' THEN 480 WHEN 'HK' THEN 480 WHEN 'US' THEN -300 WHEN 'GB' THEN 0
                    WHEN 'JP' THEN 540 WHEN 'FR' THEN 60  WHEN 'DE' THEN 60  WHEN 'SG' THEN 480 END,
          cts)) AS cld
  FROM (
    SELECT
      n,
      1 + (n % 10000) AS cid,
      1 + (n % 2000) AS pid,
      ELT(1 + (n % 8), 'OA','ERP','CRM','HRM','MES','SCM','WMS','KMS') AS sys,
      ELT(((1 + (n % 10000)) * 3) % 8 + 1, 'CN','HK','US','GB','JP','FR','DE','SG') AS cty,
      ELT(((1 + (n % 10000)) * 3) % 8 + 1, 'CNY','HKD','USD','GBP','JPY','EUR','EUR','SGD') AS cur,
      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;
 
SELECT COUNT(*) AS `交易总行数(应=100000)`,
       (SELECT COUNT(*) FROM vw_customer) AS 客户数,
       (SELECT COUNT(*) FROM vw_product) AS 商品数
FROM vw_txn;
 
-- §03  标准视图:抽象查询逻辑(隐藏 JOIN / 聚合复杂度)
-- 3.1 国家级 GMV 聚合视图(屏蔽折算与分组细节)
DROP VIEW IF EXISTS v_sales_by_country;
CREATE VIEW v_sales_by_country AS
  SELECT country_code,
         COUNT(*)                         AS txn_cnt,
         ROUND(SUM(amount_usd),2)         AS gmv_usd,
         ROUND(AVG(amount_usd),4)         AS avg_usd
  FROM vw_txn
  GROUP BY country_code;
 
-- 3.2 订单明细视图(三表 JOIN 封装)
DROP VIEW IF EXISTS v_order_detail;
CREATE VIEW v_order_detail AS
  SELECT t.txn_id, c.cust_name, p.prod_name, p.category,
         t.system_code, t.country_code, t.currency_code,
         t.amount_orig, t.amount_usd, t.create_local_date
  FROM vw_txn t
  JOIN vw_customer c ON c.customer_id = t.customer_id
  JOIN vw_product  p ON p.product_id  = t.product_id;
 
SELECT * FROM v_sales_by_country ORDER BY gmv_usd DESC;
SELECT * FROM v_order_detail LIMIT 3;
 
-- V1 标准聚合视图按 8 个国家分组
SELECT COUNT(*) INTO @v1 FROM v_sales_by_country;
CALL dyn_v('V1_聚合视图分组', IF(@v1=8,1,0), CONCAT('国家数=',@v1,'(应=8)'));
-- V2 三表 JOIN 视图行数=事实表(100000)
SELECT COUNT(*) INTO @v2 FROM v_order_detail;
CALL dyn_v('V2_JOIN视图行数', IF(@v2=100000,1,0), CONCAT('行数=',@v2,'(应=100000)'));
 
-- §04  视图与访问控制:列级/聚合级权限抽象(安全视图)
-- 4.1 对外只读视图:仅暴露客户主键与姓名,屏蔽国家/币种/等级等敏感列
DROP VIEW IF EXISTS v_customer_public;
CREATE VIEW v_customer_public AS
  SELECT customer_id, cust_name FROM vw_customer;
 
-- 4.2 财务驾驶舱视图:只给汇总(无 PII、无逐笔金额),作为财务角色授权对象
DROP VIEW IF EXISTS v_finance_dashboard;
CREATE VIEW v_finance_dashboard AS
  SELECT country_code,
         COUNT(*)                    AS txn_cnt,
         ROUND(SUM(amount_usd),2)    AS gmv_usd
  FROM vw_txn
  GROUP BY country_code;
 
SELECT COUNT(*) INTO @v3 FROM information_schema.columns
  WHERE table_schema='JewelryAnalytics' AND table_name='v_customer_public';
CALL dyn_v('V3_列级安全视图', IF(@v3=2,1,0), CONCAT('暴露列数=',@v3,'(应=2:仅id+姓名)'));
 
-- §05  可更新视图 + WITH CHECK OPTION(行级写权限边界)
-- 5.1 单表可更新视图:仅暴露中国(CN)交易,WITH CHECK OPTION 阻止越权写入
--     注意:可更新视图须覆盖基表所有 NOT NULL(无默认)列,否则 INSERT 报 1423。
DROP VIEW IF EXISTS v_apac_txn;
CREATE VIEW v_apac_txn AS
  SELECT txn_id, customer_id, product_id, system_code, country_code,
         currency_code, amount_orig, amount_usd, created_ts, create_local_date
  FROM vw_txn
  WHERE country_code = 'CN'
  WITH CHECK OPTION;
 
-- 合法写入(CN):应成功
SET @v4_before = (SELECT COUNT(*) FROM vw_txn WHERE country_code='CN');
INSERT INTO v_apac_txn (txn_id, customer_id, product_id, system_code, country_code, currency_code, amount_orig, amount_usd, created_ts, create_local_date)
  VALUES (999999001, 1, 1, 'CRM', 'CN', 'CNY', 999.99, ROUND(999.99/7.20,4), '2023-06-01 00:00:00', '2023-06-01');
SET @v4_after = (SELECT COUNT(*) FROM vw_txn WHERE country_code='CN');
CALL dyn_v('V4_可更新视图合法写', IF(@v4_after=@v4_before+1,1,0), CONCAT('CN行 ',@v4_before,'->',@v4_after));
 
-- 5.2 非法写入(越权写到 EMEA/US):CHECK OPTION 拦截 → errno 1369
CALL dyn_err_probe('check_option_violation',
  'INSERT INTO v_apac_txn (txn_id, customer_id, product_id, system_code, country_code, currency_code, amount_orig, amount_usd, created_ts, create_local_date) VALUES (999999002, 1, 1, \'CRM\', \'US\', \'USD\', 1.00, 1.00, \'2023-06-01 00:00:00\', \'2023-06-01\')');
 
-- 5.3 多表 JOIN 视图无法 INSERT(MySQL 无法决定插入哪张基表)→ errno 1394
--     注:MySQL 允许通过连接视图更新「键保留列」(key-preserved),故 INSERT 才是可靠失败点。
DROP VIEW IF EXISTS v_join_cust_txn;
CREATE VIEW v_join_cust_txn AS
  SELECT t.txn_id, c.cust_name, t.amount_usd
  FROM vw_txn t JOIN vw_customer c ON c.customer_id=t.customer_id;
CALL dyn_err_probe('multitable_view_insert', 'INSERT INTO v_join_cust_txn VALUES (1,\'x\',0)');
 
-- 5.4 含 GROUP BY 的聚合视图永远不可更新(无键保留)→ errno 1288
DROP VIEW IF EXISTS v_agg_gmv;
CREATE VIEW v_agg_gmv AS
  SELECT country_code, SUM(amount_usd) AS gmv FROM vw_txn GROUP BY country_code;
CALL dyn_err_probe('agg_view_update', 'UPDATE v_agg_gmv SET gmv=0 WHERE country_code=\'CN\'');
 
SELECT errno INTO @v5 FROM dyn_demo_probe WHERE label='check_option_violation' LIMIT 1;
CALL dyn_v('V5_CHECK_OPTION拦截', IF(@v5=1369,1,0), CONCAT('errno=',@v5,'(应=1369)'));
SELECT errno INTO @v6a FROM dyn_demo_probe WHERE label='multitable_view_insert' LIMIT 1;
SELECT errno INTO @v6b FROM dyn_demo_probe WHERE label='agg_view_update' LIMIT 1;
CALL dyn_v('V6_多表_聚合视图不可写', IF(@v6a=1394 AND @v6b=1288,1,0),
           CONCAT('join视图INSERT=',@v6a,'(应=1394), 聚合视图UPDATE=',@v6b,'(应=1288)'));
 
-- §06  视图算法 MERGE vs TEMPTABLE + 执行计划评审
-- MERGE:优化器把视图定义合并进外层查询,可下推谓词、走索引
DROP VIEW IF EXISTS v_merge_txn;
CREATE ALGORITHM=MERGE VIEW v_merge_txn AS
  SELECT txn_id, system_code, amount_usd, create_local_date FROM vw_txn;
-- TEMPTABLE:先把整个视图结果物化到临时表,再对临时表应用外层谓词(开销更大)
DROP VIEW IF EXISTS v_tt_txn;
CREATE ALGORITHM=TEMPTABLE VIEW v_tt_txn AS
  SELECT txn_id, system_code, amount_usd, create_local_date FROM vw_txn;
 
CALL dyn_snap();
SELECT COUNT(*) INTO @x1 FROM v_merge_txn WHERE system_code='SCM';
CALL dyn_measure('VW_MERGE');
SELECT innodb_rows, tmp_tables INTO @m_merge, @t_merge FROM dyn_demo_metric WHERE tag='VW_MERGE';
 
CALL dyn_snap();
SELECT COUNT(*) INTO @x2 FROM v_tt_txn WHERE system_code='SCM';
CALL dyn_measure('VW_TT');
SELECT innodb_rows, tmp_tables INTO @m_tt, @t_tt FROM dyn_demo_metric WHERE tag='VW_TT';
 
SELECT @x1 AS merge_cnt, @x2 AS tt_cnt, @m_merge AS merge_innodb_rows, @m_tt AS tt_innodb_rows, @t_merge AS merge_tmp, @t_tt AS tt_tmp;
-- 关键区别:TEMPTABLE 必须把整个视图结果物化到临时表(CREATED_TMP_TABLES +1),MERGE 不建临时表
CALL dyn_v('V7_TEMPTABLE建临时表', IF(@t_tt>@t_merge,1,0),
           CONCAT('TEMPTABLE临时表=',@t_tt,' > MERGE临时表=',@t_merge,'(TEMPTABLE需物化,高并发下内存/IO开销更大)'));
 
-- §07  物化视图(MySQL 无原生 MV → 用普通表 + 刷新过程模拟)
-- 7.1 物化表:按「本地营业日 × 国家」预聚合 GMV(已折算 USD,跨币种可直接相加)
DROP TABLE IF EXISTS mv_daily_gmv;
CREATE TABLE mv_daily_gmv(
  biz_date     DATE NOT NULL,
  country_code CHAR(2) NOT NULL,
  gmv_usd      DECIMAL(16,4) NOT NULL,
  txn_cnt      INT NOT NULL,
  refreshed_at DATETIME NOT NULL,
  PRIMARY KEY (biz_date, country_code),
  INDEX ix_mv_country (country_code)
);
 
-- 7.2 全量刷新过程(万亿场景更常用「分区增量刷新」,这里演示全量范式)
DROP PROCEDURE IF EXISTS refresh_mv_daily_gmv;
DELIMITER //
CREATE PROCEDURE refresh_mv_daily_gmv()
BEGIN
  TRUNCATE TABLE mv_daily_gmv;
  INSERT INTO mv_daily_gmv (biz_date, country_code, gmv_usd, txn_cnt, refreshed_at)
  SELECT create_local_date, country_code,
         ROUND(SUM(amount_usd),4), COUNT(*), NOW()
  FROM vw_txn
  GROUP BY create_local_date, country_code;
END//
DELIMITER ;
 
-- 7.3 增量刷新(触发器)演示:写时即时维护,但带来写放大,万亿高并发慎用
DROP TABLE IF EXISTS vw_small;
DROP TABLE IF EXISTS mv_small;
DROP TRIGGER IF EXISTS trg_small_ai;
CREATE TABLE vw_small(id INT PRIMARY KEY, amt DECIMAL(10,2));
CREATE TABLE mv_small(total DECIMAL(14,2) NOT NULL, cnt INT NOT NULL);
INSERT INTO mv_small VALUES (0,0);
DELIMITER //
CREATE TRIGGER trg_small_ai AFTER INSERT ON vw_small FOR EACH ROW
BEGIN
  UPDATE mv_small SET total = total + NEW.amt, cnt = cnt + 1;
END//
DELIMITER ;
INSERT INTO vw_small VALUES (1,10.00),(2,20.00),(3,7.50);
SELECT * FROM mv_small;  -- 触发器即时维护:total=37.50, cnt=3
 
-- 7.4 事件调度刷新(scheduler 已 ON):每日自动全量刷新,管理新鲜度
DROP EVENT IF EXISTS ev_refresh_mv_daily;
CREATE EVENT ev_refresh_mv_daily
  ON SCHEDULE EVERY 1 DAY STARTS (CURRENT_TIMESTAMP + INTERVAL 1 MINUTE)
  DO CALL refresh_mv_daily_gmv();
SELECT '事件 ev_refresh_mv_daily 已创建(每日刷新物化视图)' AS `新鲜度调度`;
 
-- 7.5 执行全量刷新(此刻 mv 反映当前 vw_txn)
CALL refresh_mv_daily_gmv();
 
-- V8 物化视图行数 = 事实表 (本地营业日,国家) 去重组合数
SELECT COUNT(*) INTO @v8 FROM mv_daily_gmv;
SELECT COUNT(*) INTO @v8exp FROM (SELECT DISTINCT create_local_date, country_code FROM vw_txn) x;
CALL dyn_v('V8_物化视图已填充', IF(@v8>0 AND @v8=@v8exp,1,0), CONCAT('mv行=',@v8,' 期望=',@v8exp));
 
-- V9 物化视图查询读行 << 原表聚合读行(预聚合威力)
CALL dyn_snap();
SELECT ROUND(SUM(gmv_usd),2) INTO @mv_cn FROM mv_daily_gmv WHERE country_code='CN';
CALL dyn_measure('MV_QUERY');
SELECT innodb_rows INTO @mv_reads FROM dyn_demo_metric WHERE tag='MV_QUERY';
 
CALL dyn_snap();
SELECT ROUND(SUM(amount_usd),2) INTO @base_cn FROM vw_txn WHERE country_code='CN';
CALL dyn_measure('BASE_QUERY');
SELECT innodb_rows INTO @base_reads FROM dyn_demo_metric WHERE tag='BASE_QUERY';
 
SELECT @mv_cn AS mv_cn, @base_cn AS base_cn, @mv_reads AS mv_rows, @base_reads AS base_rows;
CALL dyn_v('V9_物化查询读行更少', IF(@mv_reads<@base_reads,1,0), CONCAT('mv读行=',@mv_reads,' < 原表读行=',@base_reads));
 
-- V10 一致性:物化 USD 汇总 = 原表 USD 汇总(币种已统一,无混合相加错误)
CALL dyn_v('V10_币种一致汇总', IF(ABS(@mv_cn-@base_cn)<0.05,1,0), CONCAT('mv=',@mv_cn,' base=',@base_cn,'(应相等)'));
 
-- V11 新鲜度陷阱:向原表插入新交易,未刷新前 mv 不含它(快照,非实时)
SET @v11_base_before = (SELECT COUNT(*) FROM vw_txn);
SET @v11_mv_before    = (SELECT COUNT(*) FROM mv_daily_gmv);
INSERT INTO vw_txn (txn_id, customer_id, product_id, system_code, country_code, currency_code, amount_orig, amount_usd, created_ts, create_local_date)
  VALUES (999999003, 1, 1, 'ERP', 'CN', 'CNY', 123.00, ROUND(123.00/7.20,4), '2023-06-02 10:00:00', '2023-06-02');
SET @v11_base_after = (SELECT COUNT(*) FROM vw_txn);
SET @v11_mv_after   = (SELECT COUNT(*) FROM mv_daily_gmv);
CALL dyn_v('V11_物化视图快照性', IF(@v11_base_after=@v11_base_before+1 AND @v11_mv_after=@v11_mv_before,1,0),
           CONCAT('原表 ',@v11_base_before,'->',@v11_base_after,';mv 保持 ',@v11_mv_after,'(未刷新即陈旧)'));
 
-- §08  嵌套视图(视图复用视图)+ 时区/币种收口
-- 8.1 在聚合视图之上再建「Top 国家」视图,证明视图可组合复用
DROP VIEW IF EXISTS v_top_countries;
CREATE VIEW v_top_countries AS
  SELECT country_code, gmv_usd
  FROM v_sales_by_country
  ORDER BY gmv_usd DESC
  LIMIT 3;
SELECT * FROM v_top_countries;
 
-- 8.2 时区收口示例:按本地营业日取某国当日 GMV(mv 直接按本地日键控,免时区重算)
SELECT biz_date, country_code, gmv_usd
FROM mv_daily_gmv
WHERE country_code='JP' AND biz_date='2023-03-15';
 
-- V12 嵌套视图返回 Top3(复用 v_sales_by_country)
SELECT COUNT(*) INTO @v12 FROM v_top_countries;
CALL dyn_v('V12_嵌套视图复用', IF(@v12=3,1,0), CONCAT('Top国家数=',@v12,'(应=3)'));
 
-- §09  边界探针:视图的固有限制
-- 9.1 视图不能带参数(与存储过程不同)------ 若要「参数化结果」须用过程/函数,而非视图。
--     这里用 err_probe 验证「试图把视图当参数化对象」的误用不被支持(演示对比,无报错即说明视图是固定查询)。
SELECT '视图是固定查询,无法像过程那样传参;参数化查询请见动态SQL专题' AS `边界说明`;
-- 9.2 递归视图不被允许(视图不能自引用);层级/树形数据须用 WITH RECURSIVE(CTE)而非视图。
SELECT '递归层级请用 CTE(WITH RECURSIVE),视图不可自引用' AS `边界说明`;
 
-- §10  自检汇总
SELECT check_id, passed, detail FROM dyn_demo_result ORDER BY check_id;
SELECT IF(COUNT(*)=SUM(passed),'全部通过 ✅','存在失败 ❌') AS `V1-V12 总判定`,
       COUNT(*) AS 检查数, SUM(passed) AS 通过数
FROM dyn_demo_result;
SELECT '本脚本幂等:连跑两次 EXIT=0 且 V1-V12 逐行一致' AS `幂等说明`;
相关推荐
饼饼学习空间智能35 分钟前
机器人规模化部署平台怎么选?Fleet、VDA 5050、多机器人调度与运营闭环解析
大数据·深度学习·机器学习
浔溺39 分钟前
Spark核心基石:RDD全方位深度解析(原理、算子、实战、优化)
大数据·分布式·spark
梦影_1 小时前
MySQL进阶教程(二)—— 储存过程与触发器(下)
开发语言·数据库·后端·sql·mysql·oracle·数据库开发
工业涂料百问1 小时前
陶瓷涂料的光泽度怎么检测?20°、60°、85° 三个角度怎么选
数据库
仙逆GPT1 小时前
ChatGPT、Codex工程实战:Agent把OFFSET分页改成游标分页,查询更快了,为什么数据反而会重复和丢失?
mysql·chatgpt·codex·chatgptplus·chatgptpro
Sayai1 小时前
Elasticsearch Rollup 汇算索引实战:监控指标多粒度预聚合查询,附 painless 时间窗取每日最新值
大数据·运维·elasticsearch·搜索引擎·性能优化·监控
ha_lydms1 小时前
Xml Functions(XML 函数)
xml·大数据·hadoop·spark·数据处理·dataworks·maxcompute
科创致远1 小时前
ESOP 电子作业指导书车间无纸化实战评测
大数据·数据库·人工智能·制造
silvamux_1 小时前
品牌事实资产建模教程:把口述材料变成可校验的故事输入
大数据·人工智能