Comment pouvez-vous représenter l'héritage dans une base de données?


236

Je réfléchis à la façon de représenter une structure complexe dans une base de données SQL Server.

Considérez une application qui doit stocker les détails d'une famille d'objets, qui partagent certains attributs, mais dont beaucoup d'autres ne sont pas communs. Par exemple, un paquet d'assurance commerciale peut inclure une couverture de responsabilité, automobile, propriété et indemnité dans le même dossier de police.

Il est trivial de l'implémenter en C #, etc., car vous pouvez créer une politique avec une collection de sections, où la section est héritée selon les besoins pour les différents types de couverture. Cependant, les bases de données relationnelles ne semblent pas permettre cela facilement.

Je peux voir qu'il y a deux choix principaux:

  1. Créez une table Policy, puis une table Sections, avec tous les champs requis, pour toutes les variantes possibles, dont la plupart seraient nulles.

  2. Créez un tableau des politiques et de nombreux tableaux de sections, un pour chaque type de couverture.

Ces deux alternatives semblent insatisfaisantes, d'autant plus qu'il est nécessaire d'écrire des requêtes dans toutes les sections, ce qui impliquerait de nombreuses jointures ou de nombreuses vérifications nulles.

Quelle est la meilleure pratique pour ce scénario?


Réponses:


430

@Bill Karwin décrit trois modèles d'héritage dans son livre SQL Antipatterns , lorsqu'il propose des solutions à l' antipattern SQL Entity-Attribute-Value . Voici un bref aperçu:

Héritage de table unique (alias héritage de table par hiérarchie):

L'utilisation d'une seule table comme dans votre première option est probablement la conception la plus simple. Comme vous l'avez mentionné, de nombreux attributs spécifiques au sous-type devront recevoir une NULLvaleur sur les lignes où ces attributs ne s'appliquent pas. Avec ce modèle, vous auriez une table de stratégies, qui ressemblerait à ceci:

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

Garder la conception simple est un plus, mais les principaux problèmes de cette approche sont les suivants:

  • En ce qui concerne l'ajout de nouveaux sous-types, vous devez modifier le tableau pour tenir compte des attributs qui décrivent ces nouveaux objets. Cela peut rapidement devenir problématique lorsque vous disposez de nombreux sous-types ou si vous prévoyez d'ajouter régulièrement des sous-types.

  • La base de données ne pourra pas appliquer les attributs qui s'appliquent et ceux qui ne le sont pas, car il n'y a pas de métadonnées pour définir quels attributs appartiennent à quels sous-types.

  • Vous ne pouvez pas non plus appliquer NOT NULLles attributs d'un sous-type qui devraient être obligatoires. Vous devrez gérer cela dans votre application, ce qui n'est généralement pas idéal.

Héritage de table en béton:

Une autre approche pour lutter contre l'héritage consiste à créer une nouvelle table pour chaque sous-type, en répétant tous les attributs communs de chaque table. Par exemple:

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

Cette conception résoudra essentiellement les problèmes identifiés pour la méthode à table unique:

  • Les attributs obligatoires peuvent désormais être appliqués avec NOT NULL.

  • L'ajout d'un nouveau sous-type nécessite l'ajout d'une nouvelle table au lieu d'ajouter des colonnes à une existante.

  • Il n'y a également aucun risque qu'un attribut inapproprié soit défini pour un sous-type particulier, tel que le vehicle_reg_nochamp d'une stratégie de propriété.

  • Il n'est pas nécessaire pour l' typeattribut comme dans la méthode de table unique. Le type est désormais défini par les métadonnées: le nom de la table.

Cependant, ce modèle présente également quelques inconvénients:

  • Les attributs communs sont mélangés avec les attributs spécifiques au sous-type, et il n'existe aucun moyen facile de les identifier. La base de données ne le saura pas non plus.

  • Lors de la définition des tables, vous devrez répéter les attributs communs pour chaque table de sous-type. Ce n'est certainement pas SEC .

  • La recherche de toutes les politiques, quel que soit le sous-type, devient difficile et nécessiterait un tas de UNIONs.

Voici comment vous devez interroger toutes les stratégies quel que soit le type:

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;

Notez que l'ajout de nouveaux sous-types nécessiterait la modification de la requête ci-dessus avec un supplément UNION ALLpour chaque sous-type. Cela peut facilement entraîner des bogues dans votre application si cette opération est oubliée.

Héritage de table de classe (alias héritage de table par type):

C'est la solution que @David mentionne dans l'autre réponse . Vous créez une table unique pour votre classe de base, qui comprend tous les attributs communs. Ensuite, vous devez créer des tables spécifiques pour chaque sous-type, dont la clé primaire sert également de clé étrangère à la table de base. Exemple:

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

Cette solution résout les problèmes identifiés dans les deux autres conceptions:

  • Les attributs obligatoires peuvent être appliqués avec NOT NULL.

  • L'ajout d'un nouveau sous-type nécessite l'ajout d'une nouvelle table au lieu d'ajouter des colonnes à une existante.

  • Aucun risque qu'un attribut inapproprié soit défini pour un sous-type particulier.

  • Pas besoin de l' typeattribut.

  • Maintenant, les attributs communs ne sont plus mélangés avec les attributs spécifiques au sous-type.

  • Nous pouvons enfin rester SECS. Il n'est pas nécessaire de répéter les attributs communs pour chaque table de sous-type lors de la création des tables.

  • La gestion d'une incrémentation automatique idpour les stratégies devient plus facile, car elle peut être gérée par la table de base, au lieu que chaque table de sous-type les génère indépendamment.

  • La recherche de toutes les politiques, quel que soit le sous-type, devient désormais très simple: pas UNIONbesoin de - juste a SELECT * FROM policies.

Je considère l'approche de la table de classe comme la plus appropriée dans la plupart des situations.


Les noms de ces trois modèles proviennent du livre Patterns of Enterprise Application Architecture de Martin Fowler .


97
J'utilise également cette conception, mais vous ne mentionnez pas les inconvénients. Plus précisément: 1) vous dites que vous n'avez pas besoin du type; true mais vous ne pouvez pas identifier le type réel d'une ligne à moins que vous ne regardiez tous les tableaux de sous-types pour trouver une correspondance. 2) Il est difficile de synchroniser la table principale et les tables de sous-types (on peut par exemple supprimer la ligne dans la table de sous-types et non dans la table principale). 3) Vous pouvez avoir plusieurs sous-types pour chaque ligne principale. J'utilise des déclencheurs pour contourner 1, mais 2 et 3 sont des problèmes très difficiles. En fait, 3 n'est pas un problème si vous modélisez la composition, mais concerne l'héritage strict.

19
+1 pour le commentaire de @ Tibo, c'est un grave problème. L'héritage de table de classe produit en fait un schéma non normalisé. Là où l'héritage de table en béton ne fonctionne pas, et je ne suis pas d'accord avec l'argument selon lequel l'héritage de table en béton gêne DRY. SQL gêne DRY, car il n'a pas de métaprogrammation. La solution consiste à utiliser un Database Toolkit (ou à écrire le vôtre) pour faire le gros du travail, au lieu d'écrire directement SQL (rappelez-vous, ce n'est en fait qu'un langage d'interface DB). Après tout, vous n'écrivez pas non plus votre application d'entreprise en assembleur.
Jo So

18
@Tibo, à propos du point 3, vous pouvez utiliser l'approche expliquée ici: sqlteam.com/article/… , consultez la section Modélisation des contraintes de une à l'une .
Andrew

4
@DanielVassallo Tout d'abord merci pour la réponse étonnante, 1 doute si une personne a un policyId comment savoir si son policy_motor ou policy_property? Une façon consiste à rechercher policyId dans toutes les sous-tables, mais je suppose que c'est la mauvaise façon, n'est-ce pas, quelle devrait être la bonne approche?
ThomasBecker

11
J'aime vraiment votre troisième option. Cependant, je ne sais pas comment fonctionnera SELECT. Si vous sélectionnez * FROM politiques, vous récupérerez les identifiants de politique mais vous ne saurez toujours pas à quelle table de sous-type la politique appartient. Ne devrez-vous pas encore faire un JOIN avec tous les sous-types afin d'obtenir tous les détails de la politique?
Adam

14

La 3ème option consiste à créer une table "Policy", puis une table "SectionsMain" qui stocke tous les champs communs aux différents types de sections. Créez ensuite d'autres tables pour chaque type de section qui ne contiennent que les champs qui ne sont pas communs.

Le choix du meilleur dépend principalement du nombre de champs dont vous disposez et de la façon dont vous souhaitez écrire votre SQL. Ils fonctionneraient tous. Si vous n'avez que quelques champs, j'irais probablement avec # 1. Avec "beaucoup" de champs, je pencherais vers # 2 ou # 3.


+1: la 3ème option est la plus proche du modèle d'héritage et la plus normalisée de l'OMI
RedFilter

Votre option # 3 est vraiment ce que je voulais dire par l'option # 2. Il existe de nombreux champs et certaines sections auraient également des entités enfants.
Steve Jones

9

Avec les informations fournies, je modéliserais la base de données pour avoir les éléments suivants:

STRATÉGIES

  • POLICY_ID (clé primaire)

PASSIFS

  • LIABILITY_ID (clé primaire)
  • POLICY_ID (clé étrangère)

PROPRIÉTÉS

  • PROPERTY_ID (clé primaire)
  • POLICY_ID (clé étrangère)

... et ainsi de suite, car je m'attends à ce qu'il y ait différents attributs associés à chaque section de la stratégie. Sinon, il pourrait y avoir une seule SECTIONStable et en plus du policy_id, il y aurait un section_type_code...

Quoi qu'il en soit, cela vous permettrait de prendre en charge les sections facultatives par politique ...

Je ne comprends pas ce que vous trouvez insatisfaisant dans cette approche - c'est ainsi que vous stockez les données tout en préservant l'intégrité référentielle et en ne dupliquant pas les données. Le terme est "normalisé" ...

Parce que SQL est basé sur SET, il est plutôt étranger aux concepts de programmation procédurale / OO et nécessite du code pour passer d'un domaine à l'autre. Les ORM sont souvent pris en compte, mais ils ne fonctionnent pas bien dans les systèmes complexes à volume élevé.


Ouais, j'obtiens la normalisation ;-) Pour une structure aussi complexe, avec certaines sections simples et d'autres ayant leur propre sous-structure complexe, il semble peu probable qu'un ORM fonctionne, bien que ce soit bien.
Steve Jones

6

De plus, dans la solution Daniel Vassallo, si vous utilisez SQL Server 2016+, il existe une autre solution que j'ai utilisée dans certains cas sans perte de performances considérable.

Vous pouvez créer juste une table avec uniquement le champ commun et ajouter une seule colonne avec le JSON chaîne qui contient tous les champs spécifiques au sous-type.

J'ai testé cette conception pour gérer l'héritage et je suis très heureux de la flexibilité que je peux utiliser dans l'application relative.


1
Voilà une idée intéressante. Je n'ai pas encore utilisé JSON dans SQL Server, mais je l'utilise beaucoup ailleurs. Merci pour l'information.
Steve Jones

5

Une autre façon de le faire est d'utiliser le INHERITScomposant. Par exemple:

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

Il est ainsi possible de définir un héritage entre les tables.


Est-ce que d'autres bases de données prennent en charge INHERITSoutre PostgreSQL ? MySQL par exemple?
giannis christofakis

1
@giannischristofakis: MySQL n'est qu'une base de données relationnelle, tandis que Postgres est une base de données relationnelle objet. Donc, aucun MySQL ne prend en charge cela. En fait, je pense que Postgres est le seul SGBD actuel qui prend en charge ce type d'héritage.
a_horse_with_no_name

2
@ marco-paulo-ollivier, la question de l'OP concerne SQL Server, donc je ne comprends pas pourquoi vous fournissez une solution qui ne fonctionne qu'avec Postgres. Évidemment, ne pas régler le problème.
plan

@mapto, cette question est devenue une sorte de cible "dupe de l'héritage de style OO dans une base de données"; qu'il s'agissait à l'origine d'un serveur sql est probablement désormais hors de propos
Caius Jard

0

Je me penche vers la méthode n ° 1 (une table de section unifiée), dans le but de récupérer efficacement des politiques entières avec toutes leurs sections (ce que je suppose que votre système fera beaucoup).

De plus, je ne sais pas quelle version de SQL Server vous utilisez, mais dans 2008+ Sparse Columns aident à optimiser les performances dans les situations où la plupart des valeurs d'une colonne seront NULL.

En fin de compte, vous devrez décider à quel point les sections de politique sont "similaires". À moins qu'ils ne diffèrent considérablement, je pense qu'une solution plus normalisée pourrait être plus problématique qu'elle ne vaut ... mais vous seul pouvez faire cet appel. :)


Il y aura beaucoup trop d'informations pour présenter l'intégralité de la politique en une seule fois, il ne serait donc jamais nécessaire de récupérer l'intégralité du dossier. Je pense que c'est 2005, même si j'ai utilisé le peu de 2008 dans d'autres projets.
Steve Jones

D'où vient le terme "table de section unifiée"? Google ne montre presque aucun résultat pour cela et il y a déjà suffisamment de termes confus ici.
Stephan-v

-1

Vous pouvez également envisager d'utiliser des bases de données de documents (telles que MongoDB) qui prennent en charge nativement les structures de données riches et l'imbrication.


En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.