MySQL 9.7 JSON Duality View:一次往返插入两张表

2026-06-04 23 预计阅读时间:1 分钟
来源:jfg-mysql.blogspot.com AI 摘要 原文链接

免责声明:本文为 AI 摘要整理,建议结合原文阅读。摘要可能省略上下文、版本差异或边界条件,不作为官方说明。

预计阅读时间:8 分钟

拆表是常见的优化手段——把一张宽表拆成两张窄表,减少冗余、提升查询效率。但拆表之后,原本一条 INSERT 就能搞定的事,变成了需要事务包裹的两条 INSERT。从自动提交的单条写入,变成一个事务块,工作负载的形态变了,锁的持有时间也变了。

MySQL 9.7 的 JSON Duality View 给了另一种解法:对着视图做一条 INSERT,数据库在内部把数据拆到两张表里,原子性由视图保证,客户端只发了一次请求。

拆表之后的问题

假设原来有一张 orders 表,既存订单基本信息,又存收货地址。地址信息冗余严重,于是拆成两张表:

CREATE TABLE orders (
  order_id   BIGINT AUTO_INCREMENT PRIMARY KEY,
  user_id    BIGINT NOT NULL,
  status     VARCHAR(20) DEFAULT 'created',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE order_addresses (
  order_id   BIGINT PRIMARY KEY,
  city       VARCHAR(50),
  district   VARCHAR(50),
  street     VARCHAR(100),
  FOREIGN KEY (order_id) REFERENCES orders(order_id)
);

拆完之后,创建一笔订单需要这样写:

START TRANSACTION;

INSERT INTO orders (user_id, status) VALUES (1001, 'created');
-- 需要拿到 LAST_INSERT_ID() 才能写第二张表
SET @oid = LAST_INSERT_ID();
INSERT INTO order_addresses (order_id, city, district, street)
  VALUES (@oid, '北京', '海淀区', '中关村南大街5号');

COMMIT;

两个问题立刻浮现:

  1. 两次往返——客户端和数据库之间多了一轮交互(拿 LAST_INSERT_ID),网络延迟被放大。
  2. 事务形态变了——原本是自动提交的单条 INSERT,现在变成显式事务,锁持有时间从一条语句拉长到整个事务块,并发吞吐可能下降。

Duality View 怎么解决

MySQL 9.7 引入的 JSON Duality View,本质是定义一个 JSON 文档结构和底层关系表的映射关系。对视图做 CRUD,数据库自动把操作翻译到底层表上。

创建视图的语法:

CREATE JSON DUALITY VIEW order_view AS
  orders
    { _id: '$.order_id',
      user_id: '$.user_id',
      status: '$.status',
      created_at: '$.created_at',
      address:
        order_addresses
          { city: '$.city',
            district: '$.district',
            street: '$.street'
          }
    };

这段定义说的是:order_view 对应的 JSON 文档,顶层字段来自 orders 表,嵌套的 address 字段来自 order_addresses 表,通过 order_id 关联。

现在插入一笔订单,只需要一条语句:

INSERT INTO order_view VALUES (
  '{"user_id": 1001, "status": "created", "address": {"city": "北京", "district": "海淀区", "street": "中关村南大街5号"}}'
);

数据库内部做了三件事:

  1. 解析 JSON,把顶层字段写入 orders,拿到生成的 order_id
  2. 把嵌套的 address 字段连同 order_id 写入 order_addresses
  3. 整个操作在一个内部事务里完成,对外表现为一条语句的自动提交。

客户端只发了一次请求,没有 LAST_INSERT_ID 的往返,没有显式事务,锁的持有范围回到单语句级别。

实操:从建表到验证

以下脚本可以在 MySQL 9.7+ 上直接运行。先确认版本:

mysql -u root -p -e "SELECT VERSION();"
# 期望输出类似 9.7.x

完整建表、建视图、插入、验证流程:

-- 1. 建表
CREATE TABLE orders (
  order_id   BIGINT AUTO_INCREMENT PRIMARY KEY,
  user_id    BIGINT NOT NULL,
  status     VARCHAR(20) DEFAULT 'created',
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE order_addresses (
  order_id   BIGINT PRIMARY KEY,
  city       VARCHAR(50),
  district   VARCHAR(50),
  street     VARCHAR(100),
  FOREIGN KEY (order_id) REFERENCES orders(order_id)
);

-- 2. 建 Duality View
CREATE JSON DUALITY VIEW order_view AS
  orders
    { _id: '$.order_id',
      user_id: '$.user_id',
      status: '$.status',
      created_at: '$.created_at',
      address:
        order_addresses
          { city: '$.city',
            district: '$.district',
            street: '$.street'
          }
    };

-- 3. 单条 INSERT 插入两张表
INSERT INTO order_view VALUES (
  '{"user_id": 1001, "status": "created", "address": {"city": "北京", "district": "海淀区", "street": "中关村南大街5号"}}'
);

-- 4. 验证:两张表都有数据
SELECT * FROM orders;
SELECT * FROM order_addresses;

-- 5. 通过视图查询,拿到完整 JSON 文档
SELECT * FROM order_view;

SELECT * FROM order_view 会返回类似这样的 JSON:

{
  "_id": 1,
  "user_id": 1001,
  "status": "created",
  "created_at": "2025-07-10T12:00:00",
  "address": {
    "city": "北京",
    "district": "海淀区",
    "street": "中关村南大街5号"
  }
}

更新和删除同样走视图

更新只改地址,不需要知道 order_id

UPDATE order_view
SET doc = JSON_SET(doc, '$.address.city', '上海')
WHERE _id = 1;

删除会同时清理两张表的关联行:

DELETE FROM order_view WHERE _id = 1;
-- orders 和 order_addresses 对应行都被删掉

边界与取舍

Duality View 不是万能的,有几个需要注意的点:

方面 说明
版本门槛 需要 MySQL 9.7+,目前还在快速迭代,生产环境需评估稳定性
映射复杂度 嵌套层级越深,视图定义越复杂,调试成本上升
性能开销 JSON 解析和拆解有额外 CPU 开销,高吞吐场景需要基准测试
直接写底层表 仍然可以直接对 ordersorder_addresses 做 INSERT,但绕过了视图的一致性保障,地址行可能孤立
索引策略 视图本身不建索引,索引仍在底层表上;按 JSON 字段查询走视图时,能否命中索引取决于底层表的设计

适合用的场景:拆表后的写入路径是你想优化的瓶颈,且你的数据模型天然是"主表 + 从属子表"的父子结构。不适合的场景:两张表之间是多对多关系,或者嵌套层级超过三层——视图定义会变得难以维护。

上手检查清单

  1. 确认 MySQL 版本 ≥ 9.7,且 CREATE JSON DUALITY VIEW 语法可用。
  2. 识别你想拆的宽表,确认拆出的是从属子表(逻辑上是主表的嵌套属性)。
  3. 建底层两张表,外键指向主表。
  4. 定义 Duality View,_id 映射主表主键,嵌套对象映射子表。
  5. 用一条 INSERT INTO view VALUES ('{...}') 替换原来的事务块。
  6. 基准测试:对比原方案(事务 + 两次往返)和新方案(视图 + 单次往返)的延迟和吞吐。
  7. 确认应用代码不再直接写子表,所有写入走视图,避免数据不一致。

拆表不再意味着必须接受事务膨胀和多次往返。Duality View 把"拆"的代价压到了最小——一条 INSERT,两张表,一次网络请求。


相关推荐