bigywater.tech
MySQL / InnoDB · 19

生成列与函数索引:把计算结果交给索引

建一张10万行的JSON表,在生成列city上建索引,对比直接查JSON路径表达式和查生成列的EXPLAIN与耗时,看生成列到底省在哪。

自动识别
优化器能认出JSON路径表达式等价于生成列
3.6倍
查生成列本身比查JSON表达式快的倍数
Covering vs 回表
两者的执行差异根源

做的事:JSON字段 + 一个STORED生成列 + 索引

payload是JSON列,city是从payload里提取出来的生成列,在city上建索引。

建表插入CREATE TABLE t6 ( id INT PRIMARY KEY AUTO_INCREMENT, payload JSON, city VARCHAR(50) GENERATED ALWAYS AS (payload->>'$.city') STORED, INDEX idx_city(city) ); 插入10万行,payload是{"city": "...", "age": ...}这样的JSON

city列不是手动写入的,是MySQL根据 payload->>'$.city' 这个表达式,在每次INSERT/UPDATE时自动计算出来、存到磁盘上的(STORED)。它上面能像普通列一样建索引。

验证:直接查JSON路径 vs 查生成列,优化器的反应

一种写法直接对JSON字段用路径表达式过滤,一种写法过滤生成列本身。

EXPLAIN结果EXPLAIN SELECT id FROM t6 WHERE payload->>'$.city' = 'Shenzhen' → Index lookup on t6 using idx_city (city = 'Shenzhen') (cost=4808 rows=48076) EXPLAIN SELECT id FROM t6 WHERE city = 'Shenzhen' → Covering index lookup on t6 using idx_city (city = 'Shenzhen') (cost=5108 rows=48076)

意外发现:哪怕WHERE条件写的是 payload->>'$.city' 这个JSON路径表达式,压根没提到"city"这个生成列的名字,MySQL优化器依然认出了这个表达式跟生成列的定义完全一致,直接用上了 idx_city 索引——不需要你在SQL里手动写"city"这个列名。

验证:都用上了同一个索引,耗时却不一样

两条查询EXPLAIN显示都走了idx_city,但一个是"Index lookup"一个是"Covering index lookup"——耗时差多少。

EXPLAIN ANALYZE耗时WHERE payload->>'$.city' = 'Shenzhen' → 耗时 16.8 ms WHERE city = 'Shenzhen' → 耗时 4.63 ms (快约3.6倍)

原因就藏在EXPLAIN那两行结果的措辞差异里:查生成列city时是"Covering index lookup"——city本身就在索引里,查id(主键)也不需要回表,整条查询完全不碰聚簇索引。但查JSON路径表达式时是普通"Index lookup"(没有Covering)——虽然索引帮忙快速定位到了匹配的记录,但MySQL还要额外回到聚簇索引,把完整的payload列读出来,重新计算一次 payload->>'$.city' 去验证结果是否真的匹配,这一步的JSON解析和回表开销,就是3.6倍耗时差距的来源。

这就是生成列的价值:它不只是"给JSON字段加索引的一种手段",而是把"从JSON里解析出某个字段"这个计算过程,提前搬到写入的时候做一次,搬到磁盘上存好——查询的时候不用每次都重新解析JSON,直接读已经算好的结果。哪怕优化器聪明到能识别等价的JSON表达式并用上索引,也省不掉"回表验证"这一步的解析开销,只有真的用生成列本身做过滤,才能完全跳过这一步。

分步演示:生成列省下的那部分JSON解析开销

每一步都是本文上面用EXPLAIN和EXPLAIN ANALYZE跑出来的原始结果。

同一个索引,查表达式还是查生成列,耗时差3.6倍
这一步新验证的写法

    本文用到的工具

    • 本机 MySQL 26.7.0(InnoDB),10万行JSON表,STORED生成列+索引
    • EXPLAIN / EXPLAIN ANALYZE:对比表达式索引与生成列索引
    京ICP备2026067502号-1京公网安备11010502063487号