07 авг. 2025 г.·6 мин

PostgreSQL JSONB vs нормализованные таблицы: как выбрать и мигрировать

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

PostgreSQL JSONB vs нормализованные таблицы: как выбрать и мигрировать

Настоящая проблема: двигаться быстро, не загородив себе путь

Требования, которые меняются каждую неделю, — нормальное явление, когда вы что-то создаёте. Клиент просит ещё одно поле. Отдел продаж хочет другой workflow. Саппорт требует трейл аудита. В итоге база данных несёт на себе последствия всех этих изменений.

Быстрая итерация — это не только более быстрый релиз экранов. Это возможность добавлять, переименовывать и удалять поля, не ломая отчёты, интеграции или старые записи. Это также способность отвечать на новые вопросы ("Сколько заказов за прошлый месяц имели отсутствующие сопроводительные документы?") без превращения каждого запроса в разовый скрипт.

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

Когда команды выбирают неправильную модель, симптомы обычно очевидны:

  • Простые вопросы превращаются в медленные, грязные запросы или кастомный код.
  • Две записи описывают одно и то же, но используют разные имена полей.
  • Опционные поля со временем становятся обязательными, и старые данные не соответствуют.
  • Невозможно гарантировать правила (уникальные значения, обязательные связи) без костылей.
  • Отчётность и выгрузки постоянно ломаются после небольших изменений.

Практическое решение такое: где вам нужна гибкость (и вы готовы терпеть несогласованность какое-то время), а где нужна структура (потому что данные управляют деньгами, операциями или соответствием требованиям)?

JSONB и нормализованные таблицы — просто объяснение

PostgreSQL может хранить данные в классических столбцах (text, number, date). Он также может хранить целый JSON-документ в столбце, используя JSONB. Разница не в «новизне» или «устаревшести». Разница в том, что вы хотите, чтобы база гарантировала.

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

Нормализованные таблицы означают разделение данных на отдельные таблицы по сущностям и соединение их через ID. Клиент в одной таблице, заказ в другой, и каждый заказ указывает на клиента. Это даёт более сильную защиту от противоречий.

В повседневной работе компромиссы просты:

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

Простой пример — кастомные поля тикета поддержки. С JSONB вы можете добавить новое поле завтра без миграции. С нормализованными таблицами добавление поля более осознанно, но отчёты и правила становятся понятнее.

Когда JSONB — правильный инструмент для быстрой итерации

JSONB — хороший выбор, когда ваш главный риск — построить неправильную форму данных, а не навязать строгие правила. Если продукт ещё ищет свой workflow, принудительная фиксация в таблицах может замедлить вас постоянными миграциями.

Хороший индикатор — когда поля меняются каждую неделю. Подумайте об onboarding-форме, где маркетинг постоянно добавляет вопросы, переименовывает метки и убирает шаги. JSONB позволяет хранить каждую отправку как есть, даже если завтра версия будет другой.

JSONB также подходит для «неизвестного»: данных, которые вы ещё не до конца понимаете, или данных, которыми вы не управляете. Если вы принимаете вебхуки от партнёров, сохранение сырого полезного пейлоада в JSONB позволяет поддерживать новые поля сразу и решить позже, какие из них сделать первоклассными столбцами.

Типичные ранние случаи: быстро меняющиеся формы, захват событий и логи аудита, настройки на клиента, флаги фич и эксперименты. Особенно полезно, когда вы в основном записываете данные, читаете их целиком и форма ещё движется.

Одна простая защита помогает больше, чем ожидают: ведите короткую общую заметку с ключами, которые вы используете, чтобы не получить пять вариантов написания одного и того же поля по разным записям.

Когда нормализованные таблицы — более безопасный долгосрочный выбор

Нормализованные таблицы выигрывают, когда данные перестают быть «только для этой фичи» и становятся общими, интенсивно опрашиваются и заслуживают доверия. Если люди будут срезать и фильтровать записи по разным измерениям (статус, владелец, регион, период), столбцы и связи делают поведение предсказуемым и проще для оптимизации.

Нормализация важна и когда правила должны применяться базой данных, а не «по доброй воле» в коде приложения. JSONB может хранить всё подряд — и это становится проблемой, когда нужны жёсткие гарантии.

Признаки, что пора нормализовать

Обычно стоит отходить от JSON-первого подхода, когда выполняются несколько из этих пунктов:

  • Вам нужна согласованная отчётность и дашборды.
  • Нужны ограничения: обязательные поля, уникальные значения или связи с другими записями.
  • Более одного сервиса или команды читают и пишут те же данные.
  • Запросы начинают сканировать много строк, потому что нельзя эффективно использовать простые индексы.
  • Вы в регулируемой или ревизуемой среде и правила нужно доказывать.

Производительность — частая точка перелома. В JSONB фильтрация часто означает частое извлечение значений. Можно индексировать JSON-пути, но требования растут в набор перекошенных индексов, которыми трудно управлять.

Конкретный пример

Прототип хранит «запросы клиентов» в JSONB, потому что каждый тип запроса имеет разные поля. Позже операциям нужна очередь, отфильтрованная по приоритету и SLA. Финансы хотят итоги по отделам. Саппорт требует гарантировать, что у каждого запроса есть customer ID и статус. Здесь нормализованные таблицы показывают свои сильные стороны: явные столбцы для общих полей, foreign key к клиентам и ограничения, которые не позволят попасть плохим данным.

Простой фреймворк решения за 30 минут

Деплойте или экспортируйте, когда готовы
Деплойте в AppMaster Cloud или на свою инфраструктуру вместе с сгенерированным исходным кодом.
Начать

Не нужно устраивать долгие дебаты о теории баз данных. Нужен быстрый письменный ответ на один вопрос: где гибкость ценнее строгой структуры?

Сделайте это с людьми, которые строят и используют систему (разработчик, операции, саппорт и, возможно, финансы). Цель — не выбрать один победивший подход, а найти правильное соответствие для каждой части продукта.

Чеклист из 5 шагов

  1. Выпишите 10 самых важных экранов и точные вопросы, на которые они отвечают. Примеры: «открыть карточку клиента», «найти просроченные заказы», «выгрузить выплаты за прошлый месяц». Если вы не можете назвать вопрос — вы не сможете для него спроектировать данные.

  2. Выделите поля, которые должны быть верны всегда. Это жёсткие правила: статус, суммы, даты, владение, права. Если неправильное значение будет стоить денег или вызвать пожар в саппорте, такое поле обычно должно быть в колонке с ограничениями.

  3. Отметьте, что меняется часто, а что редко. Еженедельные изменения (новые вопросы в форме, партнёрские детали) — сильные кандидаты для JSONB. Редко меняющиеся «ядровые» поля склоняются к нормализации.

  4. Решите, что должно быть доступно для поиска, фильтрации или сортировки в UI. Если пользователи постоянно фильтруют по полю — обычно лучше сделать его первоклассным столбцом (или аккуратно индексированным JSONB-путём).

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

Основы производительности без углубления в детали

Скорость обычно достигается одним: сделать ваши самые частые вопросы дешёвыми для выполнения. Это важнее идеологии.

Если вы используете JSONB, держите его маленьким и предсказуемым. Несколько дополнительных полей — нормально. Огромный постоянно меняющийся blob тяжело индексировать и легко неправильно использовать. Если вы знаете, что ключ будет существовать (например "priority" или "source"), сохраняйте согласованное имя ключа и одинаковый тип значения.

Индексы — не волшебство. Они меняют быстрые чтения на более медленные записи и больший объём диска. Индексируйте только то, по чему вы часто фильтруете или джоините, и только в той форме, в которой вы реально делаете запросы.

Практические правила по индексам

  • Ставьте обычные btree-индексы на частые фильтры: status, owner_id, created_at, updated_at.
  • Используйте GIN-индекс на JSONB, когда вы часто ищете внутри него.
  • Предпочитайте expression-индексы для одного-двух «горячих» JSON-полей (например (meta->>'priority')) вместо индексирования всего JSONB.
  • Используйте частичные индексы, когда важна только часть строк (например только где status = 'open').

Избегайте хранения чисел и дат как строк в JSONB. "10" сортируется раньше "2", и математические операции по датам становятся мучительными. Используйте настоящие числовые и timestamp-типы в столбцах, или по крайней мере храните числа в JSON как числа.

Часто выигрывает гибридная модель: основные поля в колонках, гибкие дополнения в JSONB. Пример: таблица операций с id, status, owner_id, created_at как столбцы и meta JSONB для опциональных ответов.

Частые ошибки, создающие боль позже

Регенерируйте при изменении требований
Изменяйте модель данных и регенерируйте backend и приложения по мере развития требований.
Начать

JSONB даёт ощущение свободы на раннем этапе. Боль обычно проявляется через месяцы, когда больше людей трогают данные и «как получится» превращается в «мы не можем ничего изменить без поломки». Эти шаблоны вызывают большую часть работы по чистке:

  • Относиться к JSONB как к свалке. Если каждая команда хранит чуть разные формы, вы получите кастомную логику парсинга повсюду. Заведите базовые соглашения: согласованные имена ключей, единые форматы дат и небольшое поле версии внутри JSON.
  • Прятать ключевые сущности внутри JSONB. Хранение клиентов, заказов или прав только как blob выглядит просто сначала, но потом джойны становятся неудобными, ограничения трудно применить, и появляются дубликаты. Держите who/what/when в колонках, а опциональные детали — в JSONB.
  • Откладывать миграцию до момента крайней необходимости. Если вы не отслеживаете существующие ключи, как они менялись и какие из них «официальны», первая серьёзная миграция становится рискованной.
  • Допускать мысль, что JSONB автоматически означает гибкость и скорость. Гибкость без правил — это просто несогласованность. Скорость зависит от паттернов доступа и индексов.
  • Ломать аналитику изменением ключей со временем. Переименование статус→state, смена чисел на строки или смешение часовых поясов тихо разрушит отчёты.

Конкретный пример: команда начинает с таблицы тикетов и поля details JSONB для ответов формы. Позже финансы хотят еженедельный разрез по категориям, операции — отслеживание SLA, саппорт — дашборд «открыто по команде». Если категории и временные метки разбегаются по ключам и форматам, каждый отчёт превращается в набор специальных случаев.

План миграции, когда прототип становится критичным для бизнеса

Поместите правила туда, где им место
Добавляйте обязательные поля и связи, чтобы снизить количество неконсистентных записей.
Начать сейчас

Когда прототип начинает управлять зарплатой, запасами или поддержкой клиентов, «пофиксим данные позже» перестаёт быть приемлемым. Самый безопасный путь — мигрировать небольшими шагами, так чтобы старые JSONB-данные продолжали работать, пока новая структура доказывает свою надёжность.

Фазовый подход избегает рискованного большого перелома:

  • Дизайн целевой схемы сначала. Опишите целевые таблицы, первичные ключи и правила именования. Решите, что является реальной сущностью (Customer, Ticket, Order) и что остаётся гибким (notes, опционные атрибуты).
  • Постройте новые таблицы рядом со старыми данными. Сохраните JSONB-столбец, добавьте нормализованные таблицы и индексы параллельно.
  • Бэ́кфилльте партиями и валидируйте. Копируйте поля из JSONB в новые таблицы кусками. Валидируйте по количествам строк, проверкам NOT NULL и выборочным проверкам.
  • Переключайте чтение раньше записи. Обновите запросы и отчёты, чтобы сначала читать из новых таблиц. Когда выводы совпадут, начните писать новые изменения в нормализованные таблицы.
  • Закройте доступ. Перестаньте писать в JSONB, затем удалите или заморозьте старые поля. Добавьте ограничения (foreign keys, unique), чтобы плохие данные не пролезли назад.

Перед окончательным переключением:

  • Прогоните оба пути неделю (старый vs новый) и сравните результаты.
  • Мониторьте медленные запросы и добавляйте индексы при необходимости.
  • Подготовьте план отката (feature flag или переключатель конфигурации).
  • Сообщите команде точное время переключения записи.

Быстрая проверка перед решением

Перед тем как закрепить подход, сделайте реальность-чек. Эти вопросы ловят большинство будущих проблем, пока изменения ещё дешёвые.

Пять вопросов, которые решают большинство исходов

  • Нужна ли нам сейчас (или в ближайшем релизе) уникальность, обязательные поля или строгие типы?
  • По каким полям пользователи должны фильтровать и сортировать (поиск, статус, владелец, даты)?
  • Понадобятся ли скоро дашборды, выгрузки или отчёты для финансов/операций?
  • Сможете ли вы объяснить модель данных новому человеку за 10 минут без ухищрений?
  • Какой у нас план отката, если миграция сломает рабочий процесс?

Если на первые три вопроса вы отвечаете «да», вы уже склоняетесь к нормализованным таблицам (или, как минимум, к гибриду: ядро нормализовано, а длиннохвост в JSONB). Если «да» только на последний — ваша большая проблема скорее процессная, а не схема.

Простое правило

Используйте JSONB, когда форма данных ещё не ясна, но вы можете назвать небольшой набор стабильных полей, которые всегда понадобятся (например id, owner, status, created_at). В тот момент, когда люди зависят от стабильных фильтров, надёжных выгрузок или строгой валидации, стоимость «гибкости» быстро растёт.

Пример: от гибкой формы к надёжной системе операций

Доставляйте ops-инструменты быстрее
Стройте внутренние инструменты и админ-панели поверх чистой модели Postgres.
Создать приложение

Представьте intake-форму поддержки, которая меняется еженедельно. На одной неделе вы добавляете «модель устройства», на следующей — «причину возврата», затем переименовываете «priority» в «urgency». Сначала поместить полезную нагрузку формы в один JSONB-столбец кажется идеальным. Изменения идут без миграций, и никто не жалуется.

Через три месяца менеджеры хотят фильтры типа «urgency = high и device model начинается с iPhone», SLA по клиентским уровням и еженедельный отчёт, который должен совпадать с прошлым. Режим отказа предсказуем: кто-то спрашивает «Куда ушло это поле?» — у старых записей было другое имя ключа, тип значения поменялся ("3" vs 3) или поле вовсе не существовало у половины тикетов. Отчёты превращаются в набор костылей.

Практическая середина — гибрид: держите стабильные, критичные поля как настоящие столбцы (created_at, customer_id, status, urgency, sla_due_at), а JSONB используйте для новых или редких полей, которые всё ещё меняются.

Низкоразрушительный график, который хорошо работает:

  • Неделя 1: Выберите 5–10 полей, которые обязаны быть фильтруемыми и отчётными. Добавьте столбцы.
  • Неделя 2: Бэ́кфилльте эти столбцы из существующего JSONB: сначала недавние записи, потом более старые.
  • Неделя 3: Обновите запись так, чтобы новые записи писали и в столбцы, и в JSONB (временная двойная запись).
  • Неделя 4: Переключите чтение и отчёты на столбцы. Оставьте в JSONB только допполя.

Следующие шаги: решите, задокументируйте и продолжайте итерации

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

Выпишите 5–10 вопросов, на которые ваше приложение должно быстро отвечать ("Показать все открытые заказы этого клиента", "Найти пользователей по email", "Отчёт по выручке по месяцам"). Рядом с каждым напишите ограничения, которые нельзя нарушать (уникальный email, обязательный статус, корректные суммы). Затем нарисуйте чёткую границу: оставляйте в JSONB поля, которые часто меняются и редко фильтруются или джоинируются, а всё, что вы ищете, сортируете, джоините или обязаны валидировать — продвигайте в столбцы и таблицы.

Если вы используете no-code платформу, которая генерирует реальные приложения, такое разделение можно проще поддерживать со временем. Например AppMaster (appmaster.io) позволяет визуально моделировать таблицы PostgreSQL и регенерировать backend и приложения по мере изменения требований, что делает итеративные изменения схем и плановые миграции менее болезненными.

Вопросы и ответы

Когда JSONB лучше, чем нормализованные таблицы?

Используйте JSONB когда форма данных часто меняется и вы в основном сохраняете и получаете полезную нагрузку целиком — например быстро меняющиеся формы, webhook-пейлоады партнёров, флаги фич или настройки на клиента. При этом держите небольшой набор стабильных полей в обычных столбцах, чтобы по ним можно было фильтровать и строить отчёты.

Когда стоит выбирать нормализованные таблицы вместо JSONB?

Нормализуйте, когда данные читаются и пишутся разными командами, их нужно гибко запрашивать, или им нужно по умолчанию доверять. Если вам нужны обязательные поля, уникальные значения, внешние ключи или стабильные дашборды и выгрузки — таблицы со явными столбцами и ограничениями обычно экономят время в будущем.

Подходит ли гибридный подход (столбцы + JSONB)?

Да — гибрид часто бывает лучшим по умолчанию: выносите критичные для бизнеса поля в столбцы и связи, а опционные или быстро меняющиеся атрибуты храните в JSONB-«meta»-столбце. Это сохраняет стабильность отчётности и правил, позволяя одновременно итеративно развивать длиннохвостые поля.

Как решить, какие поля хранить в столбцах, а какие в JSONB?

Спросите, по каким полям пользователи должны фильтровать, сортировать и экспортировать данные в UI, и какие значения должны быть верны всегда (деньги, статус, владение, права, даты). Если поле часто используется в списках, дашбордах или джойнах — продвигайте его в настоящий столбец; редко используемые дополнения оставляйте в JSONB.

Каковы основные риски использования JSONB для «всего»?

Главные риски — неконсистентные имена ключей, смешанные типы значений и молчаливые изменения во времени, которые ломают аналитику. Это предотвращают соглашения по ключам, единые форматы дат, небольшой «version»-поле внутри JSON и хранение чисел/дат в правильных типах.

Может ли JSONB быть безопасным для отчётности и валидации?

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

Как индексировать JSONB, не устроив бардак?

Индексируйте только то, что вы реально запрашиваете. Для обычных столбцов используйте btree для частых фильтров (status, timestamps); для JSONB предпочитайте expression-индексы по «горячим» ключам (например извлечение одного поля), вместо индексирования всего документа, если вы не ищете по множеству ключей.

По каким признакам понять, что пора мигрировать из JSONB в нормализованные таблицы?

Ищите медленные и грязные запросы, частые полные сканирования и растущую коллекцию одноразовых скриптов, чтобы ответить на простые вопросы. Сигналы также: несколько команд пишут одни и те же ключи по-разному, и появляется потребность в строгих ограничениях или стабильных выгрузках.

Какой безопасный план миграции от прототипа на JSONB к нормализованной схеме?

Спроектируйте целевые таблицы сначала, затем запустите их параллельно с JSONB-данными. Бэ́кфилльте партиями, валидируйте результаты, переключите чтение на новые таблицы, затем переключите запись, и в конце зафиксируйте данные ограничениями, чтобы плохие записи не вернулись.

Как no-code платформа вроде AppMaster помогает при изменениях схемы и миграциях?

Моделируйте основные сущности (customers, orders, tickets) как таблицы со явными столбцами для полей, по которым люди фильтруют и делают отчёты, и добавьте JSONB-столбец для гибких дополнений. Инструменты вроде AppMaster (appmaster.io) помогают итерациям: вы визуально обновляете модель PostgreSQL и регенерируете backend и приложения по мере изменений требований.

Легко начать
Создай что-то невероятное

Экспериментируйте с AppMaster с бесплатной подпиской.
Как только вы будете готовы, вы сможете выбрать подходящий платный план.

Попробовать AppMaster