📄

Три способа подружить наследование классов и реляционные таблицы

22.04.2026 DataBas ?>

Объектно-ориентированные языки поддерживают наследование классов напрямую. В реляционных базах данных такого встроенного механизма нет. Разработчикам приходится самостоятельно решать, как отобразить иерархию объектов на плоские таблицы. Для этого придуманы три основных паттерна:

  • Single Table Inheritance (STI) — всё лежит в одной общей таблице;
  • Class Table Inheritance (CTI) — под каждый класс создаётся своя таблица;
  • Concrete Table Inheritance (TPC) — отдельная таблица для каждого конкретного класса, дублирующая все унаследованные поля.

Каждый подход — это компромисс между простотой реализации, скоростью чтения и записи, а также строгостью соблюдения нормальных форм. Разберём их по порядку.

Single Table Inheritance (STI) — Одна таблица для всех

Как это работает. Все атрибуты — и из базового класса, и из всех наследников — собираются в одной таблице. Чтобы различать, к какому именно типу относится строка, добавляют столбец-дискриминатор (например, type). При вставке объекта конкретного подкласса заполняются только те колонки, которые имеют для него смысл. Остальные остаются пустыми (NULL).

Плюсы. У паттерна два главных достоинства:

  • Простота поддержки схемы. Добавление нового подкласса или общего атрибута — это изменение всего одной таблицы. Миграции проходят легко.
  • Высокая скорость чтения. Вся информация об объекте любого типа лежит в одной строке. Запросу не нужны соединения (JOIN), оптимизатор СУБД отработает быстро.

Минусы. За простоту приходится платить:

  • Много NULL‑значений. Таблица становится «разреженной»: поля, специфичные для грузовиков, пустуют у автобусов, и наоборот. Дисковое пространство используется неэффективно.
  • Слабая целостность данных на уровне БД. Нельзя, например, объявить NOT NULL для поля cargo_capacity, потому что для автобусов оно должно оставаться NULL. Ограничения вроде «грузоподъёмность обязательна только для грузовиков» приходится реализовывать в коде приложения. Это повышает риск ошибок.
  • Технические лимиты. У СУБД есть предельное количество столбцов в таблице. При очень широкой иерархии можно упереться в это ограничение.

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

Факт: некоторые компании (например, GitLab) рекомендуют избегать STI в новых проектах из-за долгосрочных проблем с сопровождаемостью.

Class Table Inheritance (CTI) — Таблица для каждого класса

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

Плюсы. Этот подход ближе всего к академическим принципам проектирования баз данных:

  • Высокая целостность. Для каждой таблицы можно задать полноценные ограничения NOT NULLCHECK и внешние ключи. Например, поле passenger_count будет обязательно заполнено только у автобусов.
  • Эффективное хранение. NULL‑значения сводятся к минимуму. Схема обычно удовлетворяет нормальной форме Бойса — Кодда (BCNF), усиленной версии третьей нормальной формы.
  • Удобное расширение. Добавление нового подкласса требует лишь создания ещё одной таблицы с внешним ключом. Общий атрибут меняется только в родительской таблице.

Минусы. За нормализацию тоже приходится платить:

  • Чтение требует соединений. Чтобы собрать полный объект, нужно выполнить JOIN как минимум двух таблиц. При полиморфном запросе (выборке объектов разных типов) количество соединений растёт. На практике современные оптимизаторы и индексы справляются с этим хорошо, но на очень больших объёмах или при плохом проектировании индексов скорость может упасть.
  • Более дорогая запись. При вставке нового объекта приходится обновлять несколько таблиц в одной транзакции. Это означает, как минимум, две записи в журнал предзаписи (WAL), что создаёт дополнительную нагрузку на подсистему ввода-вывода. В высоконагруженных системах с огромным потоком вставок это может стать узким местом.

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

Concrete Table Inheritance (TPC) — Таблица для каждого конкретного класса

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

Плюс. Скорость чтения такая же высокая, как у STI: вся информация об объекте лежит в одной строке без каких‑либо соединений.

Минусы. Недостатки серьёзные:

  • Дублирование общих полей. Атрибуты вроде brand и model копируются в каждую таблицу‑потомок. Это увеличивает занимаемое место и усложняет хранение.
  • Очень сложная поддержка схемы. Добавление одного общего атрибута требует изменения структуры всех таблиц конкретных классов. Миграция становится трудоёмкой и чревата ошибками.
  • Нет единого источника истины для общих полей. Если потребуется массово обновить значение общего атрибута для объектов разных типов, придётся менять данные в нескольких таблицах — есть риск расхождений.

Когда применять. Иерархия классов абсолютно стабильна и никогда не будет меняться; полиморфные запросы практически не используются; операции с объектами конкретных типов должны выполняться максимально быстро.

Примеры всех трёх схем

Представим базовый класс Vehicle и два подкласса: Truck и Bus. Вот как будут выглядеть таблицы в каждом паттерне.

STI — всё в одной таблице vehicles:

idtypebrandmodelcargo_capacitypassenger_count
1TruckMANTGS20.5NULL
2BusPAZ3205NULL42

CTI — три таблицы с разделением общих и специфичных полей:

vehicles (базовая):

idbrandmodel
1MANTGS
2PAZ3205

trucks (внешний ключ id → vehicles.id):

idcargo_capacity
120.5

buses:

idpassenger_count
242

TPC — две полностью независимые таблицы:

trucks:

idbrandmodelcargo_capacity
1MANTGS20.5

buses:

idbrandmodelpassenger_count
2PAZ320542

Короткие советы по выбору

  • Выбирайте STI, если иерархия маленькая и стабильная, производительность чтения критична, а строгая нормализация не важна. Но помните о долгосрочных рисках сопровождения.
  • Выбирайте CTI, если строите сложную, расширяемую систему, для которой целостность данных и минимум NULL — приоритет. Вы готовы мириться с соединениями и чуть более дорогой записью.
  • Рассматривайте TPC, только когда нужна скорость STI, а недостатки STI неприемлемы, но при этом классы сильно различаются, иерархия никогда не меняется и полиморфные запросы не используются.
  • Анализируйте свои запросы. Если в приложении преобладают полиморфные выборки (SELECT * FROM vehicles), CTI с множеством JOIN может действительно оказаться медленнее. Если вы чаще работаете с конкретными типами (SELECT * FROM trucks), CTI или TPC покажут себя лучше.
  • Учитывайте поддержку вашего ORM. Hibernate, Doctrine, ActiveRecord и другие фреймворки имеют встроенные средства для работы с этими паттернами. Проверьте документацию: иногда возможности или ограничения ORM предопределяют выбор.

Архив публикаций