В SQLite нет типа JSON — и это нормально
В SQLite отсутствует отдельный тип данных для JSON. Документ хранится в обычной колонке TEXT, а набор встроенных функций — известный как расширение JSON1 — умеет его разбирать, читать и изменять. JSON1 уже встроено в любую современную сборку SQLite, так что ставить ничего не нужно.
Идея простая: храните документ как текст, а заглядывать внутрь поручайте функциям.
Две строки, в каждой — JSON-документ в обычной текстовой колонке. Теперь нужно научиться доставать оттуда значения.
Извлечение полей: json_extract и оператор ->>
json_extract(column, path) достаёт значение из JSON-документа. Путь всегда начинается с $ (корень документа), для ключей объекта используется .field, а для индексов массива — [i].
Писать json_extract(data, '$.name') в каждом запросе быстро надоедает, поэтому в SQLite есть два удобных оператора:
->возвращает значение в JSON-виде (строки приходят вместе с кавычками).->>возвращает обычное SQL-значение (текст или число, без кавычек).
name_json вернёт "Ada" (это всё ещё JSON), а name_text — просто Ada. Оператор ->> берите, когда значение нужно сравнить или показать пользователю. А -> — когда результат пойдёт дальше на вход другой JSON-функции.
Фильтрация по полям JSON
Раз уж мы умеем доставать значения, то и фильтровать по ним сможем. Выражение пишем в WHERE так же, как и любое другое:
Работает, но на таблице любого приличного размера это будет тормозить — для проверки условия SQLite приходится парсить JSON в каждой строке. Чуть ниже починим это с помощью индекса.
Собираем JSON: json_object и json_array
В обратную сторону тоже можно — конструировать JSON прямо в запросе:
json_object('k1', v1, 'k2', v2, ...) собирает объект, а json_array(v1, v2, ...) — массив. Удобно, когда нужно собрать ответ для API прямо в SQL, и эти функции спокойно вкладываются друг в друга:
Изменение JSON: json_set, json_insert, json_replace
В SQLite есть три близкие по смыслу функции, которые меняют JSON-документ и возвращают его новую версию:
json_set(doc, path, value)— записывает значение по пути: создаёт его, если такого пути ещё нет, и перезаписывает, если он уже существует.json_insert(doc, path, value)— добавляет значение только в том случае, если по этому пути ещё ничего нет.json_replace(doc, path, value)— наоборот, обновляет значение только если путь уже существует.
Важный момент: эти функции не меняют документ на месте — они возвращают новый JSON. Поэтому результат обычно сохраняют обратно через UPDATE:
Обратите внимание: json_set умеет принимать сразу несколько пар «путь — значение» в одном вызове. А чтобы убрать ключ, используйте json_remove(doc, path).
Разворачиваем JSON-массив через json_each
json_each — это табличная функция: она берёт JSON-массив (или объект) и возвращает по строке на каждый элемент. Благодаря этому задача «найти пользователей с тегом admin», которая на чистом SQL выглядит громоздко, превращается в обычный JOIN:
Каждая строка из users соединяется с элементами своего массива tags. json_each отдаёт удобные колонки: key, value, type, fullkey. Её родственник json_tree обходит документ рекурсивно, заходя в каждый вложенный узел — пригодится, когда структура документа заранее неизвестна.
Индексы по JSON-полям в SQLite
Запрос WHERE data ->> '$.active' = 1 выше работает, но SQLite приходится парсить каждую строку, чтобы проверить условие. Для полей, по которым вы ищете часто, заведите индекс по выражению:
Индекс должен использовать ровно то же выражение, что и запрос. Если в индексе у вас json_extract(data, '$.email'), а в запросе — data ->> '$.email', совпадения не будет, и индекс просто останется лежать без дела. Выберите один вариант и придерживайтесь его.
Для полей, по которым запросы идут постоянно, удобнее завести генерируемый столбец:
email для тех, кто пишет запросы, выглядит как самый обычный столбец, но при этом автоматически синхронизируется с JSON.
Проверка корректности JSON
json_valid(text) возвращает 1, если текст парсится как JSON, и 0 в противном случае. В связке с ограничением CHECK это позволяет отсекать кривые данные ещё на этапе записи:
Первая вставка пройдёт, а вторая упадёт с ошибкой ограничения. Без такой проверки кривой JSON будет тихо лежать в таблице, пока через пару месяцев какой-нибудь вызов json_extract не рванёт на ровном месте.
JSON и JSONB: в чём отличия
Начиная с SQLite 3.45 появилось двоичное представление — JSONB. Это те же данные, но заранее распарсенные в компактный бинарный формат, чтобы функции не разбирали JSON заново при каждом обращении. Семейство функций jsonb_* (jsonb_extract, jsonb_set, jsonb_object и так далее) возвращает JSONB вместо текста, а к колонкам с JSONB применимы те же операторы, что и к обычному JSON.
Простой JSON (текст) удобен, когда документы должны быть читаемыми глазами в дампах и легко поддаваться осмотру. JSONB стоит брать тогда, когда таблица большая, к ней часто обращаются, и накладные расходы на парсинг реально вылезают в профайлере. Не переключайтесь на JSONB по умолчанию — читаемость обычного JSON очень выручает при отладке.
Когда JSON — правильный выбор
Колонки с JSON хорошо себя показывают, когда:
- Структура меняется от строки к строке (события, журналы аудита, входящие вебхуки от интеграций).
- Вы кэшируете ответ внешнего API и хотите сохранить его как есть.
- Поле запрашивается редко и почти никогда не используется в фильтрах.
И плохо подходят, когда:
- JSON используется, чтобы не проектировать схему. Если у всех строк одни и те же поля — это колонки, а не JSON.
- По значению часто нужно фильтровать или джойнить. Обычная колонка с индексом всегда обгонит обращение по JSON-пути.
- Напрашиваются внешние ключи. У JSON нет реляционной целостности.
Золотая середина — комбинировать оба подхода: скалярные колонки для полей, по которым идут запросы и ограничения, и рядом колонка JSON для «длинного хвоста» переменных данных.
Что дальше: полнотекстовый поиск
JSON даёт гибкость на стороне хранения. На следующей странице разберём FTS5 — встроенный в SQLite движок полнотекстового поиска, который умеет нормальный поиск по тексту с ранжированием и подсветкой, и это сильно мощнее, чем LIKE.
Часто задаваемые вопросы
В каком виде SQLite хранит JSON?
Отдельного типа под JSON в SQLite нет — данные лежат как обычный TEXT. Работу с ним обеспечивает встроенное расширение JSON1 (включено по умолчанию начиная с версии 3.38): функции json_extract, json_set, json_each и другие парсят этот текст и работают с ним как с JSON. С версии 3.45 появился ещё и бинарный формат JSONB — он быстрее, если к одному документу обращаются часто.
Как достать значение из JSON-колонки в SQLite?
Через json_extract(column, '$.path') или короткий оператор ->>. Например, SELECT data ->> '$.name' FROM users вернёт поле name из JSON-документа в колонке data. В путях $ — это корень, .field — обращение к ключу объекта, [i] — индекс массива.
Можно ли построить индекс по полю внутри JSON?
Да, через индекс по выражению: CREATE INDEX idx_user_email ON users(json_extract(data, '$.email')). Если в WHERE использовать ровно то же выражение — индекс подхватится. Когда поле дёргают часто, чище получается вариант с generated-колонкой и обычным индексом по ней.
Чем отличаются операторы -> и ->> в SQLite?
-> возвращает JSON-значение (строки приходят в кавычках, как валидный JSON), а ->> — обычное SQL-значение (текст или число, без кавычек). Для вывода или сравнения в WHERE берите ->>, а -> удобен, когда дальше идёт ещё один JSON-вызов по цепочке.