JSONB в PostgreSQL: практические правила без лишней магии
JSONB удобен для атрибутов, которые меняются от записи к записи, но не заменяет схему базы и не ускоряет запросы сам по себе. Разбираем, что хранить в документе, какой индекс выбрать и где гибкость начинает дорого обходиться.

Если положить в JSONB весь заказ, а затем фильтровать по его полям, гибкость быстро обернётся индексами, которые не помогают запросам, и обновлениями больших документов. В PostgreSQL 16 JSONB лучше применять для переменных атрибутов, а поля, по которым постоянно ищут, сортируют и связывают таблицы, чаще оставлять обычными колонками.
Сначала решите, что именно меняется
JSONB хранит разобранное бинарное представление JSON: PostgreSQL может искать значения и строить индексы, но не сохраняет порядок ключей и повторяющиеся ключи объекта так, как они были в исходном тексте. Если важна точная исходная строка, например для подписи или аудита, JSONB для неё не подходит.
Практичный кандидат для JSONB — набор необязательных характеристик товара, параметры внешнего события или настройки, структура которых зависит от типа записи. Имя покупателя, сумма заказа, статус и внешний ключ на другую таблицу — обычно плохие кандидаты: это поля с понятным типом и регулярными запросами.
JSONB снимает необходимость описывать каждый редкий атрибут отдельной колонкой. Он не снимает необходимость понимать, какие данные вы храните и как по ним будете искать.
Термин. GIN — тип индекса PostgreSQL для значений, в которых нужно искать отдельные элементы, например ключи и фрагменты JSONB. Он не является универсальным ускорителем: его польза зависит от оператора в запросе и структуры данных.
Три способа хранить поля
| Подход | Когда подходит | Цена решения |
|---|---|---|
| Обычные колонки | Поле известно заранее; по нему фильтруют, сортируют, считают или связывают записи. | Схему нужно менять при добавлении полей, зато типы и ограничения задаются явно. |
| JSONB целиком | Структура часто меняется, а чтение и запись обычно работают с документом как с целым. | Типы и обязательность значений приходится контролировать отдельно; индексы и запросы требуют внимания. |
| Гибрид | Стабильные и важные для запросов поля вынесены в колонки, редкие атрибуты лежат в JSONB. | Нужно определить границу между схемой и документом и не хранить одно значение в двух местах. |
На практике гибрид часто проще поддерживать: например, у заказа есть колонки с идентификатором и статусом, а JSONB содержит дополнительные свойства интеграции. Если значение из документа стало важным для отчётов или бизнес-правил, это сигнал оценить перенос в колонку.
Выбирайте индекс под запрос
Предположим, таблица orders содержит колонку payload типа jsonb. Обычный GIN-индекс подходит для запросов по содержимому документа:
CREATE INDEX orders_payload_gin
ON orders USING GIN (payload);
SELECT id
FROM orders
WHERE payload @> '{"status":"paid"}'::jsonb;
Оператор @> проверяет, содержит ли JSONB слева указанный фрагмент справа. Такой индекс полезен, когда запросы ищут разные вложенные ключи или фрагменты документа. Но он не превращает любое обращение к JSONB в быстрый поиск: например, выражение с извлечением текста может потребовать другого индекса.
Если приложение часто фильтрует по одному текстовому полю, создайте индекс на том же выражении, которое стоит в условии:
CREATE INDEX orders_status_idx
ON orders ((payload ->> 'status'));
SELECT id
FROM orders
WHERE payload ->> 'status' = 'paid';
Здесь ->> извлекает значение как текст. Такой B-tree индекс рассчитан именно на выражение payload ->> 'status'; запрос через другое выражение не обязан его использовать. Если статус — основное поле заказа, обычная колонка обычно понятнее и удобнее для ограничений.
Для GIN можно выбрать класс операторов jsonb_path_ops:
CREATE INDEX orders_payload_path_gin
ON orders USING GIN (payload jsonb_path_ops);
Он поддерживает поиск через @> и операторы JSONPath, но не проверку существования ключа оператором ?. Класс по умолчанию jsonb_ops поддерживает более широкий набор операций, в том числе проверку ключей. Не выбирайте класс по принципу «этот быстрее»: проверьте, какие операторы использует ваше приложение, и сравните план и размер индекса на своих данных.
Когда что выбирать
- Нужны сортировка, уникальность, внешние ключи или частые диапазонные фильтры? Начните с обычной колонки. Для JSONB-поля такие правила часто сложнее выразить и поддерживать.
- Нужно искать по разным ключам и фрагментам документа? Проверьте GIN с оператором
@>; если нужна проверка наличия ключа — убедитесь, что выбранный класс индекса её поддерживает. - Один ключ стал горячим в запросах? Сравните индекс на выражении вроде
payload ->> 'status'с переносом значения в колонку. Решение зависит от частоты запросов и требований к данным. - Большинство запросов читает документ целиком? JSONB может быть удобным форматом хранения, но измерьте стоимость записи и объём индексов до того, как объявлять его выигрышем.
Что ломается на практике
Запрос не использует индекс
Наличие GIN-индекса ещё не доказывает, что конкретный запрос его использует. Оператор может не поддерживаться выбранным классом, выражение — не совпадать с индексом, а планировщик — предпочесть другое выполнение. Смотрите EXPLAIN на реальном запросе и данных; для оценки фактического времени используйте EXPLAIN ANALYZE с пониманием, что команда действительно выполняет запрос.
Типы значений плавают
В JSON одно и то же поле может оказаться строкой в одной записи и числом в другой. Это осложняет фильтрацию и вычисления. Задайте правила записи в приложении, а критичные инварианты проверяйте подходящими ограничениями базы. Не полагайтесь на то, что каждое значение можно безошибочно привести к нужному типу.
Небольшое изменение переписывает большой документ
Обновление JSONB-поля всё равно создаёт новую версию строки PostgreSQL. Если документ большой и часто меняется по одному мелкому атрибуту, растут затраты на запись и обслуживание таблицы. Особенно внимательно проверьте такой сценарий для часто обновляемых полей. Инвалидация и очистка устаревших данных — отдельная тема; здесь речь только о выборе хранения и индексов.
Путают отсутствующий ключ и null
В документе могут различаться отсутствующий ключ и JSON-значение null; кроме того, у колонки может быть SQL NULL. Условия на эти случаи не всегда равнозначны. Перед миграцией или отчётом проверьте, какие из вариантов уже есть в данных.
Что измерить перед запуском
У PostgreSQL нет универсального порога, после которого JSONB или GIN «становятся медленными». Сравните варианты на копии характерных данных: используйте те же запросы, объём документов, частоту обновлений и параллельность, что ожидаются в приложении. Запишите планы выполнения, задержки чтения и записи, размер таблицы и индексов. Замер без описания данных и нагрузки нельзя переносить на другой проект.
Для PostgreSQL 16 сверяйте поддерживаемые операторы и поведение индексов с документацией именно этой версии. Начните с одного реального запроса, выберите под него способ хранения и индекс, затем проверьте план на данных, похожих на продакшен. После запуска следите за временем запросов и ростом индексов — и переносите в обычные колонки те JSONB-поля, которые стали частью постоянной логики приложения.
Редактор изображений: Убрать лишнее, заменить фон, добавить объект, расширить кадр — на вашем изображении.
Спросить за 20 ₽Источники: PostgreSQL 16: типы JSON, PostgreSQL 16: индексы для JSONB
Частые вопросы
JSONB быстрее обычного JSON?
Какой индекс поставить на JSONB?
Можно ли индексировать только одно поле JSONB?
Что делать, если запрос не использует созданный индекс?
Есть ли смысл класть весь объект в JSONB?
Какие подводные камни есть в продакшене?
Советы отличаются для другой версии PostgreSQL?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.