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

Если у товара есть цвет, размер и вес, а у другого — срок гарантии, состав и параметры питания, JSONB позволяет хранить различающиеся атрибуты без десятков пустых столбцов. Но как только эти свойства нужно часто фильтровать, связывать с другими записями или проверять на уникальность, удобный документ начинает требовать обходных решений. Для примеров и особенностей ниже ориентируемся на PostgreSQL 16.
В чём разница между JSONB и таблицей
Обычная таблица задаёт структуру явно: каждая строка представляет запись, а столбцы — её поля. Для повторяющихся сущностей можно создать отдельную таблицу и связать её с основной внешним ключом. PostgreSQL проверяет типы столбцов, а ограничения могут обеспечивать уникальность и обязательность данных.
JSONB хранит объект или массив в одном столбце в бинарном представлении PostgreSQL. Это не просто текст с фигурными скобками: база умеет искать значения внутри документа и строить по ним индексы. Но для полей внутри JSONB база не создаёт автоматически те же гарантии, что для столбцов и связей. Если связь на другой объект лежит в JSON, PostgreSQL не обеспечит её внешним ключом.
Термин: GIN — тип индекса PostgreSQL, подходящий для поиска по компонентам составных значений, в том числе JSONB. Индекс ускоряет подходящие запросы, но занимает место и требует обновления при изменении данных.
Когда JSONB действительно удобнее
Атрибуты меняются от записи к записи
JSONB подходит для необязательных и неодинаковых свойств: пользовательских настроек, параметров интеграции, характеристик товаров в разных категориях. Если новый атрибут появляется редко, а большинство запросов получает весь объект целиком, документ может быть проще, чем отдельная таблица с большим числом пустых значений.
Это не означает, что схема больше не нужна. Приложению всё равно важно проверять допустимые поля, типы и обязательные значения. Иначе одна часть системы запишет число, другая — строку, а ошибка проявится только в отчёте или клиентском интерфейсе.
Данные приходят и уходят как один документ
Если приложение получает настройки или описание внешнего объекта готовым JSON и обычно читает их вместе, хранить данные одним полем бывает естественно. Не приходится раскладывать произвольный набор атрибутов по отдельным строкам и собирать обратно при каждом чтении.
Проверьте, что именно значит «вместе». Если документ приходится регулярно соединять с таблицей справочника, искать по нескольким его свойствам и агрегировать значения, это уже не просто вложенный объект: он участвует в реляционных запросах.
Когда отдельная таблица окупает свою сложность
Поле стало самостоятельной сущностью
Создавайте таблицу, если у значения появились собственный идентификатор, связи, жизненный цикл или правила целостности. Например, адреса пользователя можно хранить в документе настроек, пока это непросматриваемый дополнительный набор. Но если по адресу ищут заказы, его нужно проверять, редактировать независимо или ссылаться на него из других данных, отдельная строка обычно делает модель яснее.
По полю часто фильтруют и считают
JSONB позволяет искать по вложенным данным, а GIN-индекс может помочь запросам с поддерживаемыми операторами. В PostgreSQL 16 класс операторов jsonb_ops поддерживает больше операций, включая проверку наличия ключа; jsonb_path_ops поддерживает более узкий набор, в том числе проверку вхождения, и может быть компактнее. Это не обещание, что он быстрее именно на ваших данных: проверяйте план и стоимость обслуживания индекса.
Если приложение постоянно сортирует, группирует или соединяет записи по одному конкретному свойству, обычный столбец часто проще индексировать и использовать. Выраженческий индекс по извлечённому из JSONB значению — возможный промежуточный вариант, но запросы должны соответствовать выражению индекса, а структура поля всё равно останется менее явно проверяемой.
Записи обновляют независимо и параллельно
PostgreSQL блокирует строку при её изменении. Если несколько операций меняют разные свойства одного JSONB-документа в одной строке, они всё равно могут конкурировать за эту строку. Разделение независимых и часто обновляемых сущностей по строкам помогает уменьшить такую конкуренцию и упрощает адресное обновление. Но оно добавляет соединения и усложняет чтение: платить придётся в другую сторону.
Если вы часто обсуждаете, как достать поле из JSONB, но редко — зачем это поле является частью документа, значит, оно уже может быть отдельной сущностью.
Сравнение подходов
| Критерий | JSONB | Отдельная таблица |
|---|---|---|
| Неодинаковые и редко меняющиеся атрибуты | Удобно: структура может различаться между объектами | Потребуется продумать столбцы или отдельные записи атрибутов |
| Типы, обязательность и уникальность | Часть проверок придётся поддерживать в приложении или ограничениях | Столбцы, ограничения и уникальные индексы выражают правила явно |
| Поиск и аналитика | Возможны с операторами JSONB и подходящими индексами; сложность зависит от запросов | Столбцы проще использовать в соединениях, фильтрах и группировках |
| Обновление независимых значений | Изменение всё равно относится к строке-владельцу документа | У каждой сущности своя строка, но чтение может потребовать соединения |
| Обслуживание | Меньше таблиц, но индексы и правила для документа нужно проектировать отдельно | Больше явных связей и запросов, зато структура понятнее базе и команде |
Как выбрать без гадания
- Соберите реальные запросы. Запишите, какие поля читают, по каким фильтруют, что сортируют и группируют. Если поле участвует в основных запросах постоянно, не прячьте его автоматически в документ.
- Назовите правила целостности. Нужны ли уникальность, обязательное значение, связь с другой записью или запрет на конкретный тип? Если такие правила важны и их трудно выразить для JSONB, вынесите данные в столбцы или таблицу.
- Посмотрите на частоту изменений. Отдельно обновляемые записи и частые параллельные изменения — аргумент в пользу отдельных строк. Если документ меняется и читается преимущественно целиком, JSONB может быть уместнее.
- Проверьте решение на своих данных. Сравните типичные запросы на реалистичном объёме и параллельности, посмотрите план выполнения, время ответа и влияние индексов на запись. Универсального порога, после которого JSONB обязательно нужно заменить таблицей, нет.
Что ломается на практике
- JSONB превращают в склад всего. Схема исчезает только на уровне базы, а проверки переезжают в код. Несколько сервисов могут начать записывать несовместимые варианты одного поля.
- Индексируют документ целиком без проверки запросов. Индекс не ускоряет любые обращения к JSONB; важны конкретный оператор и класс индекса. Кроме того, индексы требуют диска и увеличивают стоимость записи.
- Хранят массив связанных объектов внутри строки. Если элементам массива нужны собственные внешние ключи, уникальность или регулярные выборки, модель быстро усложняется. Нормальная таблица часто яснее.
- Переносят поле в таблицу, не пересматривая запросы. Выигрыш в ограничениях может сопровождаться дополнительными соединениями. Измеряйте полный рабочий запрос, а не только скорость чтения одной таблицы.
В PostgreSQL 16 JSONB — не замена реляционной модели и не её противоположность. Часто разумно оставить в столбцах идентификаторы и свойства, по которым строится основной запрос, а изменчивую периферию хранить документом. Когда свойство становится самостоятельным участником бизнес-логики, его проще превратить в явную связь.
Что проверить дальше
Возьмите несколько самых частых запросов к проблемной сущности и проверьте, какие поля в них участвуют. Затем сравните два варианта схемы на копии данных: измерьте чтение, запись и размер нужных индексов. Если проблема в одном постоянно используемом поле, начните с него — не переносите весь JSONB только потому, что один запрос оказался неудобным.
Проверка перед сдачей: Заимствования, вода, структура, соответствие требованиям
Спросить за 20 ₽Источники: PostgreSQL 16: типы JSON, PostgreSQL 16: индексы GIN
Частые вопросы
JSONB медленнее отдельной таблицы?
Можно ли индексировать отдельное поле внутри JSONB?
Можно ли хранить связанные объекты массивом в JSONB?
Нужно ли переносить в таблицу всё содержимое JSONB, если одно поле стало важным?
Подходы отличаются в других версиях PostgreSQL?
Как понять, что JSONB стал проблемой в проде?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.