lenec ru

← все посты

Postgres JSONB vs JSON: когда что выбирать на практике

18K

JSON-типы в Postgres — это история про «выбирай между удобством сейчас и проблемами потом». Я несколько раз ловила и одно, и второе, поэтому хочу разложить, чем JSON и JSONB отличаются на самом деле и когда какой брать.

Короткая разница

В Postgres два типа: json и jsonb. Звучат похоже, но устроены принципиально по-разному.

  • json хранит исходный текст почти как есть. Парсер только проверяет, что это валидный JSON. Пробелы, порядок ключей, дубли ключей — всё сохраняется.
  • jsonb — бинарный разобранный формат. Пробелов нет, порядок ключей не гарантирован, дубли отбрасываются (остаётся последний). Зато внутрь можно индексироваться, добираться до полей быстро, делать оператор @>.

Это и есть главный вопрос на собеседованиях. На практике 95% случаев — это jsonb. json я применяла буквально пару раз, и то с конкретной целью.

Где выигрывает jsonb

Скорость доступа к полям

Если тебе нужно достать одно поле, jsonb уже разобран — это просто чтение по offset. На json каждый раз парсится заново.

SELECT payload->>'user_id' FROM events WHERE id = $1;

На json эта операция парсит всю строку. На jsonb — берёт поле напрямую. Разница ощутима на тысячах вызовов.

Индексы

На jsonb работают GIN-индексы, и это даёт быстрые запросы вида «у объекта есть такой ключ» и «объект содержит такое поле».

CREATE INDEX events_payload_gin_idx
  ON events USING gin(payload jsonb_path_ops);

SELECT * FROM events WHERE payload @> '{"type":"signup"}';

На обычном json такой индекс не построишь: для GIN нужен бинарный формат. Поэтому если ты планируешь поиски по содержимому — это автоматически jsonb.

Богатый набор операторов и функций

На jsonb работает jsonb_set, jsonb_path_query, jsonb_object_agg, JSON_TABLE в Postgres 17. Большая часть инструментов в стандартной библиотеке делалась под jsonb. На json половины этих функций нет или они работают через cast.

Где выигрывает json

Это короткий список, но он есть.

  • Хранение «как есть». Если важен исходный порядок ключей и форматирование — например, ты сохраняешь подписанный JWT, для которого подпись считается по конкретной строке — нужно использовать json или просто text. jsonb перепакует тебе данные.
  • Архив, лог запросов, аудит. Когда ты складываешь сырой запрос или ответ, и поверх ничего не ищешь, json чуть дешевле по записи: не надо разбирать структуру.
  • Очень большие документы, к которым ты не обращаешься. На больших объектах разбор в jsonb — заметная нагрузка на запись.

Во всех остальных случаях — jsonb.

Реальные ловушки jsonb

1. TOAST и большие объекты

jsonb-объект больше 2 КБ Postgres утаскивает в TOAST — отдельное хранилище со сжатием. На больших документах сжатие даёт хорошее место, но любой доступ к полю становится дороже: сначала разжать, потом разобрать.

Если у тебя есть «горячие» поля, по которым ты постоянно фильтруешь — выноси их в отдельные колонки. Это и индексировать проще, и не упираешься в TOAST.

2. Уникальные индексы по полю

Хочется уникальность по payload->>'external_id'. Делается так:

CREATE UNIQUE INDEX events_external_id_uidx
  ON events ((payload->>'external_id'))
  WHERE payload ? 'external_id';

Скобочки вокруг выражения обязательны: уникальный индекс по выражению. И WHERE добавляю, чтобы записи без поля не конфликтовали.

3. jsonb_set с null

Очень классическая ошибка. jsonb_set(value, path, null) ставит null. А jsonb_set(null, ...) возвращает null. Если в строке payload null, ты не сможешь обновить вложенное поле — нужно coalesce(payload, '{}'::jsonb).

UPDATE events
SET payload = jsonb_set(coalesce(payload, '{}'::jsonb), '{status}', '"done"')
WHERE id = $1;

4. Партиальные обновления и ?, @>, ?|

Если массово фильтруешь «у объекта есть такое поле и оно равно X», смотри в сторону @>. Он ложится на GIN с jsonb_path_ops и обычно даёт лучший план, чем сложные комбинации ? и стрелочек.

SELECT * FROM events WHERE payload @> '{"status":"done","channel":"email"}';

Полу-структурированные данные: jsonb vs отдельная таблица

Иногда хочется свалить всё в jsonb и не возиться со схемой. Это работает первые полгода. Потом накапливаются проблемы:

  • Типы внутри jsonb — это text, numeric, boolean. Дата и timestamp — строки. Всякий раз cast.
  • Пустые поля и null поля — разные сущности. Половина запросов ловит баги именно тут.
  • Миграции данных в jsonb писать сложнее, чем DDL по обычной колонке.

Простое правило: если по полю когда-нибудь придётся писать миграцию данных или агрегацию — оно должно жить в обычной колонке, а не в jsonb. jsonb хорош для расширяемых атрибутов, которых много, но они read-mostly.

Производительность: что я мерила у себя

На таблице 50 млн строк, payload — около 1.5 КБ jsonb с 12 ключами:

  • Доступ к полю по id: jsonb — субмилли, json — 0.3 мс на разбор + чтение.
  • Поиск с условием по полю: с GIN-индексом по jsonb — десятки мс на 100 совпадениях; с json без индекса — секунды.
  • Запись: jsonb с 1.5 КБ — на 5–10% медленнее, чем такой же текст в json. Платим за разбор.

То есть для read-heavy сценариев jsonb выигрывает катастрофически за счёт индексов. Для write-heavy без поиска — json чуть бодрее, но обычно не настолько, чтобы ради этого отказываться от индексов.

Как я решаю на старте проекта

Когда проектирую таблицу с гибким полем, я задаю себе два вопроса:

  1. Будет ли поиск по содержимому? Если да — jsonb с GIN. Если нет — можно json или text.
  2. Будут ли отдельные поля «вылазить» в горячий путь? Если да — выношу их в обычные колонки. jsonb остаётся для всего остального.

Этого хватает. На моём опыте, если ответы на оба вопроса нечёткие, лучше брать jsonb: расширяться будет проще.

Краткая шпаргалка

  • По умолчанию — jsonb.
  • Хранение «как есть» (подпись, аудит, архив без поиска) — json или text.
  • Большие документы со сжатием — jsonb попадёт в TOAST, об этом помни.
  • GIN-индекс с jsonb_path_ops для оператора @>.
  • Уникальность по полю — индекс по выражению с скобками.
  • Горячие поля выноси в отдельные колонки заранее.

jsonb — отличный инструмент, но он не заменяет нормализацию. Это удобный карман для атрибутов, которых много и они «полу-структурированы». Всё остальное по-прежнему живёт в обычных колонках, и нет ничего плохого, чтобы половина таблицы была привычным реляционным полем, а вторая половина — гибкими данными в jsonb.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.