Come si può rappresentare l'eredità in un database?


236

Sto pensando a come rappresentare una struttura complessa in un database di SQL Server.

Considera un'applicazione che deve archiviare i dettagli di una famiglia di oggetti, che condividono alcuni attributi, ma ne hanno molti altri non comuni. Ad esempio, un pacchetto assicurativo commerciale può includere la copertura di responsabilità civile, automobilistica, patrimoniale e d'indennità all'interno dello stesso record di polizza.

È banale implementarlo in C #, ecc., Poiché è possibile creare una politica con una raccolta di sezioni, in cui la sezione viene ereditata come richiesto per i vari tipi di copertura. Tuttavia, i database relazionali non sembrano permetterlo facilmente.

Vedo che ci sono due scelte principali:

  1. Creare una tabella dei criteri, quindi una tabella delle sezioni, con tutti i campi richiesti, per tutte le possibili variazioni, la maggior parte delle quali sarebbe nulla.

  2. Crea una tabella dei criteri e numerose tabelle delle sezioni, una per ogni tipo di copertina.

Entrambe queste alternative sembrano insoddisfacenti, soprattutto perché è necessario scrivere query in tutte le sezioni, che comporterebbero numerosi join o numerosi controlli null.

Qual è la migliore pratica per questo scenario?


Risposte:


430

@Bill Karwin descrive tre modelli di ereditarietà nel suo libro SQL Antipatterns , quando propone soluzioni per l' antipattern SQL Entity-Attribute-Value . Questa è una breve panoramica:

Ereditarietà a tabella singola (aka Table Per Hierarchy Inheritance):

Usare una singola tabella come nella tua prima opzione è probabilmente il design più semplice. Come accennato, a molti attributi specifici del sottotipo dovrà essere assegnato un NULLvalore nelle righe in cui questi attributi non si applicano. Con questo modello, avresti una tabella delle politiche, che sarebbe simile a questa:

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

Mantenere il design semplice è un vantaggio, ma i problemi principali con questo approccio sono i seguenti:

  • Quando si tratta di aggiungere nuovi sottotipi, è necessario modificare la tabella per adattarsi agli attributi che descrivono questi nuovi oggetti. Questo può diventare rapidamente problematico quando si hanno molti sottotipi o se si prevede di aggiungere regolarmente sottotipi.

  • Il database non sarà in grado di imporre quali attributi si applicano e quali no, poiché non esistono metadati per definire quali attributi appartengono a quali sottotipi.

  • Inoltre, non è possibile applicare gli NOT NULLattributi di un sottotipo che dovrebbero essere obbligatori. Dovresti gestirlo nella tua applicazione, che in generale non è l'ideale.

Ereditarietà concreta della tabella:

Un altro approccio per affrontare l'ereditarietà è quello di creare una nuova tabella per ciascun sottotipo, ripetendo tutti gli attributi comuni in ciascuna tabella. Per esempio:

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

Questo progetto risolverà sostanzialmente i problemi identificati per il metodo a tabella singola:

  • Ora è possibile applicare attributi obbligatori NOT NULL.

  • L'aggiunta di un nuovo sottotipo richiede l'aggiunta di una nuova tabella anziché l'aggiunta di colonne a una esistente.

  • Inoltre, non esiste alcun rischio che venga impostato un attributo inappropriato per un particolare sottotipo, ad esempio il vehicle_reg_nocampo per una politica delle proprietà.

  • Non è necessario per l' typeattributo come nel metodo a tabella singola. Il tipo è ora definito dai metadati: il nome della tabella.

Tuttavia, questo modello presenta anche alcuni svantaggi:

  • Gli attributi comuni sono mescolati con gli attributi specifici del sottotipo e non esiste un modo semplice per identificarli. Il database non lo saprà neanche.

  • Quando si definiscono le tabelle, è necessario ripetere gli attributi comuni per ciascuna tabella dei sottotipi. Non è assolutamente ASCIUTTO .

  • La ricerca di tutte le politiche indipendentemente dal sottotipo diventa difficile e richiederebbe un sacco di UNIONs.

Ecco come dovresti interrogare tutti i criteri indipendentemente dal tipo:

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;

Nota come l'aggiunta di nuovi sottotipi richiederebbe la modifica della query sopra con un ulteriore UNION ALLper ciascun sottotipo. Questo può facilmente portare a bug nella tua applicazione se questa operazione viene dimenticata.

Ereditarietà delle tabelle di classe (nota anche come ereditarietà delle tabelle):

Questa è la soluzione menzionata da @David nell'altra risposta . Si crea una singola tabella per la classe di base, che include tutti gli attributi comuni. Quindi creare tabelle specifiche per ciascun sottotipo, la cui chiave primaria funge anche da chiave esterna per la tabella di base. Esempio:

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

Questa soluzione risolve i problemi identificati negli altri due progetti:

  • È possibile applicare attributi obbligatori NOT NULL.

  • L'aggiunta di un nuovo sottotipo richiede l'aggiunta di una nuova tabella anziché l'aggiunta di colonne a una esistente.

  • Nessun rischio che venga impostato un attributo inappropriato per un particolare sottotipo.

  • Non è necessario per l' typeattributo.

  • Ora gli attributi comuni non vengono più mescolati con gli attributi specifici del sottotipo.

  • Finalmente possiamo restare ASCIUTTI. Non è necessario ripetere gli attributi comuni per ciascuna tabella dei sottotipi durante la creazione delle tabelle.

  • La gestione di un incremento automatico idper i criteri diventa più semplice, poiché può essere gestita dalla tabella di base anziché da ciascuna tabella dei sottotipi generandoli in modo indipendente.

  • La ricerca di tutte le politiche indipendentemente dal sottotipo ora diventa molto semplice: non UNIONè necessario, solo a SELECT * FROM policies.

Considero l'approccio del tavolo di classe come il più adatto nella maggior parte delle situazioni.


I nomi di questi tre modelli derivano dal libro Patterns of Enterprise Application Architecture di Martin Fowler .


97
Sto usando anche questo disegno, ma non menzionate gli svantaggi. In particolare: 1) dici di non aver bisogno del tipo; vero ma non è possibile identificare il tipo effettivo di una riga a meno che non si guardino tutte le tabelle dei sottotipi per trovare una corrispondenza. 2) È difficile mantenere sincronizzate la tabella principale e le tabelle dei sottotipi (ad esempio, è possibile rimuovere la riga nella tabella dei sottotipi e non nella tabella principale). 3) Puoi avere più di un sottotipo per ogni riga principale. Uso i trigger per lavorare intorno a 1, ma 2 e 3 sono problemi molto difficili. In realtà 3 non è un problema se si modella la composizione, ma è per eredità rigorosa.

19
+1 per il commento di @ Tibo, questo è un grave problema. L'ereditarietà della tabella di classi produce effettivamente uno schema non normalizzato. Dove l'eredità di Concrete Table non lo è, e non sono d'accordo con l'argomento secondo cui l'ereditarietà di Concrete Table ostacola il DRY. SQL ostacola il DRY, perché non ha funzionalità di metaprogrammazione. La soluzione è quella di utilizzare un Database Toolkit (o scrivere il tuo) per fare il lavoro pesante, invece di scrivere direttamente SQL (ricorda, in realtà è solo un linguaggio di interfaccia DB). Dopotutto, anche tu non scrivi la tua applicazione aziendale in assembly.
Jo So,

18
@Tibo, a proposito del punto 3, è possibile utilizzare l'approccio spiegato qui: sqlteam.com/article/… , controllare la sezione Vincoli di modellazione da uno a uno .
Andrew

4
@DanielVassallo In primo luogo grazie per la straordinaria risposta, 1 dubbio se una persona ha una politicaId come sapere se la sua politica_motore o policy_property? Un modo è quello di cercare policyId in tutte le tabelle secondarie ma immagino che questo sia il brutto modo non è vero, quale dovrebbe essere l'approccio corretto?
Thomas Becker

11
Mi piace molto la tua terza opzione. Tuttavia, sono confuso su come funzionerà SELECT. Se SELEZIONA * DA criteri, otterrai gli ID della politica ma non saprai ancora a quale tabella dei sottotipi appartiene la politica. Non dovrai ancora fare un JOIN con tutti i sottotipi per ottenere tutti i dettagli della politica?
Adam

14

La terza opzione è quella di creare una tabella "Policy", quindi una tabella "SectionsMain" che memorizza tutti i campi comuni tra i tipi di sezioni. Quindi creare altre tabelle per ogni tipo di sezione che contenga solo i campi non comuni.

La decisione su quale sia la migliore dipende principalmente da quanti campi hai e da come vuoi scrivere il tuo SQL. Funzionerebbero tutti. Se hai solo alcuni campi, probabilmente andrei con il n. 1. Con "un sacco" di campi mi spingerei verso il n. 2 o il n. 3.


+1: la terza opzione è la più vicina al modello di ereditarietà e l'IMO più normalizzata
RedFilter,

La tua opzione n. 3 è proprio ciò che intendevo per opzione n. 2. Esistono molti campi e alcune sezioni avrebbero anche entità figlio.
Steve Jones,

9

Con le informazioni fornite, modellerei il database per avere quanto segue:

POLITICHE

  • POLICY_ID (chiave primaria)

PASSIVO

  • LIABILITY_ID (chiave primaria)
  • POLICY_ID (chiave esterna)

PROPRIETÀ

  • PROPERTY_ID (chiave primaria)
  • POLICY_ID (chiave esterna)

... e così via, perché mi aspetto che ci siano diversi attributi associati a ciascuna sezione della politica. Altrimenti, potrebbe esserci un solo SECTIONStavolo e oltre a policy_id, ci sarebbe un section_type_code...

In entrambi i casi, ciò consentirebbe di supportare sezioni opzionali per politica ...

Non capisco cosa trovi insoddisfacente di questo approccio: ecco come archiviare i dati mantenendo l'integrità referenziale e non duplicando i dati. Il termine è "normalizzato" ...

Poiché SQL è basato su SET, è piuttosto estraneo ai concetti di programmazione procedurale / OO e richiede che il codice passi da un regno all'altro. Gli ORM sono spesso considerati, ma non funzionano bene in sistemi complessi ad alto volume.


Sì, ho la cosa della normalizzazione ;-) Per una struttura così complessa, con alcune sezioni semplici e altre con una sottostruttura complessa, sembra improbabile che un ORM funzioni, anche se sarebbe bello.
Steve Jones,

6

Oltre alla soluzione Daniel Vassallo, se si utilizza SQL Server 2016+, esiste un'altra soluzione che ho usato in alcuni casi senza perdere considerevolmente le prestazioni.

È possibile creare solo una tabella con solo il campo comune e aggiungere una singola colonna con la stringa JSON che contenga tutti i campi specifici del sottotipo.

Ho testato questo progetto per gestire l'ereditarietà e sono molto felice per la flessibilità che posso usare nella relativa applicazione.


1
Questa è un'idea interessante. Non ho ancora usato JSON in SQL Server, ma lo uso molto altrove. Grazie per il testa a testa.
Steve Jones,

5

Un altro modo per farlo è usare il INHERITScomponente. Per esempio:

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

Pertanto è possibile definire un'eredità tra le tabelle.


Altri DB supportano INHERITSoltre a PostgreSQL ? MySQL per esempio?
giannis christofakis,

1
@giannischristofakis: MySQL è solo un database relazionale, mentre Postgres è un database relazionale ad oggetti. Quindi, nessun MySQL non supporta questo. In effetti, penso che Postgres sia l'unico DBMS attuale che supporta questo tipo di eredità.
a_horse_with_no_name

2
@ marco-paulo-ollivier, la domanda dell'OP riguarda SQL Server, quindi non capisco perché offri una soluzione che funziona solo con Postgres. Ovviamente, non affrontare il problema.
mapto

@mapto questa domanda è diventata una sorta di target dupe "come si fa a ereditare lo stile OO in un database"; che era originariamente su SQL Server è probabilmente ora irrilevante
Caius Jard

0

Mi sposto verso il metodo n. 1 (una tabella di sezione unificata), al fine di recuperare in modo efficiente intere politiche con tutte le loro sezioni (che presumo che il tuo sistema farà molto).

Inoltre, non so quale versione di SQL Server stai utilizzando, ma nel 2008+ Colonne sparse aiutano a ottimizzare le prestazioni in situazioni in cui molti dei valori in una colonna saranno NULL.

Alla fine, dovrai decidere quanto "simili" sono le sezioni della politica. A meno che non differiscano sostanzialmente, penso che una soluzione più normalizzata potrebbe essere più un problema di quanto valga ... ma solo tu puoi fare quella chiamata. :)


Ci saranno troppe informazioni per presentare l'intera Politica in una sola volta, quindi non sarebbe mai necessario recuperare l'intero record. Penso che sia il 2005, anche se ho usato lo scarso 2008 in altri progetti.
Steve Jones,

Da dove viene il termine "tabella di sezione unificata"? Google non mostra quasi risultati per questo e ci sono già abbastanza termini confusi qui.
Stephan-v

-1

In alternativa, prendere in considerazione l'utilizzo di un database di documenti (come MongoDB) che supporta nativamente strutture di dati e annidamento avanzati.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.