Learning Platform
Урок 11.02 · 18 мин
Средний
JSONBJSONjsonb operatorsschema designEAV

JSONB — самый популярный тип в современном Postgres и одновременно самый частый источник архитектурных проблем. Он позволяет хранить произвольную JSON-структуру внутри одной ячейки и работать с ней почти так же эффективно, как со столбцами таблицы. Это решает реальную проблему: иногда у объектов много полей с разной структурой, и описать каждую в виде колонки нереалистично.

Но платформа за эту гибкость собирает дань: типизация частичная, индексация дорогая, отладка сложнее. Этот урок — про то, как пользоваться JSONB осмысленно.

JSON vs JSONB — выбирай всегда JSONB

В Postgres есть два типа: JSON и JSONB. На первый взгляд они одинаковые, но различие фундаментальное:

  • JSON хранит текст. Каждый раз при чтении или операторе Postgres парсит его заново.
  • JSONB хранит разобранный бинарный формат. Парсинг один раз — при INSERT, а потом операторы работают по дереву напрямую.

JSONB теряет порядок ключей и схлопывает дубликаты ключей (при повторе побеждает последний). JSON сохраняет всё «как было». На практике порядок ключей нужен лишь в редких случаях — например, при подписи payload’а для внешнего API.

Дефолтный выбор
JSONB. Дальше в курсе мы будем работать только с ним.

JSON vs JSONB на диске

JSON — сырой текст, парсится на каждый запрос. JSONB — заранее разобранное дерево, читается быстро.

JSONtext
хранениестрока байт-в-байт
чтение поляparse → walk → return
порядок ключейсохраняется
индексынеэффективны
JSONBbinary tree
хранениеразобранное дерево
чтение поляwalk → return
порядок ключейне сохраняется
индексыGIN, expression — эффективны

Наша JSONB-вселенная

В колонке customers.profile лежат настройки клиента, в products.attrs — характеристики товара. Посмотрим, что есть:

Глянем на содержимое profile у первых клиентов:

PostgreSQL

Атрибуты ноутбуков — там самые «жирные» JSONB:

PostgreSQL

Операторы доступа: → и →>

Два главных оператора:

  • -> возвращает значение как JSONB.
  • ->> возвращает значение как TEXT.

Это самое путающее место в JSONB. Разница в одном символе >, но семантика разная: -> оставляет тип JSONB (можно идти дальше внутрь), ->> извлекает «наружу» как обычную строку.

Сравни возвращаемые типы:

PostgreSQL

Заметь — as_jsonb показывается в кавычках ("email"), потому что это всё ещё JSONB-строковое значение. as_text — без кавычек, это уже обычный TEXT.

Правило: ->> — если нужно сравнивать со строкой или показывать пользователю. -> — если нужно «провалиться» глубже внутрь объекта.

Найди клиентов, которые подписаны на рассылку:

PostgreSQL

Обрати внимание на ::boolean — это явный cast. JSONB не делает implicit cast: если ты сравнишь profile->>'newsletter' = TRUE без cast, Postgres пожалуется на несовпадение типов.

Доступ по индексу — массивы внутри JSONB

-> и ->> работают и с массивами — но в качестве ключа передаётся целое число (индекс с нуля, как в JS):

Если бы у нас был JSONB-массив, доступ был бы такой:

PostgreSQL

Обрати внимание: внутри JSONB-массивов индексация с нуля и поддерживаются отрицательные индексы — это резко отличается от родных TEXT[], где индексация с единицы.

Глубокий путь: #> и #>>

Когда нужно нырнуть на несколько уровней, цеплять -> -> -> неудобно. Для этого есть path-операторы:

  • #> принимает массив ключей, возвращает JSONB.
  • #>> принимает массив ключей, возвращает TEXT.

Достанем количество USB-C портов у ноутбуков — это вложенный путь attrs.ports.usb_c:

PostgreSQL

Эта запись эквивалентна attrs -> 'ports' -> 'usb_c', но читается лучше, особенно когда путь длинный.

Проверка существования ключа: оператор ?

Иногда нужно знать «есть ли вообще такой ключ» — не «равно ли значение чему-то», а именно есть/нет.

У каких клиентов в profile проставлен флаг b2b:

PostgreSQL

Оператор ? проверяет наличие ключа на верхнем уровне JSONB. Это не то же самое, что profile->'b2b' IS NOT NULL — на верхнем уровне совпадает, но семантика немного разная: ? ищет именно ключ, а -> пытается достать значение (и не отличит «ключа нет» от «ключ есть, но значение JSON null»).

Агрегация и группировка по JSONB-полям

JSONB-поля можно использовать в GROUP BY и агрегатах ровно как обычные колонки — нужно только не забывать про cast при сравнении.

Сколько продуктов в каждом цвете:

PostgreSQL

Средний вес ноутбуков — числовое значение из JSONB:

PostgreSQL

В обоих запросах extracted-значение явно приводится к нужному типу: для числовой агрегации это критично — иначе Postgres попытается сложить строки.

Когда JSONB — это хорошо, а когда — побег от схемы

Хорошие сценарии:

  • Сильно вариативные атрибуты товара (разные продукты — разные характеристики). Делать колонку под каждое поле = разреженная таблица из 200 колонок.
  • Снимки внешних данных (webhook payload, ответ API), где структура продиктована поставщиком и может меняться.
  • Конфигурации пользователя, где набор настроек большой и редко используется в фильтрах.

Плохие сценарии (то, что должно быть колонками):

  • Поля, по которым часто фильтруют или сортируют (e-mail, дата создания, статус). Колонка с B-tree индексом всегда быстрее JSONB-фильтра.
  • Поля с FOREIGN KEY. JSONB не умеет ссылочную целостность.
  • Поля с CHECK constraint на конкретное значение. Тоже сложнее, чем в обычной колонке.
  • Когда в JSONB лежит «таблица под видом объекта»{"1": {...}, "2": {...}}, где ключи это id. Это
    EAV-антипаттерн
    в чистом виде; почти всегда лучше нормальная таблица.
Проверка знанийKnowledge check
У продуктов в attrs могут быть очень разные характеристики: у ноутбука weight_kg и ports, у книги pages и language. Команда хочет вообще не делать дополнительных колонок и положить всё в JSONB. Какие два следующих шага нужны для нормальной работы такой схемы в проде?
ОтветAnswer
1) Договориться о стабильных именах ключей и описать их в документации/типах приложения — JSONB не помешает положить туда что угодно, и через полгода поле окажется то 'weight_kg', то 'weight', то 'weightKg'. 2) Поставить GIN-индекс на attrs, иначе запросы вроде WHERE attrs @> '{"color":"black"}' будут seq scan на каждой выборке. На очень частые фильтры (например, по color) полезно вынести expression index: CREATE INDEX ON products ((attrs->>'color')). Опционально 3) добавить CHECK на тип значения (jsonb_typeof(attrs->'pages') = 'number'), чтобы случайно не положить туда строку.
Document-моделирование: embedding и referencing STRUCT в DuckDB: вложенный тип

Чек-лист

  • Используй JSONB, не JSON. JSON — только когда нужна побайтовая идентичность.
  • -> возвращает JSONB (можно идти глубже), ->> возвращает TEXT (готово к сравнению со строкой).
  • #> и #>> ходят по пути из массива ключей — удобнее, чем длинная цепочка ->.
  • Индексация массивов внутри JSONB — с нуля, в отличие от TEXT[] (с единицы).
  • ? проверяет наличие ключа на верхнем уровне.
  • При сравнении JSONB-значения с числом или булем — нужен явный cast (::int, ::boolean).
  • JSONB оправдан для вариативных, редко-фильтруемых данных. Поля, по которым фильтруют каждый запрос, должны быть обычными колонками.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. В чём ключевое различие JSON и JSONB в PostgreSQL?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7