Индексирование JSON в MySQL

GuDron

dumpz.ws
Admin
Регистрация
28 Янв 2020
Сообщения
7,752
Реакции
1,448
Credits
25,205

Индексирование JSON в MySQL​

фронт.jpg
MySQL предоставил нам Для просмотра ссылки Войди или Зарегистрируйсяип данных JSON еще в середине 2015 года с выпуском MySQL 5.7.8. С тех пор он использовался как способ избежать жестких определений столбцов и хранить документы JSON всех форм и размеров: журналы аудита, настройки конфигурации, сторонние полезные нагрузки, пользовательские поля иподробнее.
Хотя MySQL предоставляет нам функции для чтения и записи данных JSON, вы быстро обнаружите то, чего явно не хватает: возможность прямого индексирования ваших столбцов JSON.
В других базах данных лучшим способом прямого индексирования столбца JSON обычно является тип индекса, известный как обобщенный инвертированный индекс, или сокращенно GIN. Поскольку MySQL не предлагает индексы GIN, мы не можем напрямую индексировать весь сохраненный документ JSON. Однако не все потеряно, потому что MySQL дает нам возможность косвенно индексировать части наших сохраненных документов JSON.
В зависимости от версии MySQL, которую вы используете, у вас есть два варианта индексации JSON. В MySQL 5.7 вам пришлось бы создать промежуточный Для просмотра ссылки Войди или Зарегистрируйся, но, начиная с MySQL 8.0.13, вы можете создать Для просмотра ссылки Войди или Зарегистрируйся напрямую.
Давайте начнем с примера таблицы, используемой для регистрации различных действий, выполняемых в приложении.
Код:
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": "[email protected]",
"firstName": "Little",
"formType": "vehicle-inquiry",
"lastName": "Bobby",
"message": "Hello, can you tell me what the specs are for this vehicle?",
"postcode": "75016",
"townCity": "Dallas"
}
}
В нашем примере мы будем индексировать emailключ внутри requestобъекта. Это позволит нашим (вымышленным) пользователям быстро находить формы, отправленные конкретными людьми.
Давайте рассмотрим наш первый вариант индексации: сгенерированные столбцы.

Индексирование JSON через сгенерированный столбец #​

Сгенерированный столбец можно рассматривать как вычисляемый, вычисляемый или производный столбец. Это столбец, значение которого является результатом выражения, а не прямого ввода данных. Выражение может содержать литеральные значения, встроенные функции или ссылки на другие столбцы. Результат выражения должен быть скалярным и детерминированным.
Поскольку мы пытаемся проиндексировать request.emailполе в propertiesстолбце, наш сгенерированный столбец будет использовать Для просмотра ссылки Войди или Зарегистрируйся значения.
Чтобы убедиться, что мы правильно сформировали наше выражение, мы сначала выполним SELECTинструкцию и проверим результаты.
фронт.jpg
->>Оператор является сокращенным оператором извлечения без кавычек, что делает его эквивалентным JSON_UNQUOTE(JSON_EXTRACT(column, path)). Мы могли бы написать предыдущее SELECTутверждение, используя longhand, и получить тот же результат.
фронт.jpg
Какой метод вы выберете, зависит от личных предпочтений!
Теперь, когда мы убедились, что наше выражение корректно и точно, давайте используем его для создания сгенерированного столбца.

Теперь, когда мы убедились, что наше выражение корректно и точно, давайте используем его для создания сгенерированного столбца.

Код:
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 | [email protected] |
+----+-------------------------+

Вы увидите, что MySQL теперь поддерживает этот столбец для нас. Если бы мы обновили значение JSON, сгенерированное значение столбца также изменилось бы.

Теперь, когда у нас есть сгенерированный столбец, мы можем добавить к нему индекс, как к любому другому столбцу.

Код:
ALTER TABLE activity_log ADD INDEX email (email) USING BTREE;

Вот именно! Теперь вы проиндексировали request.email ключ в своем properties столбце JSON. Давайте проверим, что MySQL будет использовать индекс для ускорения запросов, которые фильтруются по электронной почте.
Код:
mysql> EXPLAIN SELECT * FROM activity_log WHERE email = '[email protected]';
*************************** 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индекс для удовлетворения этого запроса.
 

GuDron

dumpz.ws
Admin
Регистрация
28 Янв 2020
Сообщения
7,752
Реакции
1,448
Credits
25,205

Сгенерированные индексы столбцов и оптимизатор#​

Оптимизатор MySQL - это мощная и таинственная сущность. Когда мы даем MySQL команду, мы говорим ему, чего мы хотим, а не как это получить. Часто MySQL принимает наш запрос и слегка его переписывает, что хорошо! Десятки тысяч часов в течение десятков лет ушли на то, чтобы сделать оптимизатор эффективным и действенным.

Когда дело доходит до индексов в сгенерированных столбцах, оптимизатор может "видеть" различные шаблоны доступа, чтобы гарантировать, что используется базовый индекс.

Мы определили индекс on email, который представляет собой сгенерированный столбец на основе выражения properties->>"$.request.email". Мы уже доказали, что индекс используется при запросе к email столбцу. Что еще интереснее, так это то, что оптимизатор достаточно умен, чтобы помочь нам, если мы забудем выполнить запрос к именованному email столбцу!

В следующем запросе мы не обращаемся к сгенерированному столбцу по имени, а вместо этого используем сокращенный оператор извлечения JSON. (Некоторые строки опущены из EXPLAIN инструкции для краткости.)
Код:
mysql> EXPLAIN SELECT * FROM activity_log
-> WHERE properties->>"$.request.email" = '[email protected]';
*************************** 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")
-> ) = '[email protected]';
*************************** 1. row ***************************
id: 1
possible_keys: email
key: email
key_len: 768
[...]: [...]
Опять же, оптимизатор "считывает" наше выражение и использует индекс электронной почты.

Не убедили? Давайте взглянем на то, что делает оптимизатор, запустив SHOW WARNINGSafter нашего предыдущего EXPLAIN оператора, чтобы увидеть переписанный запрос.

Код:
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` = '[email protected]')
Если вы присмотритесь, то увидите, что оптимизатор переписал наш запрос и изменил сравнение на равенство, чтобы ссылаться на индексированный столбец. Это особенно полезно, если вы не можете контролировать шаблон доступа, потому что запрос выдается из стороннего пакета в вашей кодовой базе, или вы не можете изменить эту часть своего кода по какой-либо другой причине.

Если базовое выражение не соответствует очень близко, то оптимизатор не сможет использовать индекс, поэтому будьте осторожны при создании сгенерированного столбца. Для просмотра ссылки Войди или Зарегистрируйся более подробно объясняет использование оптимизатором сгенерированных индексов столбцов.

Функциональные индексы #​

Начиная с 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.

Итак, что здесь происходит? В наших предыдущих примерах мы отвечали за создание сгенерированного столбца и объявили его как a VARCHAR(255), который легко индексируется MySQL.

Однако, когда мы используем функциональный индекс, MySQL создаст для нас этот столбец на основе типа данных, который он определяет. JSON_UNQUOTE возвращает LONGTEXT значение, которое не может быть проиндексировано.

К счастью, сообщение об ошибке указывает нам правильное направление: нам нужно привести наше значение к типу, который не LONGTEXT является . Приведение с использованием CHAR функции Для просмотра ссылки Войди или Зарегистрируйся.
Код:
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" = '[email protected]';
*************************** 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" = '[email protected]';
*************************** 1. row ***************************
id: 1
possible_keys: email
key: email
key_len: 1023
[...]: [...]

Очевидно, что функциональные индексы имеют несколько подводных камней, некоторые из которых являются явными и простыми в отладке, а некоторые требуют немного более глубокого изучения документации.

Помните, что функциональные индексы используют скрытые сгенерированные столбцы под капотом. Если вы предпочитаете самостоятельно управлять сгенерированным столбцом (даже в MySQL 8.0.13 и более поздних версиях), это вполне разумный подход!

В то время как прямое индексирование JSON может быть недоступно в MySQL, косвенное индексирование определенных ключей может охватывать большинство вариантов использования.

Также не останавливайтесь только на JSON! Вы можете использовать сгенерированные столбцы и функциональные индексы для всех типов распространенных, трудно индексируемых шаблонов.