JSON-индексы

JSON-индексы — это разновидность вторичного индекса, реализованная поверх инвертированного индекса, которая ускоряет фильтрацию строк таблицы по условиям, накладываемым на содержимое колонок типа Json и JsonDocument. Индекс задействуется, если в предикате WHERE используются функции JSON_EXISTS и JSON_VALUE с выражениями JsonPath. В отличие от традиционных вторичных индексов, оптимизированных для поиска по равенству или диапазону отдельных колонок таблицы, JSON-индекс работает с произвольными путями внутри JSON-документа.

Общее описание поиска по JSON и устройства инвертированного индекса по путям JSON-документа см. в разделе Поиск по содержимому JSON-документов.

Характеристики JSON-индексов

JSON-индексы в YDB позволяют:

  • быстро фильтровать строки по JSON_EXISTS и JSON_VALUE с выражениями JsonPath;
  • комбинировать индексируемые условия операторами AND и OR;
  • использовать значения параметров запроса, переданные приложением, при обработке проверяемых предикатов.

JSON-индекс является глобальным синхронным индексом — его данные всегда согласованы с основной таблицей.

При выполнении запроса JSON-индекс может быть применён:

  • явно — через оператор <имя_таблицы> VIEW <имя_индекса>;
  • автоматически — оптимизатором, если предикат подходит под формальные правила.

Синтаксис JSON-индексов

Создание JSON-индекса:

Удаление JSON-индексов выполняется через ALTER TABLE:

ALTER TABLE documents DROP INDEX json_idx

Синтаксис запроса с явным указанием JSON-индекса:

Функции и выражения для работы с JSON в предикатах:

Готовые сценарии использования собраны в разделе рецептов поиска по JSON-документам.

Обновление JSON-индексов

JSON-индексы автоматически поддерживаются при модификации данных и обновляются синхронно вместе с основной таблицей. Таблицы с JSON-индексами поддерживают:

  • INSERT
  • UPSERT
  • REPLACE
  • UPDATE
  • DELETE

Пакетные операции (BATCH UPDATE и BATCH DELETE) для таблиц с JSON-индексами не поддерживаются. При попытке выполнения такого запроса над таблицей, для которой создан JSON-индекс, этот запрос будет отклонен, в приложение будет возвращена соответствующая ошибка, а данные останутся в неизменном состоянии.

Кроме того, для таблиц с JSON-индексами не поддерживается:

  • массовая загрузка данных через вызов BulkUpsert — запрошенная операция будет отклонена с соответствующим сообщением об ошибке;
  • автоматическое удаление строк по TTL — будут возвращены ошибки при попытке создать таблицу одновременно с TTL-политикой и JSON-индексом, а также при попытке получить такое сочетание свойств через команды ALTER TABLE.

Поддерживаемые предикаты

Для выполнения через JSON-индексы поддерживаются только выражения на основе функций JSON_EXISTS и JSON_VALUE в блоке WHERE, объединённые операторами AND / OR по правилам ниже.

JSON_EXISTS

Проверка существования пути или значения внутри фильтра JsonPath.

Разрешено:

-- Корень документа (значение не NULL)
WHERE JSON_EXISTS(doc, '$')

-- Цепочка ключей; индексы массивов «прозрачны»
WHERE JSON_EXISTS(doc, '$.user.name')
WHERE JSON_EXISTS(doc, '$.items[*].sku')
WHERE JSON_EXISTS(doc, '$.items[0 to last].active')

-- Фильтр ? (...) — предикаты внутри фильтра допустимы
WHERE JSON_EXISTS(doc, '$.items ? (@.price == 100)')
WHERE JSON_EXISTS(doc, '$.items ? (@.qty >= 1 && @.qty <= 10)')
WHERE JSON_EXISTS(doc, '$.items ? (@.tag == $t)' PASSING "sale" AS t)

-- Методы JsonPath (путь индексируется до метода; точную проверку выполняет пост-фильтр)
WHERE JSON_EXISTS(doc, '$.value.type()')
WHERE JSON_EXISTS(doc, '$.arr.size()')

-- Комбинации на одной колонке
WHERE JSON_EXISTS(doc, '$.a') AND JSON_EXISTS(doc, '$.b')
WHERE JSON_EXISTS(doc, '$.a') OR JSON_EXISTS(doc, '$.b')

Запрещено (ошибка при использовании оператора VIEW или отказ автовыбора индекса):

-- Предикаты сравнения на верхнем уровне пути (вне ? (...))
WHERE JSON_EXISTS(doc, '$.key == 10')
WHERE JSON_EXISTS(doc, 'exists($.key)')
WHERE JSON_EXISTS(doc, '$.key starts with "a"')

-- Отрицание в JsonPath
WHERE JSON_EXISTS(doc, '!($.key == 10)')

-- ON ERROR TRUE
WHERE JSON_EXISTS(doc, '$.key' TRUE ON ERROR)

-- Путь без оператора контекста ($) — переданный документ не используется
WHERE JSON_EXISTS(doc, '1')

Примечание

Функция JSON_EXISTS возвращает true для любого непустого результата JsonPath. Предикат $.key == 10, указанный на верхнем уровне, дал бы «существование пути» даже когда сравнение ложно, что не соответствует ожидаемой семантике. Сравнения нужно выносить в вызовы JSON_VALUE или в фильтр вида ? (...).

JSON_VALUE

Извлечение скалярного значения с обязательным RETURNING <тип>.

Для сравнения значения через JSON_VALUE всегда нужно указывать RETURNING с нужным типом. По умолчанию JSON_VALUE возвращает тип Utf8, что во время выполнения запроса приводит к некорректному сравнению — значения разных типов сравниваются как строки:

$tmp = Json(@@["1", 1]@@);
SELECT JSON_VALUE($tmp, '$[0]') == "1"; -- true: корректно, строка сравнивается со строкой
SELECT JSON_VALUE($tmp, '$[1]') == "1"; -- true: некорректно, число сравнивается со строкой

Поддерживаемые типы для секции RETURNING: Int8Int64, Uint8Uint64, Float, Double, Bytes (String), Text (Utf8), Bool.

Примеры предикатов, применяемых через JSON-индекс:

-- Равенство (путь + значение попадают в индекс)
WHERE JSON_VALUE(doc, '$.user.age' RETURNING Int32) = 25
WHERE JSON_VALUE(doc, '$.flag' RETURNING Bool) = true
WHERE JSON_VALUE(doc, '$.name' RETURNING Utf8) = "Alice"u

-- Неявное сравнение с true для Bool
WHERE JSON_VALUE(doc, '$.active' RETURNING Bool)

-- Параметры
WHERE JSON_VALUE(doc, '$.user.id' RETURNING Int64) = $id
WHERE JSON_VALUE(doc, '$.tag' RETURNING Utf8) = $tag

-- Сравнения (в индексе задействован только путь, сравнение выполняет пост-фильтр)
WHERE JSON_VALUE(doc, '$.score' RETURNING Int64) > 0
WHERE JSON_VALUE(doc, '$.score' RETURNING Int64) != 100
WHERE JSON_VALUE(doc, '$.score' RETURNING Int64) BETWEEN 1 AND 10
WHERE JSON_VALUE(doc, '$.score' RETURNING Int64) NOT BETWEEN 0 AND 5

-- IN: список литералов
WHERE JSON_VALUE(doc, '$.status' RETURNING Utf8) IN ("open"u, "pending"u)

-- IN: заданный параметр типа List<Utf8>
WHERE JSON_VALUE(doc, '$.status' RETURNING Utf8) IN $status_list

-- PASSING для переменных JsonPath
WHERE JSON_VALUE(doc, '$.x ? (@.y == $v)' RETURNING Int64 PASSING 42 AS v) = 10

-- Предикаты JsonPath внутри пути.
-- В отличие от JSON_EXISTS, разрешены предикаты на верхнем уровне.
WHERE JSON_VALUE(doc, '$.user ? (@.role == "admin")' RETURNING Utf8) = "ok"u
WHERE JSON_VALUE(doc, '$.code starts with "A"' RETURNING String) != ""
WHERE JSON_VALUE(doc, 'exists($.meta)' RETURNING Bool)

-- Комбинации AND / OR на одной колонке
WHERE JSON_VALUE(doc, '$.a' RETURNING Int32) = 1
   OR JSON_VALUE(doc, '$.b' RETURNING Int32) = 2
WHERE JSON_EXISTS(doc, '$.a') AND JSON_VALUE(doc, '$.a' RETURNING Int32) = 10

Примеры предикатов, которые не могут быть применены через JSON-индекс:

-- Вызов JSON_VALUE без RETURNING
WHERE JSON_VALUE(doc, '$.key') = "x"

-- DEFAULT при ON EMPTY / ON ERROR (кроме NULL)
WHERE JSON_VALUE(doc, '$.k' RETURNING Utf8 DEFAULT "x" ON ERROR) = "y"

-- Неподдерживаемый тип данных в RETURNING
WHERE JSON_VALUE(doc, '$.ts' RETURNING Timestamp) = ...

-- RETURNING Bool с операторами сравнения
WHERE JSON_VALUE(doc, '$.flag' RETURNING Bool) >= true

-- IS NULL / IS NOT NULL — семантически противоречат индексу «существования пути»
WHERE JSON_VALUE(doc, '$.k' RETURNING Utf8) IS NULL

-- Сравнение двух JSON_VALUE с разных колонок
WHERE JSON_VALUE(doc1, '$.k' RETURNING Utf8) = JSON_VALUE(doc2, '$.k' RETURNING Utf8)

-- Вложенные JSON_* в аргументах
WHERE JSON_VALUE(JSON_QUERY(doc, '$.a'), '$.b' RETURNING Utf8) = "x"

Примечание

Для проверки «значение равно false» или «значение равно null» используйте фильтр JsonPath внутри JSON_EXISTS, например JSON_EXISTS(doc, '$.k ? (@ == false)') или JSON_EXISTS(doc, '$.k ? (@ == null)'), а не JSON_VALUE(...) IS NULL.

Ограничения

  • JSON-индексы поддерживаются только для строковых таблиц.
  • Первичный ключ таблицы должен состоять из единственной колонки целочисленного типа (Uint64, Uint32, Int64 или Int32). Это временное ограничение, которое будет снято в ходе дальнейшего развития.
  • В одном JSON-индексе индексируется ровно одна колонка типа Json или JsonDocument.
  • Выражение COVER для JSON-индексов не поддерживается.
  • Для таблиц с JSON-индексами не поддерживается ряд операций и механизмов модификации данных.
  • Тип параметра запроса чтения из индекса не может быть обёрнут в Optional<T> — опциональные параметры не поддерживаются.
  • Сравнение по равенству с целочисленным литералом, абсолютное значение которого превышает 2⁵³, не ускоряется индексом по значению (такие числа не помещаются в численный тип, используемый в Json и JsonDocument) и сводится к проверке существования пути.
  • Приведение вещественных литералов (Float, Double) к целочисленным типам при сравнении не выполняется — такое сравнение не ускоряется индексом.

Рецепты

Готовые сценарии работы с JSON-индексом:

Связанные материалы