Как вы можете представить наследование в базе данных?

236

Я думаю о том, как представить сложную структуру в базе данных SQL Server.

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

Это легко реализовать в C # и т. Д., Так как вы можете создать Политику с коллекцией Разделов, где Раздел наследуется, как требуется для различных типов покрытия. Тем не менее, реляционные базы данных, кажется, не позволяют это легко.

Я вижу, что есть два основных варианта:

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

  2. Создайте таблицу правил и многочисленные таблицы разделов, по одному для каждого вида покрытия.

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

Какова лучшая практика для этого сценария?

Стив Джонс
источник

Ответы:

430

@ Билл Карвин описывает три модели наследования в своей книге « Антипаттерны SQL» , предлагая решения для антипаттерна « Значение сущности SQL» . Это краткий обзор:

Наследование в одной таблице (или Наследование таблиц в иерархии):

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

+------+---------------------+----------+----------------+------------------+
| id   | date_issued         | type     | vehicle_reg_no | property_address |
+------+---------------------+----------+----------------+------------------+
|    1 | 2010-08-20 12:00:00 | MOTOR    | 01-A-04004     | NULL             |
|    2 | 2010-08-20 13:00:00 | MOTOR    | 02-B-01010     | NULL             |
|    3 | 2010-08-20 14:00:00 | PROPERTY | NULL           | Oxford Street    |
|    4 | 2010-08-20 15:00:00 | MOTOR    | 03-C-02020     | NULL             |
+------+---------------------+----------+----------------+------------------+

\------ COMMON FIELDS -------/          \----- SUBTYPE SPECIFIC FIELDS -----/

Простота дизайна - это плюс, но основные проблемы с этим подходом заключаются в следующем:

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

  • База данных не сможет принудительно определить, какие атрибуты применяются, а какие нет, поскольку нет метаданных, которые бы определяли, какие атрибуты принадлежат каким подтипам.

  • Вы также не можете применять NOT NULLатрибуты подтипа, которые должны быть обязательными. Вам придется справиться с этим в вашем приложении, что в целом не идеально.

Наследование бетонного стола:

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

--// Table: policies_motor
+------+---------------------+----------------+
| id   | date_issued         | vehicle_reg_no |
+------+---------------------+----------------+
|    1 | 2010-08-20 12:00:00 | 01-A-04004     |
|    2 | 2010-08-20 13:00:00 | 02-B-01010     |
|    3 | 2010-08-20 15:00:00 | 03-C-02020     |
+------+---------------------+----------------+
                          
--// Table: policies_property    
+------+---------------------+------------------+
| id   | date_issued         | property_address |
+------+---------------------+------------------+
|    1 | 2010-08-20 14:00:00 | Oxford Street    |   
+------+---------------------+------------------+

Этот дизайн в основном решит проблемы, определенные для метода единой таблицы:

  • Обязательные атрибуты теперь могут быть применены с помощью NOT NULL.

  • Добавление нового подтипа требует добавления новой таблицы вместо добавления столбцов к существующей.

  • Также нет риска, что для определенного подтипа будет установлен неподходящий атрибут, такой как vehicle_reg_noполе для политики свойств.

  • typeАтрибут не нужен, как в методе с одной таблицей. Тип теперь определяется метаданными: имя таблицы.

Однако эта модель также имеет несколько недостатков:

  • Общие атрибуты смешиваются с определенными атрибутами подтипа, и нет простого способа их идентифицировать. База данных тоже не будет знать.

  • При определении таблиц вам придется повторять общие атрибуты для каждой таблицы подтипов. Это определенно не СУХОЙ .

  • Поиск всех политик, независимо от подтипа, становится трудным и потребует кучу UNIONs.

Вот как вам нужно будет запрашивать все политики независимо от типа:

SELECT     date_issued, other_common_fields, 'MOTOR' AS type
FROM       policies_motor
UNION ALL
SELECT     date_issued, other_common_fields, 'PROPERTY' AS type
FROM       policies_property;

Обратите внимание, что добавление новых подтипов потребовало бы изменения вышеуказанного запроса с дополнительным UNION ALLдля каждого подтипа. Это может легко привести к ошибкам в вашем приложении, если эта операция будет забыта.

Наследование таблиц классов (или таблица наследования типов):

Это решение, которое @David упоминает в другом ответе . Вы создаете единую таблицу для своего базового класса, которая включает в себя все общие атрибуты. Затем вы должны создать конкретные таблицы для каждого подтипа, первичный ключ которого также служит внешним ключом для базовой таблицы. Пример:

CREATE TABLE policies (
   policy_id          int,
   date_issued        datetime,

   -- // other common attributes ...
);

CREATE TABLE policy_motor (
    policy_id         int,
    vehicle_reg_no    varchar(20),

   -- // other attributes specific to motor insurance ...

   FOREIGN KEY (policy_id) REFERENCES policies (policy_id)
);

CREATE TABLE policy_property (
    policy_id         int,
    property_address  varchar(20),

   -- // other attributes specific to property insurance ...

   FOREIGN KEY (policy_id) REFERENCES policies (policy_id)
);

Это решение решает проблемы, выявленные в двух других проектах:

  • Обязательные атрибуты могут быть применены с NOT NULL.

  • Добавление нового подтипа требует добавления новой таблицы вместо добавления столбцов к существующей.

  • Нет риска, что для определенного подтипа установлен неподходящий атрибут.

  • Нет необходимости в typeатрибуте.

  • Теперь общие атрибуты больше не смешиваются с определенными атрибутами подтипа.

  • Мы можем остаться сухими, наконец. При создании таблиц нет необходимости повторять общие атрибуты для каждой таблицы подтипов.

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

  • Поиск всех политик, независимо от подтипа, теперь становится очень простым: нет UNIONнеобходимости - просто а SELECT * FROM policies.

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


Названия этих трех моделей взяты из книги Мартина Фаулера « Шаблоны архитектуры корпоративных приложений» .

Даниэль Вассалло
источник
97
Я тоже использую этот дизайн, но вы не упоминаете о недостатках. В частности: 1) вы говорите, что вам не нужен тип; Значение true, но вы не можете определить фактический тип строки, если не посмотрите на все таблицы подтипов, чтобы найти соответствие. 2) Трудно синхронизировать основную таблицу и таблицы подтипов (например, можно удалить строку в таблице подтипов, а не в основной таблице). 3) Вы можете иметь более одного подтипа для каждой основной строки. Я использую триггеры, чтобы обойти 1, но 2 и 3 - очень сложные проблемы. На самом деле 3 не проблема, если вы моделируете композицию, но для строгого наследования.
19
+1 за комментарий @ Тибо, это серьезная проблема. Наследование таблиц классов фактически приводит к ненормализованной схеме. Где, как наследование бетонной таблицы не, и я не согласен с аргументом, что наследование бетонной таблицы мешает DRY. SQL мешает DRY, потому что у него нет средств метапрограммирования. Решение состоит в том, чтобы использовать Database Toolkit (или написать свой собственный) для выполнения тяжелой работы, вместо того, чтобы писать SQL напрямую (помните, что на самом деле это только язык интерфейса БД). В конце концов, вы также не пишете свое корпоративное приложение в сборке.
Джо Со
18
@ Tibo, о пункте 3, вы можете использовать подход, описанный здесь: sqlteam.com/article/… , проверьте раздел Моделирование однозначных ограничений .
Эндрю
4
@DanielVassallo Во-первых, спасибо за потрясающий ответ, 1 сомнение, если у человека есть политика. Как узнать, является ли его policy_motor или policy_property? Одним из способов является поиск policyId во всех под-таблицах, но я думаю, что это плохой путь, не так ли? Какой должен быть правильный подход?
ThomasBecker
11
Мне очень нравится ваш третий вариант. Тем не менее, я запутался, как SELECT будет работать. Если вы выберете * FROM policy, вы получите идентификаторы политики, но вы все равно не будете знать, к какой таблице подтипов относится политика. Неужели вам еще не нужно присоединиться ко всем подтипам, чтобы получить все детали политики?
Адам
14

Третий вариант - создать таблицу «Policy», а затем таблицу «SectionsMain», в которой хранятся все поля, общие для всех типов разделов. Затем создайте другие таблицы для каждого типа раздела, которые содержат только поля, которые не являются общими.

Выбор наилучшего зависит в основном от того, сколько полей у вас есть и как вы хотите написать свой SQL. Они все будут работать. Если у вас есть только несколько полей, то я бы, вероятно, пошел с # 1. С «множеством» полей я бы склонялся к # 2 или # 3.

Дэвид
источник
+1: 3-й вариант является наиболее близким к модели наследования и наиболее нормализованным ИМО
RedFilter
Ваш вариант № 3 действительно именно то, что я имел в виду под вариантом № 2. Существует много полей, и в некоторых разделах также будут дочерние объекты.
Стив Джонс
9

Учитывая предоставленную информацию, я бы смоделировал базу данных следующим образом:

ПОЛИТИКА

  • POLICY_ID (первичный ключ)

ОБЯЗАННОСТИ

  • LIABILITY_ID (первичный ключ)
  • POLICY_ID (внешний ключ)

СВОЙСТВА

  • PROPERTY_ID (первичный ключ)
  • POLICY_ID (внешний ключ)

... и так далее, потому что я ожидаю, что будут разные атрибуты, связанные с каждым разделом политики. В противном случае, не может быть один SECTIONSстол и в дополнение к policy_id, там был бы section_type_code...

В любом случае, это позволит вам поддерживать дополнительные разделы для каждой политики ...

Я не понимаю, что вы считаете неудовлетворительным в этом подходе - это то, как вы храните данные, сохраняя ссылочную целостность и не дублируя данные. Термин "нормализован" ...

Поскольку SQL основан на SET, он довольно чужд понятиям процедурного / OO-программирования и требует, чтобы код переходил из одной области в другую. ORM часто рассматриваются, но они плохо работают в больших объемах и сложных системах.

OMG пони
источник
Да, у меня есть нормализация ;-) Для такой сложной структуры, когда некоторые разделы просты, а некоторые имеют собственную сложную подструктуру, маловероятно, что ORM будет работать, хотя это было бы неплохо.
Стив Джонс
6

Кроме того, в решении Daniel Vassallo, если вы используете SQL Server 2016+, есть другое решение, которое я использовал в некоторых случаях без значительной потери производительности.

Вы можете создать только таблицу только с общим полем и добавить один столбец со строкой JSON, которая содержит все специфичные для подтипа поля.

Я протестировал этот дизайн для управления наследованием, и я очень рад за гибкость, которую я могу использовать в соответствующем приложении.

преодолевающим
источник
1
Это интересная идея. Я еще не использовал JSON в SQL Server, но использую его в других местах. Спасибо за внимание.
Стив Джонс
5

Другой способ сделать это, используя INHERITSкомпонент. Например:

CREATE TABLE person (
    id int ,
    name varchar(20),
    CONSTRAINT pessoa_pkey PRIMARY KEY (id)
);

CREATE TABLE natural_person (
    social_security_number varchar(11),
    CONSTRAINT pessoaf_pkey PRIMARY KEY (id)
) INHERITS (person);


CREATE TABLE juridical_person (
    tin_number varchar(14),
    CONSTRAINT pessoaj_pkey PRIMARY KEY (id)
) INHERITS (person);

Таким образом, можно определить наследование между таблицами.

Марко Пауло Олливье
источник
Поддерживает ли другие БД INHERITSпомимо PostgreSQL ? MySQL например?
Джаннис Кристофакис
1
@giannischristofakis: MySQL - это только реляционная база данных, тогда как Postgres - это объектно-реляционная база данных. Итак, ни один MySQL не поддерживает это. Фактически, я думаю, что Postgres - единственная текущая СУБД, которая поддерживает этот тип наследования.
a_horse_with_no_name
2
@ marco-paulo-ollivier, вопрос OP касается SQL Server, поэтому я не понимаю, почему вы предлагаете решение, которое работает только с Postgres. Очевидно, не решается проблема.
карта на
@mapto этот вопрос стал чем-то вроде «как сделать наследование стилей ОО в базе данных»; то, что это было первоначально о сервере sql, вероятно, теперь не имеет значения
Caius Jard
0

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

Кроме того, я не знаю, какую версию SQL Server вы используете, но в 2008+ Sparse Columns помогают оптимизировать производительность в ситуациях, когда многие значения в столбце будут равны NULL.

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

Дэн Дж
источник
Будет слишком много информации, чтобы представить всю Политику за один раз, поэтому никогда не потребуется извлекать всю запись. Я думаю, что это 2005 год, хотя я использовал редкость 2008 года в других проектах.
Стив Джонс
Откуда происходит термин «унифицированная таблица разделов»? Google почти ничего не показывает, и здесь уже достаточно запутанных терминов.
Стефан-v
-1

В качестве альтернативы рассмотрите возможность использования баз данных документов (таких как MongoDB), которые изначально поддерживают богатые структуры данных и вложенность.

Григорий Мельник
источник