Як можна представити спадщину в базі даних?


236

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

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

Це реально реалізувати в C # та ін., Оскільки ви можете створити Політику з колекції розділів, де розділ успадковується відповідно до вимог для різних типів обкладинки. Однак реляційні бази даних, здається, не дозволяють цього легко.

Я бачу, що є два основні варіанти:

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

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

Обидві ці альтернативи здаються незадовільними, тим більше, що потрібно писати запити в усіх розділах, які б передбачали численні приєднання або численні нульові перевірки.

Яка найкраща практика для цього сценарію?


Відповіді:


430

@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атрибуті, як у методі єдиної таблиці. Тип тепер визначається метаданими: ім'ям таблиці.

Однак ця модель також має ряд недоліків:

  • Загальні атрибути змішуються з атрибутами підтипу, і не існує простого способу їх ідентифікації. База даних також не буде знати.

  • Визначаючи таблиці, вам доведеться повторити загальні атрибути для кожної таблиці підтипів. Це напевно НЕ СУХО .

  • Пошук усіх політик незалежно від підтипу стає складним, і знадобиться купа 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потрібно - просто a SELECT * FROM policies.

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


Назви цих трьох моделей походять з книги Мартіна Фаулера " Шаблони архітектури прикладних програм підприємства" .


97
Я також використовую цей дизайн, але недоліки ви не згадуєте. Зокрема: 1) ви говорите, що вам не потрібен тип; true, але ви не можете ідентифікувати фактичний тип рядка, якщо не переглянути всі таблиці підтипів, щоб знайти відповідність. 2) Важко тримати головну таблицю та таблиці підтипів синхронізовано (можна, наприклад, видалити рядок у таблиці підтипів, а не в головній таблиці). 3) Ви можете мати більше одного підтипу для кожного головного ряду. Я використовую тригери, щоб обійтися навколо 1, але 2 і 3 - дуже важкі проблеми. Насправді 3 не є проблемою, якщо ти моделюєш композицію, але для суворого успадкування.

19
+1 для коментаря @ Tibo, це серйозна проблема. Наслідування таблиці класів фактично дає ненормалізовану схему. Де наслідування конкретної таблиці не відповідає, і я не згоден з аргументом, що спадкування конкретної таблиці перешкоджає СУХОМУ. SQL перешкоджає DRY, оскільки він не має засобів метапрограмування. Рішення полягає в тому, щоб використовувати інструментарій баз даних (або написати свій власний), щоб зробити важкий підйом, замість того, щоб писати SQL безпосередньо (пам'ятайте, що це фактично лише мова інтерфейсу БД). Зрештою, ви також не пишете свою корпоративну заявку на зборах.
Jo So

18
@Tibo, про пункт 3, ви можете використовувати підхід, пояснений тут: sqlteam.com/article/… , Перевірте розділ « Моделювання обмежень один на один» .
Андрій

4
@DanielVassallo По-перше, дякую за приголомшливу відповідь, 1 сумнів, якщо людина має політику, як знати, чи є її policy_motor чи policy_property? Один із способів - це пошук policyId у всіх підтаблицях, але, мабуть, це поганий спосіб, чи не так? Яким повинен бути правильний підхід?
ThomasBecker

11
Мені дуже подобається ваш третій варіант. Однак я переплутана, як працюватиме SELECT. Якщо ви виберете * ІЗ політики, ви отримаєте ідентифікатори політики, але ви все одно не будете знати, до якої таблиці підтипів належить політика. Чи все-таки вам не доведеться робити ПРИЄДНАННЯ з усіма підтипами, щоб отримати всі деталі політики?
Адам

14

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

Вибір найкращого варіанту залежить в основному від того, скільки у вас є полів і як ви хочете написати свій SQL. Вони б усі працювали. Якщо у вас є лише кілька полів, я, мабуть, пішов би з №1. З "лотами" полів я б схилявся до №2 або №3.


+1: 3-й варіант є найближчим до моделі успадкування, і найбільш нормалізований IMO
RedFilter

Ваш варіант №3 - це саме те, що я мав на увазі під варіантом №2. Є багато полів, і в деяких розділах також є дочірні сутності.
Стів Джонс

9

З наданою інформацією я б моделював базу даних таким чином:

ПОЛІТИКА

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

ВІДПОВІДАЛЬНІСТЬ

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

ВЛАСТИВОСТІ

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

... і так далі, тому що я б очікував, що в кожному розділі політики пов’язані різні атрибути. Інакше може бути одна SECTIONSтаблиця, і крім того policy_id, там буде section_type_code...

У будь-якому випадку це дозволить вам підтримувати необов'язкові розділи за політикою ...

Я не розумію, що ви вважаєте незадовільним у цьому підході - саме так ви зберігаєте дані, зберігаючи референтну цілісність і не дублюючи дані. Термін "нормалізується" ...

Оскільки SQL заснований на SET, він досить чужий для процедурних концепцій / програм програмування OO та вимагає переходу коду з однієї сфери в іншу. ОРМ часто вважають, але вони не працюють добре у складних системах з високим обсягом.


Так, я розумію, нормалізація ;-) Для такої складної структури, де деякі розділи є простими, а деякі мають власну складну підструктуру, малоймовірно, що ОРМ спрацює, хоча це було б добре.
Стів Джонс

6

Крім того, у рішенні Даніеля Вассалло, якщо ви використовуєте 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 ?
giannis christofakis

1
@giannischristofakis: MySQL - це лише реляційна база даних, тоді як Postgres - об'єктно-реляційна база даних. Отже, жоден MySQL цього не підтримує. Насправді я думаю, що Postgres - це єдина поточна СУБД, яка підтримує цей тип успадкування.
a_horse_with_no_name

2
@ marco-paulo-ollivier, питання ОП стосується SQL Server, тому я не розумію, чому ви надаєте рішення, яке працює лише з Postgres. Очевидно, не вирішення проблеми.
карта

@mapto це питання стало чимось "цільовим способом успадкування стилю OO у базі даних"; що це спочатку про сервер sql, мабуть, зараз не має значення
Caius Jard

0

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

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

Зрештою, вам доведеться вирішити, наскільки "схожі" розділи політики. Якщо вони суттєво не відрізняються, я думаю, що більш нормоване рішення може мати більше проблем, ніж це варте ... але тільки ви можете зробити цей дзвінок. :)


Тут буде занадто багато інформації, щоб представити всю Політику за один раз, тому ніколи не доведеться отримувати весь запис. Я думаю, що це 2005 рік, хоча я використовував рідкісний 2008 рік в інших проектах.
Стів Джонс

Звідки походить термін "уніфікована таблиця розділів"? Google майже не показує результатів для цього, і тут вже досить заплутаних термінів.
Stephan-v

-1

Крім того, рекомендуємо використовувати бази даних документів (наприклад, MongoDB), які в основному підтримують багаті структури даних та вкладення.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.