Être ou ne pas être NULL...Telle est la question !Date de publication : 17/09/2004
Par
SQLPro (autres articles) (CV) niveau : intermédiaire Sans vouloir être shakespearien la problématique du traitement des NULL rebute souvent les développeurs. 1. NULL c'est quoi ? 1.1. Pas de NULL svp ! 1.2. Plus de "NULL" svp ? 2. Test des marqueurs NULL 2.1. Test direct : IS NULL, IS NOT NULL 2.2. Dénullification : COALESCE 2.3. Nullification : NULLIF 2.4. Structures de traitement des marqueurs NULL 3. Jointures externes 4. Traitement des marqueurs NULL 4.1. Déclaration de variable dans le code procédurale SQL : DECLARE 4.2. Les calculs d'agrégats statistiques ignorent les marqueurs NULL : COUNT, SUM, AVG, MAX ou MIN 4.3. Le comptage des lignes de la table prend en compte les lignes NULL ! : COUNT(*) 4.4. NULL existe bien ! 4.5. L'ordre du NULL... 4.6. NULL avec DISTINCT et UNION 5. Définition de table et contraintes 5.1. Contrainte de colonne NUL / NOT NULL 5.2. Contrainte d'unicité 5.3. Autres contraintes 6. Mise à jour avec les NULL 6.1. NULL et INSERT 6.2. NULL et UPDATE 7. Particularité de certains SGBDR 7.1. Autres fonctions de traitement des NULL 7.2. NULL comme contrainte par défaut 7.3. NULL et CHAR 7.4. NULL et UNIQUE 7.5. Jointures externes et anciennes syntaxes 7.6. Prédicat = NULL ! 8. Références bibliographiques 1. NULL c'est quoi ?Première erreur courante, NULL, n'est pas une valeur. Pourtant on voit souvent fleurir dans la littérature comme dans les forums ou les pages Internet, des rédacteurs qui vous affirment que la colonne machin peut prendre la valeur NULL ! Or NULL n'est pas une valeur, c'est justement l'absence de valeur d'une colonne de table. En fait NULL est un marqueur, tout comme l'est le nil de certains langages de programmation qui permettent d'accéder directement à une adresse mémoire (C, Pascal...). Nil n'indique pas l'adresse... Il indique tout simplement qu'aucune assignation d'adresse et donc aucune réservation d'espace mémoire n'a encore eu lieu. Conséquence de tout cela, il n'est pas possible de comparer un marqueur comme NULL, car c'est justement une non valeur. En l'occurence une requête comme celle-ci :
...est syntaxiquement fausse et conduira la plupart du temps à un message d'erreur. Sauf que certains SGBDR acceptent délibérement une syntaxe aussi violante. 1.1. Pas de NULL svp !Certaines personnes préfèrent ne jamais avoir de colonnes marquées à NULL dans leurs bases de données. Cette position intégriste est une stupidité. Par exemple si vous notez la date de naissance des personnes, alors quelle valeur donnerez vous à cette colonne pour indiquez que vous ne savez pas ? Facile, me direz vous, on conviendra d'une valeur arbitraire, par exemple le 1er janvier 1900... Conséquence, ce que vous aviez pris pour une bonne idée devient vite un cauchemar... Car dès lors, la plupart des requêtes vont devenir un enfer et les performances se dégrader. Comment calculer l'âge par exemple ? Il en est de même de l'usage de la chaîne vide ou du zéro pour "pallier" aux NULL. Vous aurez l'air malin quand vous voudrez connaître le prix moyen d'un produit, si vous l'avez quantifié à zéro dans une ligne de facture : diviser par zéro vous assurera une erreur que le NULL vous aurait évité ! 1.2. Plus de "NULL" svp ?Chez les adeptes du NULL, l'idée émergente a longtemps été de rajouter quelques marqueurs supplémentaires : valeur inaplicable, valeur inconnue, etc... Dans un document devenu célèbre (Interim Report 75-02-08 voir en ref. bibliographique), les logiciens préconisaient l'usage de 14 marqueurs différents afin de rendre compte des situations d'incomplétude de données. Le docteur Codd classifia cela en deux grandes familles : les valeurs inapplicables (par exemple la couleur du toît d'une moto) et les valeurs inconnues (par exemple la couleur des yeux d'une personne portant des lunettes de soleil). Mais la chose ne manquait pas d'intérêt ! Comment par exemple faire en sorte de stocker une date représentant TOUJOURS le futur ? Un petit exemple : vous avez une table de tarifs applicable avec des dates butoirs. Les dates butoirs sont DATE_DEBUT et DATE_FIN. Comment spécifier que le tarif le plus récent doit avoir pour la colonne DATE_FIN, une valeur toujours supérieure à la date du jour ? Autrement dit, comment dans une base de données, représenter l'infini positif ou l'infini négatif ? De même comment représenter le débordement ?
Ou bien même, comment représenter une erreur, afin de la traiter "en ligne" dans la requête ? Se serait tellement plus simple si l'on pouvait faire, par exemple :
Mais la logique de traitement des marqueurs s'avéra à ce point complexe et rebutante qu'elle ne fût jamais réellement inscrite dans la norme et que les éditeurs refusèrent de dépenser le moindre centime en implémentation de cette extension... 2. Test des marqueurs NULLPar défaut, tout prédicat (expression logique évaluable) contenant un marqueur NULL, doit prendre la valeur UNKNOWN... Alors là, il convient de dire que UNKNOWN est bien une valeur. C'est de la logique 3 état. Autrement dit en SQL, un prédicat peut prendre les valeurs TRUE, FALSE et UNKNOWN. En principe on devrait même pouvoir construire des prédicats du genre :
Et donc la présente clause WHERE serait vraie si COLONNE1 ou COLONNE2 est marqué à NULL (ou inclusif). Mais peu de SGBDR ont implémenté un tel test. Attention à la logique des NULL dans les prédicats... Ainsi NULL OR TRUE vaut TRUE, NULL OR FALSE vaut FALSE, NULL AND FALSE vaut FALSE Mais la norme SQL nous fournit différents outils pour tester les marqueurs NULL : 2.1. Test direct : IS NULL, IS NOT NULLIl s'agit simplement de savoir si une colonne ou une expression est marquée à NULL Exemple :
2.2. Dénullification : COALESCEIl est possible de remplacer à la volée, donc dans une requête, un marqueur NULL par une valeur conventionnelle. Par exemlple, zéro, chaîne vide, etc... La particularité de cette fonction est d'accepter autant de paramètres que l'on veut (sans limitation théorique). Sa syntaxe est la suivante :
Elle renvoie la première expression évaluable (donc ne contenant pas de marqueur NULL) dans l'ordre positionnel de l'écriture de gauche à droite. Exemple :
Dans cet exemple, l'évaluation ligne à ligne renverra la valeur de la colonne debit si elle n'est pas marqué à NULL, sinon, la valeur de l'expression "credit * -1" si credit n'est pas marqué à NULL sinon, dans le cas ou debit et credit sont marqués à NULL, la valeur zéro. 2.3. Nullification : NULLIFNULLIF permet de marquer à NULL une expression donnant une certaine valeur. Exemple :
Renverra le marqueur NULL à chaque fois que debit et credit ont la même valeur. Donc ne sont pas marqués à NULL, ni l'un ni l'autre. 2.4. Structures de traitement des marqueurs NULLOn peut aussi utiliser les structures CASE et l'UNION pour traiter les NULL :
3. Jointures externesUn autre moyen de bien s'amuser avec les NULL est de faire des jointures externes. Au tout début du SQL, de telles jointures étaient impossibles car la base devait être parfaite ! Hélas, la perfection étant franchement dérangeant dans ce cas, il fallut se rendre à l'évidence : les jointures externes se devaient d'exister et donc les NULL de proliférer. Pour une étude sur les jointures externes et l'hypothèse du monde clos, lire : Les jointures Dès lors, avec une jointure externe gauche, la partie des colonnes de la table de droite sans lien avec la table gauche voit les valeurs de toute ses colonnes marquées à NULL. C'est bien entendu l'inverse dans une jointure externe droite. Avec la jointure externe bilatérale on se retrouve avec des NULL tantôt à droite tantôt à gauche.
Enfin, avec le UNION JOIN on se retrouve avec un résultat dont l'apparence est un genre de matrice dont seule la diagonale est alimentée par les valeurs originales des tables, toutes les autres parties étant nullifiées. Cette petite différence, va faire des choses bien intéressante lorsque l'on va rajouter un filtre WHERE... Voici un exemple de ce qui vous attend :
Suivant que l'on filtre dans la clause WHERE ou dans le prédicat de jointure, le résultat n'est pas le même. C'est logique car le filtrage s'applique APRÈS la jointure. Autrement dit la jointure externe conserve ses NULL. Pour remédier à l'un ou à l'autre on peut faire :
Ou encore utiliser des fonctions et structures de denullifiation comme indiqué en 2.2 et 2.3 4. Traitement des marqueurs NULLMais comment SQL utilise t-il les marqueurs NULL dans ses traitements ? C'est de cela que nous allons maintenant parler... On dit que les marqueurs NULL se propagent dans les expressions. Certains auteurs le qualifient "d'absorbant"... Exemple :
En conclusion, quelque soit l'expression, même si elle est calculée, elle vaudra UNKNOWN en présence d'un ou de plusieurs marqueurs NULL. 4.1. Déclaration de variable dans le code procédurale SQL : DECLARELe mot clef DECLARE qui permet de spécifier une variable dans un code procédural SQL (procédure stockée, fonction, triggers...) marque immédiatement la variable déclarée à NULL.
Il convient donc de l'initialiser si l'on veut l'utiliser dans une expression. 4.2. Les calculs d'agrégats statistiques ignorent les marqueurs NULL : COUNT, SUM, AVG, MAX ou MINPar exception à la régle de propagation des NULL, l'absence de valeur dans une colonne n'a pas pour effet d'entâcher le calcul d'un agrégat. Si l'on utilise SUM, AVG, MAX ou MIN, le résultat renverra toujours une valeur si au moins une valeur existe, sinon le marqueur NULL. En revanche, COUNT renverra toujours une valeur et jamais le marqueur NULL : lorsque toutes les occurences de la colonne sont NULL, COUNT renvoi zéro.
Attention, car vos statistiques peuvent être faussées... Par exemple le calcul d'une moyenne est différent si l'on dénullifie à 0 ou 1... Exemple :
AVG et MIN n'ont plus les mêmes valeurs... 4.3. Le comptage des lignes de la table prend en compte les lignes NULL ! : COUNT(*)COUNT(*) est un calcul d'agrégat un peut particulier dans le sens ou il compte le nombre de lignes que possède la table. Dans ce cas, même si la ligne voit toute ses colonnes marquées à NULL, elle est quand même comptée. C'est l'exception de l'exception !
Les 1ère et 10ème lignes entièrement à NULL ont bien été comptées ! 4.4. NULL existe bien !Le prédicat EXISTS est évalué à vrai, même si les lignes retournées ont toutes leurs colonnes marquées à NULL :
En dépit du fait que la seule ligne insérée dans la table est "vide", EXISTS indique qu'il y a bien une ligne... On rencontrera un comportement similaire avec les prédicats MATCH et UNIQUE (attention pour le UNIQUE, si plusieurs lignes renvoyées sont NULL, le principe est que l'unicité est conservée !)... 4.5. L'ordre du NULL...Une question intérressante est de savoir dans quel ordre doivent se trouver les marqueurs NULL ? Comme il ne s'agit pas de valeurs, une clause tel que ORDER BY peut difficilement décider de les trier... Mais la norme SQL:1999 (Window partionning, OLAP) a bien fait les choses, puisqu'elle précise un paramètre d'ordonancement des NULL qui peut prendre la valeur NULL FIRST ou NULL LAST. Le problème c'est que peut d'éditeur ont optés pour un tel rafinement. Ainsi dans la documentation de MS SQL Server, on trouve la mention : "Les valeurs NULL sont traitées comme les valeurs les plus petites possible." Tant et si bien que la même requête basique avec un ORDER BY ne donnera pas le même classement, DB2 UDB ayant opté pour l'inverse ! En effet , dans son aide en ligne IBM indique que "Ordering is performed in accordance with the comparison rules described in "Language Elements". The null value is higher than all other values." 4.6. NULL avec DISTINCT et UNIONAussi bizarre que cela puisse paraître, l'utilisation de DISTINCT dans la clause SELECT de l'ordre SELECT ne doit pas "dédoublonner" les lignes à NULL. Pourquoi ? Parce que encore une fois, toute comparaison de NULL est impossible, donc chaque NULL est différent ! C'est assez logique d'ailleurs. Par exemple, si vous avez une colonne représentant le n° de téléphone de vos proches et que cette colonne n'est pas toujours renseignée, il parait évident que les n° que vous mettrez à la place des marqueurs vides seront différentes les uns des autres (ou alors c'est que vous n'avez pas tenu compte des principes fondamentaux de la normalisation !) Exemple :
Mais à ma connaissance, aucun SGBDR ne traite le dédoublonnement des NULL de cette façon car tous considérent que NULL est une "valeur" ! Il en est de même dans le cas de l'union... Par défaut l'UNION de deux résultats de requête est dédoublonné. Sauf pour les lignes dont les colonnes sont marquées à NULL...
Même constat chez les éditeurs de SGBDR ! Aucun éditeur n'a pensé à conserver les doublons de NULL dans l'UNION "distinct"... 5. Définition de table et contraintesDans la création des tables d'une base de données, beaucoups d'éléments reposent sur la problématique du traitement des NULL. Nous allons en voir quelques unes... 5.1. Contrainte de colonne NUL / NOT NULLEn l'absence de toute spécification dans l'ordre de création, une colonne peut ne contenir aucune valeur. En l'occurence, les deux formulations :
sont équivalentes... car par défaut, la contrainte est : marqueur NULL possible. Si l'on veut contraindre la colonne à toujours devoir spécifier une valeur il faut ajouter la contrainte NOT NULL. 5.2. Contrainte d'unicitéDans une contrainte d'unicité, si l'on ne précise pas NOT NULL comme contrainte des colonnes visées par la contrainte "UNIQUE" alors il pourra y avoir autant de tuple contraint n'ayant pas de valeurs spécifique. Exemple :
La contrainte ne sera violée QUE pour la dernière ligne. La 5eme provoquera l'insertion d'un doublon de ligne NULL sans déclencher de violation de contrainte d'unicité, car la comparaison entre NULL est par définition une hérésie ! 5.3. Autres contraintesLa violation des contraintes d'intégrité en présence de marqueur NULL dépend de la clause MATCH qui permet d'indiquer de quelle façon traiter l'absence de valeur. Ainsi, un "MATCH SIMPLE" implique que toutes les colonnes composant la contrainte d'intégrité soient renseignées. Dans le cas ou l'un des colonne possède un marqueur NULL, la contrainte est violée. A lire sur le sujet : ../sqlaz/ddl/?page=partie2#L7.3.1 Mais peu de SGBDR vont jusque là dans la gestion de l'intégrité référentielle. 6. Mise à jour avec les NULLSQL nous facilite la vie à l'aide des NULL pour les insertions et les mises à jour... 6.1. NULL et INSERTOn peut spécifier le marqueur NULL comme absence de valeur de colonnes lors d'une insertion Exemple :
NOTA : ces deux requêtes sont équivalentes Point n'est besoin de spécifier le marqueur NULL si l'on ne spécifie pas la colonne dans le liste des colonnes cible. Exemple :
NOTA : cette requête est équivalente aux deux précédentes. On peut spécifier une contrainte de défaut NULL et faire usage du mot clef DEFAULT lors de l'insertion. Exemple :
6.2. NULL et UPDATEOn peut supprimer la valeur d'une colonne en assignant le marqueur NULL. Exemple :
Ces deux écritures conduiront toutes deux à effacer toutes les valeurs contenues dans la colonne CLI_ENSEIGNE de la table T_CLIENT, pourvu que la colonne CLI_ENSEIGNE soit "NULLABLE". NOTA : on peut remarquer que la norme à manqué de finesse en utilisant un "= NULL" mal à propos pour la désassignation de valeur, alors qu'elle nous oblige à utiliser IS NULL et non = NULL dans un prédicat. On aurait préféré rester cohérent et ajouter une désassignation telle que : SET NULL par exemple. 7. Particularité de certains SGBDR7.1. Autres fonctions de traitement des NULLCertains SGBDR utilisent d'autres fonctions pour dénullifier. Par exemple on trouve NVL sous Oracle, en plus de COALESCE : cette fonction ne porte que sur 2 arguments. On trouve également en alternative sous SQL Server une fonction ISNULL. 7.2. NULL comme contrainte par défautSybase et SQL Server font à ce sujet, l'inverse de ce que spécifie la norme. Ainsi toute table créée sans précision de NULL sont NOT NULL ! 7.3. NULL et CHARSybase n'accepte pas le CHAR sans contrainte NOT NULL. Il le transforme en VARCHAR. En fait il stocke le fait d'être NULL pour un VARCHAR en y mettant une chaine vide ! Plus curieux encore, si vous tentez de mettre une chaine vide dans une telle colonne, alors elle ne sera pas réellement vide. Elle sera composée d'un seul caractère blanc ! 7.4. NULL et UNIQUEPeu de SGBDR acceptent la contrainte d'unicité telle que définie dans la norme. SQL Server la refuse et considère que différentes lignes à NULL violent la contrainte d'unicité. Il accepte une seule ligne avec des NULL, tandis que DB2 refuse tout NULL. 7.5. Jointures externes et anciennes syntaxesLa plupart du temps, les anciennes syntaxes de traitement des jointures externes sont incohérentes. Lire la démonstration qu'a fait Steve Kass de la Drew University au sujet de SQL Server et des différents résultats qu'il obtient pour une deux requêtes logiquement équivalente, l'une basée sur la syntaxe des jointures normative et l'autre sur l'ancienne syntaxe en =*... Les jointures 7.6. Prédicat = NULL !Encore plus stupéfiant... Sous SQL Server, la requête :
ne provoque aucune erreur de syntaxe, et ne donne pas le même résultat que :
8. Références bibliographiquesIncomplete and Uncertain Information in Relational Databases - Esteban ZIMANYI - Université Libre de Bruxelles : http://cs.ulb.ac.be/publications/PhD-92-01.pdf NULL Handling in SQLite Versus Other Database Engines : http://www.hwaci.com/sw/sqlite/nulls.html ANSI/X3/SPARC, "Study Group on Data Base Management Systems", Interim Report 75-02-08, FDT Bulletin of ACM SIGMOD, Volume 7-2, February 1975. SQL for smarties - Joe Celko - Morgan Kaufmann SQL Performance Tuning - Peter Gulutzan & Trudy Pelzer - Addison Wesley SQL:1999 - Jim Melton - Morgan Kaufman Remerciements à Braim pour la relecture orthographique de cet article, ainsi qu'aux membres de la rubrique SGBD.
|
Copyright © 2004 Frédéric Brouard. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Cette page est déposée.
Copyright © 2000-2012 - www.developpez.com