如何为MySQL中的JSON字段设置索引

背景

MySQL在2015年中发布的5.7.8版本中首次引入了JSON数据类型。自此,它成了一种逃离严格列定义的方式,可以存储各种形状和大小的JSON文档,例如审计日志、配置信息、第三方数据包、用户自定义字段等。

虽然MySQL提供了读写JSON数据的函数,但你很快会发现一个显著的缺失:直接给JSON列建立索引的能力。

在其他数据库中,直接索引JSON列的最佳方法通常是使用一种叫做广义倒排索引(Generalized Inverted Index,简称GIN)的类型。然而,由于MySQL没有提供GIN索引,我们无法直接对整个存储的JSON文档建立索引。不过不必担心!MySQL确实为我们提供了一种间接索引存储在JSON文档中特定部分的方式。

根据所使用的MySQL版本,有两个选项可以给JSON建立索引:

  • 如果使用MySQL 5.7,需要创建一个中间生成列(Generated Column)
  • 从MySQL 8.0.13开始,可以直接创建函数索引(Functional Index)

接下来,我们以一个示例表为例,该表用于记录应用程序中的各种操作日志:

sql 复制代码
CREATE TABLE `activity_log` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `properties` json NOT NULL,
  `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
   PRIMARY KEY (`id`)
)

在该表的properties字段中插入如下结构的JSON文档:

perl 复制代码
{
  "uuid": "e7af5df8-f477-4b9b-b074-ad72fe17f502",
  "request": {
    "email": "little.bobby@tables.com",
    "firstName": "Little",
    "formType": "vehicle-inquiry",
    "lastName": "Bobby",
    "message": "Hello, can you tell me what the specs are for this vehicle?",
    "postcode": "75016",
    "townCity": "Dallas"
  }
}

在本例中,我们将尝试索引request对象内的email键,这可以让用户快速找到由特定人员提交的表单。

方法一:通过"生成列"索引JSON

生成列(Generated Column) 可以视为计算列、派生列或公式列。它的值是某个表达式的运算结果,而不是直接的数据输入。表达式可以包含常量值、内置函数或对其他列的引用。表达式的结果必须是定量的(Scalar)且具有确定性(Deterministic)。

由于我们试图索引properties列中的request.email字段,生成列将使用JSON的解引用(Unquoting Extraction)运算符来提取该值。

首先,运行一个SELECT语句来验证表达式是否正确:

sql 复制代码
mysql> SELECT properties->>"$.request.email" FROM activity_log;
+--------------------------------+
| properties->>"$.request.email" |
+--------------------------------+
| little.bobby@tables.com        |
+--------------------------------+

符号->>是解引用运算符,它等价于如下的写法:

sql 复制代码
mysql> SELECT JSON_UNQUOTE(JSON_EXTRACT(properties, "$.request.email"))
    ->   FROM activity_log;
+-----------------------------------------------------------+
| JSON_UNQUOTE(JSON_EXTRACT(properties, "$.request.email")) |
+-----------------------------------------------------------+
| little.bobby@tables.com                                   |
+-----------------------------------------------------------+

上述两种写法,具体使用哪种方式可完全取决于个人偏好。

确认表达式的有效性和准确性后,我们使用它创建一个生成列

sql 复制代码
ALTER TABLE activity_log ADD COLUMN email VARCHAR(255)
  GENERATED ALWAYS as (properties->>"$.request.email");

这条ALTER语句的前半部分非常熟悉,添加了一个名为email的列,并将其定义为VARCHAR(255)类型。而后半部分声明该列为生成列,并定义它始终等于表达式properties->>"$.request.email"的结果。

我们可以像其他列一样查询它,确认生成列已被成功添加:

sql 复制代码
mysql> SELECT id, email FROM activity_log;
+----+-------------------------+
| id | email                   |
+----+-------------------------+
|  1 | little.bobby@tables.com |
+----+-------------------------+

从结果可以看到,MySQL将动态维护这个列。如果我们更新了JSON数据,生成列的值也会随之改变。

接下来,我们像其他普通列一样为这生成列添加索引:

sql 复制代码
ALTER TABLE activity_log ADD INDEX email (email) USING BTREE;

现在已经成功为JSON中request.email键建立了索引。可以通过EXPLAIN验证索引是否会被用于查询:

ini 复制代码
mysql> EXPLAIN SELECT * FROM activity_log WHERE email = 'little.bobby@tables.com';

结果显示MySQL计划使用email索引来满足该查询。

索引生成列与优化器(Optimizer)

MySQL的优化器是一个强大但神秘的组件。当我们给MySQL下达命令时,它理解的是我们想要什么,而不是我们明确指定如何实现。通常,MySQL会稍微改写我们的查询,这通常是一件好事。

对于生成列上的索引,优化器能"透过"不同的访问模式以确保使用索引。例如,在以下查询中,我们通过JSON提取运算符访问数据,而不是直接使用生成的email列:

sql 复制代码
mysql> EXPLAIN SELECT * FROM activity_log
    ->   WHERE properties->>"$.request.email" = 'little.bobby@tables.com';

结果可以看到优化器仍然使用了email索引。哪怕使用长写的表达式,也可以看到优化器仍然"穿透"表达式并利用了索引,甚至可以通过SHOW WARNINGS查看优化器改写后的查询:

ini 复制代码
mysql> SHOW WARNINGS;

显示结果表明查询被改写为直接参考了索引的列。

方法二:函数索引(Functional Index)

从MySQL 8.0.13开始,可以跳过创建生成列的中间步骤,直接创建表达式索引(Function Index)。例如:

sql 复制代码
ALTER TABLE activity_log
  ADD INDEX email ((properties->>"$.request.email")) USING BTREE;

然而,当你尝试运行上述语句时会遇到错误:

sql 复制代码
ERROR: Cannot create a functional index on an expression that returns a BLOB or TEXT. Please consider using CAST.

这是因为MySQL自动推断JSON解引用操作返回LONGTEXT类型,而无法对其直接建立索引。可通过CAST将值转化为MySQL可索引的数据类型:

sql 复制代码
ALTER TABLE activity_log
  ADD INDEX email ((CAST(properties->>"$.request.email" AS CHAR(255)))) USING BTREE;

此外还需要解决字符集不匹配 的问题,需要显式设置排序规则为utf8mb4_bin

sql 复制代码
ALTER TABLE activity_log
  ADD INDEX email ((
    CAST(properties->>"$.request.email" AS CHAR(255)) COLLATE utf8mb4_bin
  )) USING BTREE;

运行EXPLAIN后可以确认函数索引已成功被使用。

总结

尽管MySQL无法直接对JSON列建立索引,但通过生成列和函数索引的方式间接索引特定字段能够满足绝大多数场景。同时这种方式不仅适用于JSON,还适用于其它复杂或难以索引的模式。

相关推荐
Ultipa7 小时前
查询语言的进化:SQL之后,为什么是GQL?数据世界正在改变
数据库·sql·图数据库·gql
LB21127 小时前
SQL隐式链接显式连接
大数据·数据库·sql
隔壁阿布都8 小时前
spring boot + mybatis 使用线程池异步修改数据库数据
数据库·spring boot·mybatis
MAGICIAN...16 小时前
【Redis】--持久化机制
数据库·redis·缓存
我真的是大笨蛋16 小时前
JVM调优总结
java·jvm·数据库·redis·缓存·性能优化·系统架构
步步为营DotNet17 小时前
5-2EFCore性能优化
数据库·性能优化·.net
2501_9200470318 小时前
Redis-集群
数据库·redis·bootstrap
半夏陌离19 小时前
SQL 拓展指南:不同数据库差异对比(MySQL/Oracle/SQL Server 基础区别)
大数据·数据库·sql·mysql·oracle·数据库架构
旋转的油纸伞19 小时前
SQL表一共有几种写入方式
数据库·sql