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

Банальный пример – пользователи и профили. Пользователь – это системная информация, и меняется она редко. А любимая группа и оконченный институт – это другой домен, и логично вынести его в другую таблицу.

Еще пример – рейтинг пользователя. Начинающие программисты создают в таблице users поле rating, и зря. Рейтинг – это забава, в которую любят играть продакт-оунеры. Как только вы сделаете рейтинг, вам скажут: теперь сделаем рейтинг по сезонам и будем отслеживать взлеты и падения. Вы же не будете делать колонки rating_summer_2025, rating_autumn_2025 и так далее. Все это поедет в новые таблицы, и первый рейтинг тоже должен был храниться отдельно.

Технический момент: в Postgres любая операция над строкой порождает новую строку. В этом плане он похож на Git: единица изменения – строка. Если в поле 20 колонок и вы изменили одну, будет создана новая строка на 20 колонок. Старая будет болтаться, пока ее не соберет vacuum. Если таблица обновляется часто, в ней должно быть как можно меньше полей. Если какой-то набор колонок не меняется, лучше вынести их в таблицу и связать один к одному.

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

Еще пример: индексация документов. В Postgres есть очень качественный полнотекстовый поиск – tsvector. С ним документы разбиваются на лексемы (стемминг), причем разбивка сильно зависит от указанного языка. На выходе получается набор уникальных лексем, причем каждая помнит свои положения в документе, порядок следования, а еще им можно задать веса. Разобранный документ хранят в отдельном поле, чтобы не парсить его каждый раз.

Предположим, у нас аналог Хабра, и есть таблица posts с полем content – статьей. Логично добавить поле tsvector и складывать в него разобранный документ. При этом мы считаем: поскольку сайт русскоязычный, то при разборе документа указываем язык ru.

На практике в статье могут быть цитаты на английском или код. Они будут индексированы русским стеммингом, и это плохо скажется на качестве. Чтобы найти документ по английским словам, нужно ввести их в точности как в статье.

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

Идет текст на русском, а потом <quote lang="en">some text in English</quote>.

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

select * from split_by_lang(post.comment);

┌──────┬──────┬─────────────────────────┐
│ part │ lang │  content                │
├──────┼──────┼─────────────────────────┤
│    1 │ ru   │ Всем привет! ...        │
├──────┼──────┼─────────────────────────┤
│    2 │ en   │ A quote from Wikipedia  │
├──────┼──────┼─────────────────────────┤
│    3 │ ru   │ дальнейший текст        │
├──────┼──────┼─────────────────────────┤
│    4 │ en   │ another quote from wiki │

Каждая часть индексируется языком из колонки lang. Накопленные части объединяются в один документ и записываются в поле tsvector.

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

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

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

Решение о том, что хранить в основной таблице, а что в связанной, часто ситуативно. Нужно знать контекст и условия задачи. Но одно можно сказать точно: объединить данные проще, чем разделить. “Design is about keeping things apart” (c).