Jak możesz reprezentować dziedziczenie w bazie danych?

236

Zastanawiam się, jak reprezentować złożoną strukturę w bazie danych SQL Server.

Rozważ aplikację, która musi przechowywać szczegóły rodziny obiektów, które mają pewne atrybuty, ale wiele innych nie jest wspólnych. Na przykład pakiet ubezpieczeń komercyjnych może obejmować ochronę od odpowiedzialności cywilnej, komunikacyjnej, majątkowej i odszkodowawczej w ramach tego samego zapisu polisy.

Zaimplementowanie tego w C # itp. Jest trywialne, ponieważ możesz utworzyć Politykę z kolekcją Sekcji, w których sekcja jest dziedziczona zgodnie z wymaganiami dla różnych rodzajów ochrony. Jednak relacyjne bazy danych nie pozwalają na to łatwo.

Widzę, że są dwie główne opcje:

  1. Utwórz tabelę zasad, a następnie tabelę sekcji ze wszystkimi wymaganymi polami dla wszystkich możliwych odmian, z których większość byłaby zerowa.

  2. Utwórz tabelę zasad i liczne tabele sekcji, po jednej dla każdego rodzaju ubezpieczenia.

Obie te alternatywy wydają się niezadowalające, zwłaszcza, że ​​konieczne jest pisanie zapytań we wszystkich sekcjach, co wiązałoby się z licznymi sprzężeniami lub licznymi kontrolami zerowymi.

Jaka jest najlepsza praktyka dla tego scenariusza?

Steve Jones
źródło

Odpowiedzi:

430

@Bill Karwin opisuje trzy modele dziedziczenia w swojej książce SQL Antipatterns , proponując rozwiązania antipatternu SQL Entity-Attribute-Value . Oto krótki przegląd:

Dziedziczenie pojedynczej tabeli (alias Table Per Hierarchy Inheritance):

Korzystanie z jednego stołu jak w pierwszej opcji jest prawdopodobnie najprostszym projektem. Jak wspomniałeś, wiele atrybutów specyficznych dla podtypów będzie musiało otrzymać NULLwartość w wierszach, w których te atrybuty nie mają zastosowania. W tym modelu miałbyś jedną tabelę polityk, która wyglądałaby mniej więcej tak:

+------+---------------------+----------+----------------+------------------+
| 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 -----/

Prostota projektu to plus, ale główne problemy z tym podejściem są następujące:

  • Jeśli chodzi o dodawanie nowych podtypów, musiałbyś zmienić tabelę, aby uwzględnić atrybuty opisujące te nowe obiekty. Może to szybko stać się problematyczne, jeśli masz wiele podtypów lub planujesz regularnie dodawać podtypy.

  • Baza danych nie będzie w stanie wymusić, które atrybuty mają zastosowanie, a które nie, ponieważ nie ma metadanych określających, które atrybuty należą do poszczególnych podtypów.

  • Nie można również wymuszać NOT NULLatrybutów podtypu, które powinny być obowiązkowe. Będziesz musiał sobie z tym poradzić w swojej aplikacji, co ogólnie nie jest idealne.

Dziedziczenie stołu betonowego:

Innym podejściem do rozwiązania problemu dziedziczenia jest utworzenie nowej tabeli dla każdego podtypu, powtarzając wszystkie wspólne atrybuty w każdej tabeli. Na przykład:

--// 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    |   
+------+---------------------+------------------+

Ten projekt zasadniczo rozwiąże problemy zidentyfikowane dla metody z pojedynczą tabelą:

  • Obowiązkowe atrybuty można teraz egzekwować NOT NULL .

  • Dodanie nowego podtypu wymaga dodania nowej tabeli zamiast dodawania kolumn do istniejącej.

  • Nie ma również ryzyka, że ​​ustawiony zostanie nieodpowiedni atrybut dla konkretnego podtypu, takiego jak vehicle_reg_nopole dla polityki właściwości.

  • typeAtrybut ten nie jest potrzebny, jak w metodzie pojedynczej tabeli. Typ jest teraz zdefiniowany przez metadane: nazwa tabeli.

Jednak ten model ma również kilka wad:

  • Wspólne atrybuty są mieszane z atrybutami specyficznymi dla podtypu i nie ma łatwego sposobu ich zidentyfikowania. Baza danych też nie będzie wiedziała.

  • Podczas definiowania tabel należy powtórzyć wspólne atrybuty dla każdej tabeli podtypu. To zdecydowanie nie jest SUCHE .

  • Wyszukiwanie wszystkich zasad niezależnie od podtypu staje się trudne i wymagałoby kilku UNIONs.

W ten sposób będziesz musiał wysłać zapytanie do wszystkich zasad niezależnie od typu:

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;

Zauważ, że dodanie nowych podtypów wymagałoby modyfikacji powyższego zapytania o dodatkowe UNION ALLdla każdego podtypu. Może to łatwo prowadzić do błędów w aplikacji, jeśli operacja zostanie zapomniana.

Dziedziczenie tabeli klas (zwanej także dziedziczeniem tabeli według typu):

Jest to rozwiązanie, o którym @David wspomina w innej odpowiedzi . Tworzysz pojedynczą tabelę dla swojej klasy podstawowej, która zawiera wszystkie typowe atrybuty. Następnie utworzyłbyś określone tabele dla każdego podtypu, którego klucz podstawowy służy również jako klucz obcy do tabeli podstawowej. Przykład:

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)
);

To rozwiązanie rozwiązuje problemy zidentyfikowane w dwóch pozostałych projektach:

  • Atrybuty obowiązkowe można egzekwować NOT NULL.

  • Dodanie nowego podtypu wymaga dodania nowej tabeli zamiast dodawania kolumn do istniejącej.

  • Brak ryzyka, że ​​dla określonego podtypu zostanie ustawiony niewłaściwy atrybut.

  • Nie ma potrzeby tego typeatrybutu.

  • Teraz wspólne atrybuty nie są już mieszane z atrybutami specyficznymi dla podtypu.

  • W końcu możemy pozostać SUCHY. Podczas tworzenia tabel nie trzeba powtarzać wspólnych atrybutów dla każdej tabeli podtypu.

  • Zarządzanie automatyczną inkrementacją idzasad staje się łatwiejsze, ponieważ może to być obsługiwane przez tabelę podstawową, zamiast generowania ich niezależnie przez każdą tabelę podtypów.

  • Wyszukiwanie wszystkich polityk niezależnie od podtypu staje się teraz bardzo łatwe: nie UNIONsą potrzebne - wystarczy SELECT * FROM policies.

Podejście oparte na stole klasowym uważam za najbardziej odpowiednie w większości sytuacji.


Nazwy tych trzech modeli pochodzą z książki Martina Fowlera Wzory architektury aplikacji korporacyjnych .

Daniel Vassallo
źródło
97
Ja również używam tego projektu, ale nie wspominasz o wadach. W szczególności: 1) mówisz, że nie potrzebujesz tego typu; prawda, ale nie można zidentyfikować rzeczywistego typu wiersza, dopóki nie spojrzy się na tabele wszystkich podtypów w celu znalezienia dopasowania. 2) Trudno jest zsynchronizować tabelę główną i tabele podtypów (można np. Usunąć wiersz z tabeli podtypów, a nie z tabeli głównej). 3) Możesz mieć więcej niż jeden podtyp dla każdego wiersza głównego. Używam wyzwalaczy do pracy wokół 1, ale 2 i 3 to bardzo trudne problemy. Właściwie 3 nie stanowi problemu, jeśli modelujesz kompozycję, ale jest dla ścisłego dziedziczenia.
19
+1 za komentarz @ Tibo, to poważny problem. Dziedziczenie tabeli klas w rzeczywistości daje nienormalizowany schemat. Gdzie nie ma dziedziczenia konkretnego stołu, a ja nie zgadzam się z argumentem, że dziedziczenie konkretnego stołu utrudnia OSUSZANIE. SQL utrudnia OSUSZANIE, ponieważ nie ma funkcji metaprogramowania. Rozwiązaniem jest użycie zestawu narzędzi bazy danych (lub napisanie własnego) do ciężkiego podnoszenia, zamiast bezpośredniego pisania SQL (pamiętaj, że w rzeczywistości jest to tylko język interfejsu DB). W końcu nie piszesz również aplikacji korporacyjnej w asemblerze.
Jo So
18
@ Tibo, o punkcie 3, możesz zastosować podejście wyjaśnione tutaj: sqlteam.com/article/… , Sprawdź sekcję Modelowanie ograniczeń jeden do jednego .
Andrew
4
@DanielVassallo Po pierwsze dziękuję za oszałamiającą odpowiedź, 1 wątpię, czy dana osoba ma polisę, jak się dowiedzieć, czy jej polis_motor czy polis_property? Jednym ze sposobów jest przeszukiwanie zasad we wszystkich podtabelach, ale myślę, że to zły sposób, prawda? Jakie powinno być właściwe podejście?
ThomasBecker,
11
Naprawdę podoba mi się twoja trzecia opcja. Jestem jednak zdezorientowany, jak będzie działać SELECT. Jeśli wybierzesz * Z zasad, otrzymasz identyfikatory zasad, ale nadal nie będziesz wiedział, do której tabeli podtypów należy zasada. Czy nadal nie będziesz musiał wykonać JOIN ze wszystkimi podtypami, aby uzyskać wszystkie szczegóły polityki?
Adam
14

Trzecią opcją jest utworzenie tabeli „Zasady”, a następnie tabeli „SectionsMain”, która przechowuje wszystkie pola wspólne dla różnych typów sekcji. Następnie utwórz inne tabele dla każdego typu sekcji, które zawierają tylko pola, które nie są wspólne.

Wybór najlepszego zależy głównie od tego, ile pól masz i jak chcesz napisać SQL. Wszyscy by działali. Jeśli masz tylko kilka pól, prawdopodobnie wybrałbym numer 1. Z „dużą ilością pól” skłaniałbym się ku # 2 lub # 3.

David
źródło
Opcja +1: 3. jest najbliższa modelowi dziedziczenia i najbardziej znormalizowana IMO
RedFilter
Twoja opcja nr 3 jest naprawdę tym, co miałem na myśli przez opcję nr 2. Istnieje wiele pól, a niektóre Sekcja również miałyby byty potomne.
Steve Jones
9

Na podstawie dostarczonych informacji modelowałbym bazę danych, aby miała:

POLITYKI

  • POLICY_ID (klucz podstawowy)

ZADŁUŻENIE

  • LIABILITY_ID (klucz podstawowy)
  • POLICY_ID (klucz obcy)

NIERUCHOMOŚCI

  • PROPERTY_ID (klucz podstawowy)
  • POLICY_ID (klucz obcy)

... i tak dalej, ponieważ spodziewałbym się, że z każdą sekcją zasad będą powiązane różne atrybuty. W przeciwnym razie mógłby istnieć pojedynczy SECTIONSstół, a oprócz tego policy_idbyłby section_type_code...

Tak czy inaczej, pozwoli to na obsługę opcjonalnych sekcji według zasad ...

Nie rozumiem, co uważasz za niezadowalające w tym podejściu - w ten sposób przechowujesz dane, zachowując spójność referencyjną i nie powielając danych. Termin jest „znormalizowany” ...

Ponieważ SQL jest oparty na SET, jest raczej obcy pojęciom programowania proceduralnego / OO i wymaga przejścia kodu z jednej dziedziny do drugiej. Często brane są pod uwagę ORM, ale nie działają one dobrze w złożonych systemach o dużej objętości.

Kucyki OMG
źródło
Tak, dostaję kwestię normalizacji ;-) W przypadku tak złożonej struktury, przy czym niektóre sekcje są proste, a niektóre mają własną złożoną podstrukturę, wydaje się mało prawdopodobne, aby ORM działał, chociaż byłoby fajnie.
Steve Jones
6

Ponadto w przypadku rozwiązania Daniel Vassallo, jeśli używasz SQL Server 2016+, istnieje inne rozwiązanie, z którego korzystałem w niektórych przypadkach bez znacznej utraty wydajności.

Możesz utworzyć tylko tabelę zawierającą tylko wspólne pole i dodać jedną kolumnę za pomocą JSON łańcuchem który zawiera wszystkie pola specyficzne dla podtypu.

Przetestowałem ten projekt pod kątem zarządzania dziedziczeniem i bardzo się cieszę z elastyczności, jaką mogę zastosować we względnej aplikacji.

zwycięzca
źródło
1
To ciekawy pomysł. Nie korzystałem jeszcze z JSON w SQL Server, ale używam go dużo gdzie indziej. Dzięki za heads-upy.
Steve Jones
5

Innym sposobem na to jest użycie INHERITSkomponentu. Na przykład:

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);

W ten sposób można zdefiniować dziedziczenie między tabelami.

Marco Paulo Ollivier
źródło
Czy inne bazy danych obsługują INHERITSoprócz PostgreSQL ? MySQL na przykład?
giannis christofakis
1
@giannischristofakis: MySQL jest tylko relacyjną bazą danych, podczas gdy Postgres jest relacyjną bazą danych obiektów. Zatem żaden MySQL nie obsługuje tego. W rzeczywistości uważam, że Postgres jest jedynym aktualnym systemem DBMS, który obsługuje ten typ dziedziczenia.
a_horse_w_no_name
2
@ marco-paulo-ollivier, pytanie OP dotyczy SQL Servera, więc nie rozumiem, dlaczego udostępniasz rozwiązanie, które działa tylko z Postgres. Oczywiście nie rozwiązuje problemu.
mapto
@mapto to pytanie stało się czymś w rodzaju „w jaki sposób dziedziczyć styl OO w bazie danych” cel dupe; że pierwotnie chodziło o serwer SQL, prawdopodobnie teraz nie ma znaczenia
Caius Jard
0

Opieram się na metodzie nr 1 (ujednolicona tabela sekcji), w celu wydajnego pobierania całych polityk wraz ze wszystkimi ich sekcjami (zakładam, że twój system będzie dużo robił).

Ponadto nie wiem, jakiej wersji programu SQL Server używasz, ale w 2008 r. W rzadkich kolumnach pomagają zoptymalizować wydajność w sytuacjach, w których wiele wartości w kolumnie będzie NULL.

Ostatecznie musisz zdecydować, jak „podobne” są sekcje zasad. Chyba, że ​​różnią się znacznie, myślę, że bardziej znormalizowane rozwiązanie może być większym problemem niż jest warte ... ale tylko Ty możesz wykonać to połączenie. :)

Dan J.
źródło
Będzie za dużo informacji, aby przedstawić całą Politykę za jednym razem, więc nigdy nie byłoby konieczne odzyskanie całego rekordu. Myślę, że jest rok 2005, chociaż wykorzystałem rzadkie dane z 2008 roku w innych projektach.
Steve Jones
Skąd pochodzi termin „ujednolicona tabela sekcji”? Google nie pokazuje prawie żadnych wyników, a tu jest już dość mylących warunków.
Stephan-v
-1

Alternatywnie, rozważ użycie baz danych dokumentów (takich jak MongoDB), które natywnie obsługują bogate struktury danych i zagnieżdżanie.

Grigori Melnik
źródło