Transitivité des contraintes CHECK ou quand les tables stockées dans SYS n'obéissent pas aux mêmes règles !

Les contraintes CHECK, comme les autres contraintes donnent des informations à l’optimiseur pour résoudre les plans d’exécutions. Cependant, du fait de la manière dont Oracle estime les cardinalités avec des fonctions cela peut poser des problèmes. Jonathan Lewis donne un exemple intéressant sur son blog. De ce fait, en fonction des versions et des cas d’utilisation, Oracle tire partie ou pas de ces contraintes…

Autremennt dit, la manière dont les prédicats associés aux contraintes CHECK sont poussés dans les opérations des plans d’exécution dépend d’un certain nombre de facteurs et notamment :

  • Si la colonne testée implique directement la colonnes sur laquelle il y a une contrainte CHECK ou, par transitivité, une autre colonne
  • Si la condition de test est contradictoire avec la contrainte
  • Si le propriétaire de la table incriminée est SYS ou un autre utilisateur

Cet article présente plusieurs exemples de plans mettant en oeuvre une contrainte CHECK pour accompagner vos réflexions. Et si vous n’êtes pas satisfait du comportement d’Oracle, vous pouvez toujours utiliser la Note [ID 271999.1] « Disable generation of predicates from CHECK constraints »

Comme vous le verrez ci-dessous, il est essentiel d’utiliser un autre utilisateur que SYS dans vos exemples. Ces tests ont été réalisés avec une version 11.2.0.2.2 sous Linux 32 bits. Il est possible que vous ayez des comportements différents avec des versions différentes

Schéma exemple

Pour commencer, créez un schéma d’exemple spécifique :

create user demo
identified by demo
default tablespace users
temporary tablespace temp;

grant connect, resource, dba to demo;

connect demo/demo

drop table T1 purge;
create table t1 (id number,
id2 number,
lib varchar2(100));

insert into T1
(select 1, rownum, rpad(to_char(rownum),100)
from dual connect by level <=10000);

alter table T1 add constraint T1_IDIS1 check (id=1);

commit;

create index T1_ID2 on T1(ID2);

exec dbms_stats.gather_table_stats(user,'T1')

Exemple simple où la contrainte n’apparait pas

Nous allons commencer par un exemple simple. Comme vous le voyez ci-dessous, la contrainte T1_IDIS1 implique que la valeur de la colonne ID est toujours 1 ou NULL et pourtant, si vous exécutez une requête avec un prédicat plus large, la contrainte n’est pas prise en compte :

connect demo/demo

explain plan for
select *
from T1
where id>0;

select *
from table(
dbms_xplan.display(
format=>'basic +cost +predicate +note'));

PLAN_TABLE_OUTPUT
-----------------------------------------------
Plan hash value: 3617692013

-----------------------------------------------
| Id | Operation | Name | Cost (%CPU)|
-----------------------------------------------
| 0 | SELECT STATEMENT | | 68 (0)|
|* 1 | TABLE ACCESS FULL| T1 | 68 (0)|
-----------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("ID">0)

On pourrait s’attendre à avoir un filtre du genre "ID"=1 dans ce cas par exemple. Et bien non, puisque de toute façon toutes les valeurs (la contrainte est validée et immédiate) vérifient cette condition, Oracle choisit de ne pas pousser le prédicat. Ca ne changera pas le plan de toute façon…

Conditions contradictoires

En revanche si la condition de recherche est contradictoire avec la clause CHECK, Oracle saura en tirer partie ; en témoigne la condition NULL IS NOT NULL dans le plan ci-dessous qui ne sera même pas exécuté si nécessaire :

explain plan for 
select *
from T1
where id=2;

select *
from table(
dbms_xplan.display(
format=>'basic +cost +predicate +note'));

PLAN_TABLE_OUTPUT
---------------------------------------------------
Plan hash value: 3332582666

------------------------------------------------
| Id | Operation | Name | Cost (%CPU)|
------------------------------------------------
| 0 | SELECT STATEMENT | | 0 (0)|
|* 1 | FILTER | | |
|* 2 | TABLE ACCESS FULL| T1 | 68 (0)|
------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - filter(NULL IS NOT NULL)
2 - filter("ID"=2)

Propagation de la contrainte par transitivité

En revanche, si la contrainte peut être utile, par transitivité par exemple, Oracle sait en tirer partie, comme vous pouvez vous en rendre compte à l’étape 2 du plan suivant :

explain plan for 
select *
from T1
where id=id2;

select *
from table(
dbms_xplan.display(
format=>'basic +cost +predicate +note'));

PLAN_TABLE_OUTPUT
-----------------------------------------------------------
Plan hash value: 2649869177

-----------------------------------------------------------
| Id | Operation | Name | Cost (%CPU)|
-----------------------------------------------------------
| 0 | SELECT STATEMENT | | 2 (0)|
|* 1 | TABLE ACCESS BY INDEX ROWID| T1 | 2 (0)|
|* 2 | INDEX RANGE SCAN | T1_ID2 | 1 (0)|
-----------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("ID"="ID2")
2 - access("ID2"=1)

Le schéma SYS n’obéit pas aux mêmes règles

Pour terminer, il est intéressant de noter que le schéma SYS n’obéit pas aux mêmes règles, en effet, si vous supprimez le schéma DEMO et reproduisez votre table dans SYS comme ci-dessous :

connect / as sysdba

drop user demo cascade;

drop table T1 purge;
create table t1 (id number,
id2 number,
lib varchar2(100));

insert into T1
(select 1, rownum, rpad(to_char(rownum),100) from dual connect by level <=10000);

alter table T1 add constraint T1_IDIS1 check (id=1);

commit;

create index T1_ID2 on T1(ID2);

exec dbms_stats.gather_table_stats(user,'T1')

Vous constaterez que la condition de la contrainte check n’est pas propagée par transitivité et que ce qui aurait pu être traité par un index rapidement effectue un accès FULL à la table créée :

explain plan for 
select *
from T1
where id=id2;

select *
from table(
dbms_xplan.display(
format=>'basic +cost +predicate +note'));

PLAN_TABLE_OUTPUT
---------------------------------------------------
Plan hash value: 3617692013

-----------------------------------------------
| Id | Operation | Name | Cost (%CPU)|
-----------------------------------------------
| 0 | SELECT STATEMENT | | 44 (0)|
|* 1 | TABLE ACCESS FULL| T1 | 44 (0)|
------------- ----------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("ID"="ID2")

Remarquez que c’est également le cas en cas de conditions contradictoires :

explain plan for 
select *
from T1
where id=2;

select *
from table(
dbms_xplan.display(
format=>'basic +cost +predicate +note'));

PLAN_TABLE_OUTPUT
---------------------------------------------------
Plan hash value: 3617692013

-----------------------------------------------
| Id | Operation | Name | Cost (%CPU)|
-----------------------------------------------
| 0 | SELECT STATEMENT | | 2 (0)|
|* 1 | TABLE ACCESS FULL| T1 | 2 (0)|
-----------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - filter("ID"=2)

Conclusion

Pour ce qui est du dernier exemple, il ne s’agit pas d’un bug, c’est bien voulu ! Ces petits tests montrent, une fois encore l’importance d’utiliser des contraintes. Il montre également que de réaliser des tests dans le schéma SYS peut être une mauvaise idée.

Pour terminer, supprimez la table exemple :

drop table T1 purge;