par Guillaume Lelarge.

Contraintes NOT NULL


Pour indexer une condition IS NULL dans la base de données Oracle, l'index doit avoir une colonne qui ne peut jamais être NULL.

Cela étant dit, il n'est pas suffisant qu'il n'y ait pas d'entrées NULL. La base de données doit être sûre qu'il ne peut jamais y avoir d'entrée NULL. Dans le cas contraire, la base de données va supposer que la table contient des lignes qui ne sont pas dans l'index.

L'index suivant est utilisable sur la requête seulement si la colonne NOM dispose d'une contrainte NOT NULL :

DROP INDEX emp_ddn;
CREATE INDEX emp_ddn_nom
          ON employes (date_de_naissance, nom);

SELECT *
  FROM employes
 WHERE date_de_naissance IS NULL
---------------------------------------------------------------
|Id |Operation                   | Name         | Rows | Cost |
---------------------------------------------------------------
| 0 |SELECT STATEMENT            |              |    1 |    3 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES     |    1 |    3 |
|*2 |  INDEX RANGE SCAN          | EMP_DDN_NOM  |    1 |    2 |
---------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("DATE_DE_NAISSANCE" IS NULL)

Supprimer la contrainte NOT NULL rend l'index inutilisable pour cette requête :

ALTER TABLE employes MODIFY nom NULL;
SELECT *
  FROM employes
 WHERE date_de_naissance IS NULL
----------------------------------------------------
| Id | Operation         | Name      | Rows | Cost |
----------------------------------------------------
|  0 | SELECT STATEMENT  |           |    1 |  477 |
|* 1 |  TABLE ACCESS FULL| EMPLOYES  |    1 |  477 |
----------------------------------------------------

Astuce

Une contrainte NOT NULL manquante peut empêcher l'utilisation de l'index dans une base de données Oracle, tout spécialement pour les requêtes count(*).

En dehors des contraintes NOT NULL, la base de données sait aussi que les expressions constantes comme dans la section précédente ne peuvent pas devenir NULL.

Néanmoins, un index sur une fonction définie par un utilisateur n'impose pas une contrainte NOT NULL sur l'expression de l'index :

CREATE OR REPLACE FUNCTION boitenoire(id IN NUMBER) RETURN NUMBER
DETERMINISTIC
IS BEGIN
   RETURN id;
END;
DROP INDEX emp_ddn_nom;
CREATE INDEX emp_ddn_bn 
    ON employes (date_de_naissance, boitenoire(id_employe));

SELECT *
  FROM employes
 WHERE date_de_naissance IS NULL;

----------------------------------------------------
| Id | Operation         | Name      | Rows | Cost |
----------------------------------------------------
|  0 | SELECT STATEMENT  |           |    1 |  477 |
|* 1 |  TABLE ACCESS FULL| EMPLOYES  |    1 |  477 |
----------------------------------------------------

Le nom de la fonction, BOITENOIRE, met l'accent sur le fait que l'optimiseur n'a pas la moindre idée de ce que fait la fonction (voir « Recherche insensible à la casse en utilisant UPPER ou LOWER »). Nous pouvons voir que la fonction passe la valeur en entrée directement, sans transformation, mais pour la base de données c'est juste une fonction qui renvoie une valeur. La propriété NOT NULL du paramètre est perdue. Bien que l'index doive avoir toutes les lignes, la base de données ne le sait pas, donc elle ne peut pas utiliser l'index pour la requête.

Si vous savez que la fonction ne renvoie jamais NULL, comme dans cet exemple, vous pouvez changer la requête pour qu'elle reflète cette information :

SELECT *
  FROM employes
 WHERE date_de_naissance IS NULL
   AND boitenoire(id_employe) IS NOT NULL;

-------------------------------------------------------------
|Id |Operation                   | Name       | Rows | Cost |
-------------------------------------------------------------
| 0 |SELECT STATEMENT            |            |    1 |    3 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES   |    1 |    3 |
|*2 |  INDEX RANGE SCAN          | EMP_DDN_BN |    1 |    2 |
-------------------------------------------------------------

La condition supplémentaire dans la clause where est toujours vraie et, du coup, ne change pas le résultat. Néanmoins, la base de données Oracle reconnaît que vous pouvez récupérer des lignes qui doivent être dans l'index par définition.

Malheureusement, il n'existe pas de moyens pour indiquer qu'une fonction ne renvoie jamais de NULL mais vous pouvez déplacer l'appel de fonction dans une colonne virtuelle (depuis la version 11g) et placez une contrainte NOT NULL sur cette colonne.

ALTER TABLE employes ADD expression_bn
      GENERATED ALWAYS AS (boitenoire(id_employe)) NOT NULL;

DROP   INDEX emp_ddn_bn;
CREATE INDEX emp_ddn_bn 
    ON employes (date_de_naissance, expression_bn);

SELECT *
  FROM employes
 WHERE date_de_naissance IS NULL;

-------------------------------------------------------------
|Id |Operation                   | Name       | Rows | Cost |
-------------------------------------------------------------
| 0 |SELECT STATEMENT            |            |    1 |    3 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES   |    1 |    3 |
|*2 |  INDEX RANGE SCAN          | EMP_DDN_BN |    1 |    2 |
-------------------------------------------------------------

La base de données Oracle sait que certaines fonctions internes renvoient seulement NULL si NULL est fourni en entrée.

DROP INDEX emp_ddn_bn;

CREATE INDEX emp_ddn_nommaj
    ON employees (date_de_naissance, upper(nom));

SELECT *
  FROM employes
 WHERE date_de_naissance IS NULL;

----------------------------------------------------------
|Id |Operation                   | Name           | Cost |
----------------------------------------------------------
| 0 |SELECT STATEMENT            |                |    3 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES       |    3 |
|*2 |  INDEX RANGE SCAN          | EMP_DDN_NOMMAJ |    2 |
----------------------------------------------------------

La fonction UPPER conserve la propriété NOT NULL de la colonne NOM. Néanmoins, supprimer cette contrainte rend l'index inutilisable :

ALTER TABLE employes MODIFY nom NULL;

SELECT *
  FROM employes
 WHERE date_de_naissance IS NULL;

----------------------------------------------------
| Id | Operation         | Name      | Rows | Cost |
----------------------------------------------------
|  0 | SELECT STATEMENT  |           |    1 |  477 |
|* 1 |  TABLE ACCESS FULL| EMPLOYES  |    1 |  477 |
----------------------------------------------------

À propos de l'auteur

Photo de Markus Winand

Markus Winand teaches efficient SQL—inhouse and online. He minimizes the development time using modern SQL and optimizes the runtime with smart indexing—for that he also published the book SQL Performance Explained.

“Use The Index, Luke!” by Markus Winand and translated by Guillaume Lelarge is licensed under a Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License.
Mentions légales | Contact | NO WARRANTY | Marque déposée | Privacy | CC-BY-NC-ND 3.0 license