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

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

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

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

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

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

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

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

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

Ответ 1

@Bill Karwin описывает три модели наследования в своей книге "Антипаттерны SQL ", предлагая решения для антипаттерна SQL Entity-Attribute-Value. Это краткий обзор:

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

Использование одной таблицы, как в первом варианте, вероятно, самый простой дизайн. Как вы упомянули, многим атрибутам, относящимся к подтипу, должно быть присвоено значение 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, как в методе с одной таблицей. Тип теперь определяется метаданными: именем таблицы.

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

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

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

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

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

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 для каждого подтипа. Это может легко привести к ошибкам в вашем приложении, если эта операция будет забыта.

Наследование таблиц классов (aka Table Inheritance):

Это решение, которое @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.

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


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

Ответ 2

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

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

Ответ 3

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

ПОЛИТИКА

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

ОБЯЗАННОСТИ

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

Свойства

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

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

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

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

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

Ответ 4

Другой способ сделать это - использовать компонент 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);

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

Ответ 5

В дополнение к решению Daniel Vassallo, если вы используете SQL Server 2016, есть еще одно решение, которое я использовал в некоторых случаях без значительных потерь производительности.

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

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

Ответ 6

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

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

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