暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

在 MySQL 中对 JSON 编制索引

原创 肯肯在学习 2022-10-11
749

MySQL在2015年中期发布了MySQL 5.7.8,为我们提供了JSON数据类型。从那时起,它一直被用作逃避严格的列定义并存储所有形状和大小的JSON文档的一种方式:审核日志,配置设置,第三方有效负载,用户定义的字段等。

尽管MySQL为我们提供了读取和写入JSON数据的函数,但您很快就会发现明显缺少的东西:直接索引JSON列的功能

在其他数据库中,直接索引 JSON 列的最佳方法通常是通过一种称为广义倒排索引(简称 GIN)的索引类型。由于MySQL不提供GIN索引,因此我们无法直接为整个存储的JSON文档编制索引。但是,MySQL确实为我们提供了一种间接索引存储的JSON文档部分的方法。

根据您使用的 MySQL 版本,您有两个为 JSON 编制索引的选项。在MySQL 5.7中,您必须创建一个中间生成的列,但从MySQL 8.0.13开始,您可以[直接创建功能索引-(https://dev.mysql.com/doc/refman/8.0/en/create-index.html#create-index-functional-key-parts)。

让我们从一个示例表开始,该表用于记录在应用程序中执行的各种操作。

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`) )
复制

在该表中,我们将插入具有以下形状的 JSON 文档:

{ "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 编制索引

生成的列可以被视为计算列、计算列或派生列。它是一个列,其值是表达式的结果,而不是直接数据输入。表达式可以包含文本值、内置函数或对其他列的引用。表达式的结果必须是标量和确定性的。

由于我们尝试在 properties 列中为 request.email 字段编制索引,因此生成的列将使用 JSON 取消引用提取运算符来提取值。

为了验证我们是否已正确形成表达式,我们将首先运行一个 SELECT 语句并检查结果。

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

该运算符是一个速记的、不带引号的提取运算符,使其等效于 JSON_UNQUOTE(JSON_EXTRACT(column, path)) 。我们可以用长手写出 SELECT 语句,并得到相同的结果。

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

您选择哪种方法是个人喜好的问题!

现在我们已经确认表达式是有效且准确的,让我们使用它来创建生成的列。

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

ALTER 语句的第一部分应该看起来非常熟悉,我们正在添加一个名为 email 的列并将其定义为 VARCHAR(255) 。在语句的后半部分,我们生成了列,并且它应该始终等于properties->>"$.request.email"表达式的结果 。

我们可以像选择任何其他列一样,通过选择它来确认我们的列已添加。

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

你会看到MySQL现在正在为我们维护这个列。如果我们要更新 JSON 值,生成的列值也会更改。

现在我们已经准备好了生成的列,我们可以像添加任何其他列一样向其添加索引。

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

就是这样!现在,你已在 JSON 的 properties 列中索引了 request.email 键。让我们验证MySQL是否会使用索引来加速对电子邮件进行过滤的查询。

mysql> EXPLAIN SELECT * FROM activity_log WHERE email = 'little.bobby@tables.com'; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: activity_log partitions: NULL type: ref possible_keys: email key: email key_len: 768 ref: const rows: 1 filtered: 100.00 Extra: NULL
复制

MySQL报告它计划使用 email 索引来满足此查询。

生成的列索引和优化器

MySQL的优化器是一个强大而神秘的实体。当我们给MySQL一个命令时,我们是在告诉它我们想要什么,而不是如何得到它。通常MySQL会接受我们的查询并稍微重写它,这是一件好事!在数十年中,数万小时使优化程序变得有效和高效。

当涉及到生成列上的索引时,优化程序可以“查看”不同的访问模式,以确保正在使用基础索引。

我们在 email 上定义了一个索引,它是一个基于表达式properties->>"$.request.email" 生成的列。我们已经证明,当我们对 email 列进行查询时,会使用索引。更有趣的是,优化程序足够聪明,如果我们忘记对命名 email 列进行查询,它可以帮助我们!

在以下查询中,我们不按名称访问生成的列,而是使用速记 JSON 提取运算符。(为简洁起见,EXPLAIN 语句中省略了某些行。

mysql> EXPLAIN SELECT * FROM activity_log -> WHERE properties->>"$.request.email" = 'little.bobby@tables.com'; *************************** 1. row *************************** id: 1 possible_keys: email key: email key_len: 768 [...]: [...]
复制

即使我们没有按名称显式处理列,优化程序也会知道基于该表达式生成的列上有一个索引,并选择使用该索引。谢谢优化器!

我们可以确认,longhand也是如此。

mysql> EXPLAIN SELECT * from activity_log WHERE -> JSON_UNQUOTE( -> JSON_EXTRACT(properties, "$.request.email") -> ) = 'little.bobby@tables.com'; *************************** 1. row *************************** id: 1 possible_keys: email key: email key_len: 768 [...]: [...]
复制

同样,优化程序“通读”我们的表达式并使用电子邮件索引。

不相信?让我们看一下优化程序正在执行的操作,方法是在前面的 EXPLAIN 语句之后运行 SHOW WARNINGS 以查看重写的查询。

mysql> SHOW WARNINGS; *************************** 1. row *************************** Level: Note Code: 1003 Message: /* select#1 */ select `activity_log`.`id` AS `id`,`activity_log`.`properties` AS `properties`,`activity_log`.`created_at` AS `created_at`,`activity_log`.`email` AS `email` from `activity_log` where (`activity_log`.`email` = 'little.bobby@tables.com')
复制

如果仔细观察,您会发现优化程序已重写我们的查询,并将相等性比较更改为引用索引列。如果您无法控制访问模式,因为查询是从代码库中的第三方包发出的,或者由于其他原因无法更改这部分代码,则此功能特别有用。

如果基础表达式不匹配,则优化程序将无法使用索引,因此在创建生成的列时请务必小心。MySQL 文档更详细地解释了优化程序对生成的列索引的使用。

功能索引

从MySQL 8.0.13开始,您可以跳过创建生成的列并创建所谓的“功能索引”的中间步骤。MySQL文档调用这些功能关键部分

功能索引是表达式上的索引,而不是列上的索引。听起来很像一个生成的列,不是吗?这听起来相似是有原因的,这是因为功能索引是使用隐藏的生成列实现的!我们不再需要创建生成的列,但仍在创建生成的列。

但是,有一些功能索引的陷阱,特别是在将它们用于JSON时。

像这样创建我们的 JSON 索引会很好:

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

但是,如果您尝试这样做,则会出现令人讨厌的错误:

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

这到底是怎么回事呢?在我们之前的示例中,我们是负责创建生成列的人,我们将其称为 VARCHAR(255),MySQL可以轻松对其进行索引。

但是,当我们使用功能索引时,MySQL将根据它推断的数据类型为我们创建该列。JSON_UNQUOTE 返回 LONGTEXT 值,是一个无法编制索引的值。

幸运的是,错误消息为我们指明了正确的方向:我们需要将值转换为非 LONGTEXT 的类型。使用 CHAR 函数进行转换会告诉 MySQL 推断一个 VARCHAR 数据类型

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

现在我们已经添加了索引,我们将通过运行 .EXPLAIN

mysql> EXPLAIN SELECT * FROM activity_log -> WHERE properties->>"$.request.email" = 'little.bobby@tables.com'; *************************** 1. row *************************** id: 1 select_type: SIMPLE table: activity_log partitions: NULL type: ALL possible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 1 filtered: 100.00 Extra: Using where
复制

不幸的是,我们的指数根本没有被考虑,所以我们还没有走出困境。

除非另有指定,否则将值转换为字符串会将排序规则设置为utf8mb4_0900_ai_ci。另一方面,JSON 提取函数返回带有排序规则的utf8mb4_bin字符串。这就是我们的问题所在!由于查询的表达式和存储的索引之间的排序规则不匹配,因此未使用我们的新功能索引。

最后一步是将强制转换的排序规则显式设置为utf8mb4_bin

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

重新运行前面的 EXPLAIN ,我们可以看到我们终于可以使用函数索引了。

mysql> EXPLAIN SELECT * FROM activity_log -> WHERE properties->>"$.request.email" = 'little.bobby@tables.com'; *************************** 1. row *************************** id: 1 possible_keys: email key: email key_len: 1023 [...]: [...]
复制

显然,功能索引有一些陷阱,其中一些是显式的,易于调试,而另一些则需要对文档进行更多的深入研究。

请记住,功能索引在引擎盖下使用隐藏的生成列。如果您希望自己控制生成的列(即使在MySQL 8.0.13及更高版本中),这是一个完全合理的方法!

虽然MySQL中可能不提供直接JSON索引,但特定密钥的间接索引可以涵盖大多数用例。

也不要只停留在 JSON 上!可以将生成的列和功能索引用于所有类型的常见、难以编制索引的模式。

继续前进,充满信心地进行索引。

原文标题:Indexing JSON in MySQL
原文作者:Aaron Francis
原文地址:https://planetscale.com/blog/indexing-json-in-mysql

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论