登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL JSON_EXTRACT 查询为什么慢:用生成列索引做一次可验证优化实验

来源:17golang原创

时间:2026-07-22 12:11:10 278浏览 收藏

订单表里的渠道、地区和设备类型经常被塞进 JSON,业务查询写成 JSON_EXTRACT(order_attrs, '$.region') = '华东' 很自然,但普通索引往往帮不上忙。下面用一张小型 orders 表复现这个问题,再把查询字段落成生成列,最后用 EXPLAIN 核对优化器是否真的换了执行路径。

JSON_EXTRACT 走得慢的核心原因是普通索引覆盖不到 JSON 内部字段的计算逻辑,全量读取行数据后再做 JSON 解析会吃掉大量 IO,用和查询语义完全对齐的生成列建索引,就能把这部分筛选下压到索引层,大幅减少需要回表扫描的数据量。

要点速览
  • JSON 文档里被频繁筛选的字段,不能只看“字段有索引”就判断查询会变快。
  • 生成列要保持类型、路径和业务条件一致,CAST 与空值处理会影响索引命中。
  • 优化前后至少对照 keyrowsExtra,再结合实际耗时判断是否值得改表。
  • 联合索引的列顺序应贴合最常用的过滤条件,不能把所有 JSON 字段都提前展开。

先准备一条能复现的 JSON 查询

实验使用 MySQL 8.0 语法,假设订单表已经存在 order_attrs JSON 列。示例数据只为了观察查询计划,实际项目中可以替换成线上脱敏结构。

CREATE TABLE orders (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  user_id BIGINT NOT NULL,
  amount DECIMAL(10, 2) NOT NULL,
  order_attrs JSON NOT NULL,
  created_at DATETIME NOT NULL,
  KEY idx_created_at (created_at)
);

INSERT INTO orders (user_id, amount, order_attrs, created_at) VALUES
(1001, 199.00, '{"region":"华东","channel":"app","device":"ios"}', '2026-07-20 10:00:00'),
(1002, 88.50,  '{"region":"华南","channel":"web","device":"android"}', '2026-07-20 10:05:00');

把数据量补到真实量级后,再观察下面这条语句。这里不要急着加索引,先保存优化前的执行计划。

EXPLAIN
SELECT id, user_id, amount
FROM orders
WHERE JSON_UNQUOTE(JSON_EXTRACT(order_attrs, '$.region')) = '华东'
  AND created_at >= '2026-07-01';

为什么普通索引没有直接解决 JSON_EXTRACT

普通索引建在 created_at 上,只能帮助时间范围过滤;JSON 条件是对列内容再做函数计算。计划中的 key 可能仍然是 idx_created_at,也可能为空,关键要看 rows 是否明显偏大,以及 Extra 是否出现额外筛选。

MySQL orders 表先按 created_at 扫描,再等待 JSON_EXTRACT 筛选华东订单的查询路径示意图

如果 created_at 的范围已经很窄,普通索引可能仍然是合理选择;如果时间范围覆盖了大量订单,数据库就要读出更多行后逐条解析 JSON。优化方向不是“JSON 一律不能查”,而是把稳定、常用的筛选路径显式暴露出来。

把 region 落成生成列并建立联合索引

先增加一个与原条件语义一致的生成列。JSON_UNQUOTE 用来去掉字符串值的 JSON 引号,字段类型则按业务值长度给出边界。

ALTER TABLE orders
  ADD COLUMN region_name VARCHAR(32)
  GENERATED ALWAYS AS (
    JSON_UNQUOTE(JSON_EXTRACT(order_attrs, '$.region'))
  ) STORED;

ALTER TABLE orders
  ADD INDEX idx_region_created (region_name, created_at);

这里选 STORED 是为了让生成结果随行保存,写入时多一点计算,读取时可以直接走索引。若查询只按地区筛选,单列索引也够用;本例还带时间范围,所以把 created_at 放在第二列。

MySQL 生成列 region_name 与 idx_region_created 把华东和时间条件送入索引的优化路径示意图

用同一组条件核对优化是否生效

查询条件改为生成列后,先看计划再看实际运行。不要只凭 SQL 看起来更短就下结论。

EXPLAIN
SELECT id, user_id, amount
FROM orders
WHERE region_name = '华东'
  AND created_at >= '2026-07-01';

重点核对这三项:

  • key 是否变成 idx_region_created
  • rows 估算值是否从大范围扫描降到更接近目标数据量。
  • Extra 是否少了不必要的额外筛选;如果仍然读了很多行,要回头看数据分布和索引顺序。

在支持的环境中,可以用 EXPLAIN ANALYZE 对照实际行数与耗时。测试时固定时间范围,至少跑几次,避开刚好命中缓存的单次结果。

空值、类型和联合索引顺序是三个边界

生成列的表达式必须和查询语义稳定一致。JSON 中没有 region 时结果通常是 NULL,不要把它误当成空字符串;如果业务允许数字地区码,应该显式转换类型,避免字符串比较和数字比较混在一起。

检查项容易出现的偏差处理建议
路径$.region$.area 混用统一字段契约,先查样本数据
类型字符串值与数字值比较在生成列表达式中明确类型
顺序把低频字段放在联合索引第一列按常用过滤与选择性复核
写入代价一次展开过多 JSON 字段只为稳定高频条件建生成列

常见问题

生成列一定比直接查询 JSON 更快吗?

不一定。时间范围很小、数据量不大或 JSON 条件选择性很低时,改表的收益可能有限,应该用计划和实测结果决定。

可以直接给 JSON 列建立普通索引吗?

要看字段类型和查询方式。MySQL 支持针对 JSON 路径的函数索引写法,但团队需要统一表达式;生成列更容易被检查、复用和解释。

为什么加了索引,计划还是没有选它?

常见原因是统计信息、数据分布、条件选择性或表达式不一致。先确认查询确实使用了生成列,再更新统计信息并重新比较。

在线表很大时能直接 ALTER TABLE 吗?

不要把实验命令直接搬到生产。先评估表大小、写入峰值、锁等待和变更工具,安排灰度窗口并准备回退方案。

把实验结果留成一条可复查记录

这次优化的最小闭环是:保存原始 EXPLAIN,增加一列生成列,建立贴合条件的联合索引,再用完全相同的筛选范围复查 keyrows 与实际耗时。只有三个结果都朝预期变化,才值得把它推广到更多 JSON 字段。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>