Quel est le problème avec les colonnes Nullable dans les clés primaires composites?

149

ORACLE n'autorise les valeurs NULL dans aucune des colonnes qui comprennent une clé primaire. Il semble qu'il en soit de même pour la plupart des autres systèmes «au niveau de l'entreprise».

Dans le même temps, la plupart des systèmes autorisent également des contraintes uniques sur les colonnes Nullable.

Pourquoi les contraintes uniques peuvent-elles avoir des valeurs NULL mais pas les clés primaires? Y a-t-il une raison logique fondamentale à cela, ou s'agit-il davantage d'une limitation technique?

Roman Starkov
la source

Réponses:

216

Les clés primaires servent à identifier les lignes de manière unique. Cela se fait en comparant toutes les parties d'une clé à l'entrée.

Par définition, NULL ne peut pas faire partie d'une comparaison réussie. Même une comparaison avec elle-même ( NULL = NULL) échouera. Cela signifie qu'une clé contenant NULL ne fonctionnerait pas.

De plus, NULL est autorisé dans une clé étrangère, pour marquer une relation facultative. (*) L' autoriser également dans le PK briserait cela.


(*) Un mot d'avertissement: avoir des clés étrangères Nullable n'est pas une conception de base de données relationnelle propre.

S'il y a deux entités Aet BApeut éventuellement être lié B, la solution propre est de créer une table de résolution (disons AB). Ce tableau serait un lien Aavec B: S'il est une relation alors il contiendrait un enregistrement, s'il est pas alors il ne serait pas.

Tomalak
la source
5
J'ai changé la réponse acceptée par celle-ci. À en juger par les votes, cette réponse est la plus claire pour plus de gens. Je pense toujours que la réponse de Tony Andrews explique mieux l' intention derrière cette conception; vérifiez-le aussi!
Roman Starkov
2
Q: Quand voulez-vous un NULL FK au lieu d'un manque de ligne? R: Uniquement dans une version d'un schéma dénormalisé pour l'optimisation. Dans les schémas non triviaux, des problèmes non normalisés comme celui-ci peuvent causer des problèmes chaque fois que de nouvelles fonctionnalités sont nécessaires. otoh, la foule de la conception Web s'en fiche. J'ajouterais au moins une mise en garde à ce sujet au lieu de donner l'impression que c'est une bonne idée de conception.
zxq9
3
"Avoir des clés étrangères Nullable n'est pas une conception de base de données relationnelle propre." - une conception de base de données sans null (sixième forme normale) ajoute invariablement de la complexité, les gains d'espace gagnés sont souvent compensés par le travail de programmeur supplémentaire nécessaire pour réaliser ces gains.
Dai
1
et si c'était une table de résolution ABC? avec facultatif C
Bart Calixto
1
J'ai essayé d'éviter d'écrire «parce que la norme l'interdit», car cela n'explique vraiment rien.
Tomalak
62

Une clé primaire définit un identifiant unique pour chaque ligne d'une table: lorsqu'une table a une clé primaire, vous disposez d'un moyen garanti de sélectionner n'importe quelle ligne à partir de celle-ci.

Une contrainte unique n'identifie pas nécessairement chaque ligne; il précise juste que si une ligne a des valeurs dans ses colonnes, alors ils doivent être uniques. Cela ne suffit pas pour identifier de manière unique chaque ligne, ce que doit faire une clé primaire.

Tony Andrews
la source
10
Dans Sql Server, une contrainte unique qui a une colonne Nullable, autorise la valeur «null» dans cette colonne une seule fois (étant donné des valeurs identiques pour les autres colonnes de la contrainte). Ainsi, une telle contrainte unique se comporte essentiellement comme un pk avec une colonne Nullable.
Gerard
Je confirme la même chose pour Oracle (11.2)
Alexander Malakhov
2
Dans Oracle (je ne connais pas SQL Server), la table peut contenir de nombreuses lignes où toutes les colonnes d'une contrainte unique sont nulles. Cependant, si certaines colonnes de la contrainte unique ne sont pas nulles et certaines sont nulles, l'unicité est appliquée.
Tony Andrews
Comment cela s'applique-t-il au composite UNIQUE?
Dims
1
@Dims Comme pour presque tout le reste des bases de données SQL, "cela dépend de l'implémentation". Dans la plupart des bases de données, une "clé primaire" est en fait une contrainte UNIQUE en dessous. L'idée de "clé primaire" n'est pas vraiment plus spéciale ou plus puissante que le concept d'UNIQUE. La vraie différence est que si vous avez deux aspects indépendants d'une table qui peuvent être garantis UNIQUE, vous n'avez pas de base de données normalisée par définition (vous stockez deux types de données dans la même table).
zxq9
46

Fondamentalement, rien ne va pas avec un NULL dans une clé primaire multi-colonnes. Mais en avoir un a des implications que le concepteur n'avait probablement pas l'intention, c'est pourquoi de nombreux systèmes génèrent une erreur lorsque vous essayez ceci.

Prenons le cas des versions de module / package stockées sous la forme d'une série de champs:

CREATE TABLE module
  (name        varchar(20) PRIMARY KEY,
   description text DEFAULT '' NOT NULL);

CREATE TABLE version
  (module      varchar(20) REFERENCES module,
   major       integer NOT NULL,
   minor       integer DEFAULT 0 NOT NULL,
   patch       integer DEFAULT 0 NOT NULL,
   release     integer DEFAULT 1 NOT NULL,
   ext         varchar(20),
   notes       text DEFAULT '' NOT NULL,
   PRIMARY KEY (module, major, minor, patch, release, ext));

Les 5 premiers éléments de la clé primaire sont des parties régulièrement définies d'une version finale, mais certains packages ont une extension personnalisée qui n'est généralement pas un entier (comme "rc-foo" ou "vanilla" ou "beta" ou quoi que ce soit d'autre pour que quatre champs sont insuffisants pourraient imaginer). Si un paquet n'a pas d'extension, alors il est NULL dans le modèle ci-dessus, et aucun mal ne serait fait en laissant les choses de cette façon.

Mais qu'est - ce qu'un NULL? Il est censé représenter un manque d'information, une inconnue. Cela dit, cela a peut-être plus de sens:

CREATE TABLE version
  (module      varchar(20) REFERENCES module,
   major       integer NOT NULL,
   minor       integer DEFAULT 0 NOT NULL,
   patch       integer DEFAULT 0 NOT NULL,
   release     integer DEFAULT 1 NOT NULL,
   ext         varchar(20) DEFAULT '' NOT NULL,
   notes       text DEFAULT '' NOT NULL,
   PRIMARY KEY (module, major, minor, patch, release, ext));

Dans cette version, la partie «ext» du tuple n'est PAS NULL mais par défaut une chaîne vide - qui est sémantiquement (et pratiquement) différente d'un NULL. Un NULL est une inconnue, alors qu'une chaîne vide est un enregistrement délibéré de "quelque chose qui n'est pas présent". En d'autres termes, «vide» et «nul» sont des choses différentes. C'est la différence entre «Je n'ai pas de valeur ici» et «Je ne sais pas quelle est la valeur ici».

Lorsque vous enregistrez un package qui n'a pas d'extension de version, vous savez qu'il n'a pas d'extension, donc une chaîne vide est en fait la valeur correcte. Un NULL ne serait correct que si vous ne saviez pas s'il avait une extension ou non, ou si vous saviez qu'il le faisait mais ne saviez pas ce que c'était. Cette situation est plus facile à gérer dans les systèmes où les valeurs de chaîne sont la norme, car il n'y a aucun moyen de représenter un "entier vide" autre que l'insertion de 0 ou 1, qui finira par être cumulé dans toutes les comparaisons effectuées plus tard (ce qui a ses propres implications) *.

Incidemment, les deux méthodes sont valides dans PostgreSQL (puisque nous parlons de RDMBS "entreprise"), mais les résultats de la comparaison peuvent varier un peu lorsque vous ajoutez un NULL au mélange - parce que NULL == "ne sais pas" donc tout les résultats d'une comparaison impliquant une liquidation NULL étant NULL puisque vous ne pouvez pas savoir quelque chose d'inconnu. DANGER! Réfléchissez bien à cela: cela signifie que les résultats de comparaison NULL se propagent à travers une série de comparaisons. Cela peut être une source de bugs subtils lors du tri, de la comparaison, etc.

Postgres suppose que vous êtes un adulte et peut prendre cette décision vous-même. Oracle et DB2 supposent que vous ne vous êtes pas rendu compte que vous faisiez quelque chose de stupide et lancent une erreur. C'est généralement la bonne chose, mais pas toujours - vous pourriez en fait ne pas savoir et avoir un NULL dans certains cas et donc laisser une ligne avec un élément inconnu contre lequel des comparaisons significatives sont impossibles est un comportement correct.

Dans tous les cas, vous devez vous efforcer d'éliminer le nombre de champs NULL que vous autorisez sur l'ensemble du schéma et doublement lorsqu'il s'agit de champs qui font partie d'une clé primaire. Dans la grande majorité des cas, la présence de colonnes NULL est une indication d'une conception de schéma non normalisée (par opposition à une conception délibérément dénormalisée) et doit faire l'objet d'une réflexion approfondie avant d'être acceptée.

[* NOTE: Il est possible de créer un type personnalisé qui est l'union d'entiers et un type "bas" qui signifierait sémantiquement "vide" par opposition à "inconnu". Malheureusement, cela introduit un peu de complexité dans les opérations de comparaison et le fait d'être vraiment correct en général ne vaut pas la peine en pratique, car vous ne devriez pas avoir du tout autorisé à avoir beaucoup de NULLvaleurs en premier lieu. Cela dit, ce serait merveilleux si les SGBDR incluaient un BOTTOMtype par défaut en plus de NULLpour éviter l'habitude de confondre avec désinvolture la sémantique de "aucune valeur" avec "valeur inconnue". ]

zxq9
la source
5
Ceci est une réponse TRÈS BELLE et explique beaucoup de choses sur les valeurs NULL et ses implications dans de nombreuses situations. Vous, monsieur, avez maintenant mon respect! Même à l'université, je n'ai pas eu une si bonne explication sur les valeurs NULL dans les bases de données. Je vous remercie!
Je soutiens l'idée principale de cette réponse. Mais écrire comme 'censé représenter un manque d'information, un inconnu', 'sémantiquement (et pratiquement) différent d'un NULL', 'A NULL is an unknown', 'une chaîne vide est un enregistrement délibéré de "quelque chose qui n'est pas présent "',' NULL ==" ne sais pas "', etc. . (Y compris en inspirant la (mauvaise) conception des fonctionnalités SQL NULL.) Ils ne justifient ou n'expliquent rien; ils devraient être expliqués et démystifiés.
philipxy
21

NULL == NULL -> false (au moins dans les SGBD)

Vous ne pourrez donc pas récupérer de relations en utilisant une valeur NULL, même avec des colonnes supplémentaires avec des valeurs réelles.

Cogsy
la source
1
Cela semble être la meilleure réponse, mais je ne comprends toujours pas pourquoi cela est interdit lors de la création de la clé primaire. S'il ne s'agissait que d'un problème de récupération, vous pouvez utiliser where pk_1 = 'a' and pk_2 = 'b'avec des valeurs normales et basculer vers des valeurs where pk_1 is null and pk_2 = 'b'nulles.
EoghanM
Ou encore plus fiable, where (a.pk1 = b.pk1 or (a.pk1 is null and b.pk1 is null)) and (a.pk2 = b.pk2 or (a.pk2 is null and b.pk2 is null))/
Jordan Rieger
8
Mauvaise réponse. NULL == NULL -> INCONNU. Pas faux. Le hic, c'est qu'une contrainte n'est pas considérée comme violée si le résultat du test est INCONNU. Cela donne souvent l' impression que la comparaison donne un résultat faux, mais ce n'est vraiment pas le cas.
Erwin Smout
4

La réponse de Tony Andrews est décente. Mais la vraie réponse est que cela a été une convention utilisée par la communauté des bases de données relationnelles et n'est PAS une nécessité. C'est peut-être une bonne convention, peut-être pas.

Comparer quoi que ce soit à NULL entraîne INCONNU (3e valeur de vérité). Ainsi, comme cela a été suggéré avec des nulls, toute la sagesse traditionnelle concernant l'égalité est abandonnée. Eh bien, c'est ce que cela semble à première vue.

Mais je ne pense pas que ce soit nécessairement le cas et même les bases de données SQL ne pensent pas que NULL détruit toute possibilité de comparaison.

Exécutez dans votre base de données la requête SELECT * FROM VALUES (NULL) UNION SELECT * FROM VALUES (NULL)

Ce que vous voyez est juste un tuple avec un attribut qui a la valeur NULL. Donc, l'union a reconnu ici les deux valeurs NULL comme égales.

Lors de la comparaison d'une clé composite qui a 3 composants à un tuple avec 3 attributs (1, 3, NULL) = (1, 3, NULL) <=> 1 = 1 AND 3 = 3 AND NULL = NULL Le résultat de ceci est INCONNU .

Mais nous pourrions définir un nouveau type d'opérateur de comparaison par exemple. ==. X == Y <=> X = Y OU (X EST NULL ET Y EST NULL)

Avoir ce type d'opérateur d'égalité rendrait les clés composites avec des composants nuls ou une clé non composite avec une valeur nulle sans problème.

Rami Ojares
la source
1
Non, l'UNION a reconnu les deux NULL comme non distincts. Ce qui n'est pas la même chose que «égal». Essayez UNION ALL à la place et vous obtiendrez deux lignes. Et quant au "nouveau type d'opérateur de comparaison", SQL l'a déjà. N'EST PAS DISTINCT DE. Mais cela ne suffit pas en soi. L'utilisation de ceci dans des constructions SQL telles que NATURAL JOIN, ou la clause REFERENCES d'une clé étrangère, nécessitera encore des options supplémentaires sur ces constructions.
Erwin Smout
Aha, Erwin Smout. Vraiment un plaisir de vous rencontrer également sur ce forum! Je n'étais pas au courant du "IS NOT DISTINCT FROM" de SQL. Très intéressant! Mais il semble que c'est exactement ce que je voulais dire avec mon opérateur == inventé. Pourriez-vous m'expliquer pourquoi vous dites que: "cela ne suffit pas"?
Rami Ojares
La clause REFERENCES s'appuie sur l'égalité, par définition. Un type de RÉFÉRENCES qui correspond à un tuple / ligne enfant avec un tuple / ligne parent, basé sur le fait que les valeurs d'attribut correspondant ne sont PAS DISTINCT au lieu de (le plus strict) EQUAL, nécessiterait la possibilité de spécifier cette option, mais la syntaxe ne le fait pas permettez-ceci. Idem pour NATURAL JOIN.
Erwin Smout
Pour qu'une clé étrangère fonctionne, la référence doit être unique (c'est-à-dire que toutes les valeurs doivent être distinctes). Ce qui signifie qu'il pourrait avoir une seule valeur nulle. Toutes les valeurs nulles pourraient alors faire référence à ce nul unique si les REFERENCES étaient définies avec l'opérateur NOT DISTINCT. Je pense que ce serait mieux (dans le sens de plus utile). Avec JOINs (à la fois externes et internes), je pense que les égaux stricts sont meilleurs parce que les "NULL MATCHES" se multiplieraient lorsque les valeurs nulles du côté gauche correspondraient à toutes les valeurs nulles du côté droit.
Rami Ojares
1

Je pense toujours que c'est un défaut fondamental / fonctionnel provoqué par une technicité. Si vous avez un champ facultatif par lequel vous pouvez identifier un client, vous devez maintenant pirater une valeur fictive, simplement parce que NULL! = NULL, pas particulièrement élégant mais c'est un "standard de l'industrie"

Adriaan Davel
la source