Vous êtes sur la page 1sur 207

ÉD

ITI
ON
FR
AN
ÇA
ISE

SQL :
AU CŒUR DES
PERFORMANCES
COUVRE TOUTES LES BASES DE DONNÉES MAJEURES

TOUT CE QUE LES DÉVELOPPEURS DOIVENT SAVOIR À PROPOS


DES PERFORMANCES EN SQL

MARKUS WINAND
Accord de licence
Ce livre numérique est destiné à un usage personnel. Ce livre numérique
ne peut ni être revendu ni transmis à un tiers. Si vous souhaitez mettre ce
livre numérique à la disposition d’un tiers, procurez-vous alors une copie
supplémentaire pour chaque personne supplémentaire. Si vous lisez ce
livre numérique et qu’il n’a pas été acheté pour vous-même, veuillez en
commander votre propre exemplaire sur le lien suivant :
http://sql-au-coeur-des-performances.fr/
Prière de respecter les longs travaux de l’auteur.

Cette copie appartient à :


scscscscsc <scscscscscscscsc@rhyta.com>
Editeur (Medieninhaber/Verleger) :
Markus Winand

Maderspergerstasse 1-3/9/11
1160 Wien
AUSTRIA
<office@winand.at>

Copyright © 2013 Markus Winand

La présente publication est protégée par les droits d’auteur. Tous droits
réservés.

Malgré tous les soins apportés à la rédaction du texte, à la production des


images et des programmes utilisés, l’éditeur et l’auteur déclinent toute
responsabilité juridique ou financière pour toute erreur qui aurait pu être
commise et les conséquences qui pourraient en découler.

Les noms, noms commerciaux, désignations de produits, etc. mentionnés


dans cet ouvrage peuvent être des marques protégées par la loi sans que
ceci soit clairement indiqué.

Ce livre reflète exclusivement l’opinion de son auteur. Les producteurs de


bases de données qui y sont mentionnés n’ont pas financé la réalisation de
cet ouvrage et n’en ont pas contrôlé le contenu.

Conception de la couverture :
tomasio.design — Mag. Thomas Weninger — Wien — Austria
Photo couverture : Brian Arnold — Turriff — UK
Traduction française :
Guillaume Lelarge — Longjumeau — France
Titre original :
SQL Performance Explained
Relecture :
Audrey Chiron — Vienne — Autriche
SQL : Au cœur
des performances

Tout ce que les développeurs doivent savoir


à propos des performances en SQL

Markus Winand
Vienne, Autriche
Table des matières
Préface ............................................................................................ vi

1. Anatomie d’un index ...................................................................... 1


Les nœuds feuilles d’un index ...................................................... 2
L’arbre de recherche (B-Tree) ....................................................... 4
Index lents, partie I ..................................................................... 6

2. La clause Where ............................................................................ 9


L’opérateur d’égalité .................................................................... 9
Clés primaires ..................................................................... 10
Index concaténés ................................................................. 12
Index lents, partie II ............................................................. 18
Fonctions .................................................................................. 24
Recherche insensible à la casse en utilisant UPPER ou LOWER ....... 24
Fonctions définies par l’utilisateur ........................................ 29
Sur-indexation ..................................................................... 31
Requêtes avec paramètres .......................................................... 32
Rechercher des intervalles .......................................................... 39
Plus grand, plus petit et entre .............................................. 39
Indexer les filtres LIKE .......................................................... 45
Fusion d’index ..................................................................... 49
Index partiels ............................................................................. 51
NULL dans la base de données Oracle ........................................... 53
Indexer NULL ........................................................................ 54
Contraintes NOT NULL ............................................................ 56
Émuler des index partiels ..................................................... 60
Conditions cachées .................................................................... 62
Types date .......................................................................... 62
Chaînes numériques ............................................................ 68
Combiner des colonnes ........................................................ 70
Logique intelligente ............................................................. 72
Mathématiques ................................................................... 77

iv
SQL : Au cœur des performances

3. Performance et scalabilité ............................................................ 79


L’impact du volume de données sur les performances .................. 80
Impact de la charge système sur les performances ....................... 85
Temps de réponse et bande passante .......................................... 87

4. Opération de jointure ................................................................... 91


Boucles imbriquées .................................................................... 92
Jointure par hachage ................................................................ 101
Fusion par tri ........................................................................... 109

5. Regrouper les données ................................................................ 111


Prédicats de filtre utilisés intentionnellement sur des index ......... 112
Parcours d’index seul ................................................................ 116
Tables organisées en index ........................................................ 122

6. Trier et grouper .......................................................................... 129


Indexer un tri .......................................................................... 130
Indexer ASC, DESC et NULLS FIRST/LAST .......................................... 134
Indexer le « Group By » ............................................................ 139

7. Résultats partiels ........................................................................ 143


Récupérer les N premières lignes .............................................. 143
Parcourir les résultats .............................................................. 147
Utiliser les fonctions de fenêtrage pour une pagination ............... 156

8. Modifier les données .................................................................. 159


Insertion .................................................................................. 159
Suppression ............................................................................. 162
Mise à jour .............................................................................. 163

A. Plans d’exécution ....................................................................... 165


Oracle Database ....................................................................... 166
PostgreSQL ............................................................................... 172
SQL Server ............................................................................... 180
MySQL ..................................................................................... 188

Index ............................................................................................. 193

v
Préface

Les développeurs
ont besoin d’indexer
Les problèmes de performances en SQL sont aussi vieux que le langage SQL
lui-même. Certains n’hésitent pas à dire que SQL est intrinsèquement lent.
Même s’il a pu y avoir une part de vérité au tout début du SQL, ce n’est
plus du tout vrai. Et pourtant, les problèmes de performances en SQL sont
toujours d’actualité. Comment cela est-il possible ?

Le langage SQL est certainement le langage de programmation de qua-


trième génération ayant le plus de succès. Son principal intérêt se révèle
dans sa capacité à séparer le « quoi » et le « comment ». Une requête
SQL décrit le besoin sans donner d’instructions sur la manière de l’obtenir.
Prenez cet exemple :

SELECT date_de_naissance
FROM employes
WHERE nom = 'WINAND'

La requête SQL se lit comme une phrase écrite en anglais qui explique
les données réclamées. Écrire des requêtes SQL ne demande aucune
connaissance sur le fonctionnement interne du système de base de données
ou du système de stockage (disques, fichiers, etc.) Il n’est pas nécessaire
d’indiquer à la base de données les fichiers à ouvrir ou la manière de
trouver les lignes demandées. Beaucoup de développeurs ayant des années
d’expérience avec SQL n’ont que très peu de connaissance sur le traitement
qui s’effectue au niveau du système de bases de données.

La séparation du besoin et de la façon de l’obtenir fonctionne très bien


en SQL mais cela ne veut pas dire pour autant que tout est parfait.
Cette abstraction atteint ses limites quand des performances importantes
sont attendues : la personne ayant écrit une requête SQL n’accorde pas
d’importance sur la façon dont la base de données exécute la requête. En
conséquence, cette personne ne se sent pas responsable d’une exécution
lente. L’expérience montre le contraire. Afin d’éviter des problèmes de
performance, cette personne doit connaître un peu le système de bases de
données.

vi
Préface : Les développeurs ont besoin d’indexer

Il s’avère que la seule chose que les développeurs doivent connaître est
l’indexation. En fait, l’indexation d’une base de données est un travail
de développeurs car l’information la plus importante pour une bonne
indexation ne se situe ni au niveau de la configuration du système de
stockage ni dans la configuration du matériel, mais plutôt au niveau
de l’application : « comment l’application cherche ses données ». Cette
information n’est pas facilement accessible aux administrateurs de bases
de données ou aux consultants externes. Il est parfois nécessaire de
récupérer cette information en surveillant l’application. Par contre, les
développeurs ont cette information facilement.

Ce livre couvre tout ce que les développeurs doivent savoir sur les index, et
rien de plus. Pour être plus précis, ce livre couvre seulement le type d’index
le plus important : l’index B-tree.

L’index B-tree fonctionne de façon pratiquement identique sur la plupart


des bases de données. Ce livre utilise principalement la terminologie de
la base de données Oracle, mais se réfère aux termes correspondant aux
autres bases de données lorsque cela est approprié. Des notes fournissent
des informations supplémentaires pour les bases de données MySQL,
PostgreSQL et SQL Server®.

La structure de ce livre est conçue spécifiquement pour les développeurs ; la


plupart des chapitres correspond à une partie distincte d’une requête SQL.

CHAPITRE 1 - Anatomie d’un index


Le premier chapitre est le seul qui ne couvre pas SQL. Il se focalise
sur la structure fondamentale d’un index. Une compréhension de la
structure des index est essentielle pour continuer. Ne sous-estimez pas
ce chapitre, il est essentiel !

Bien que ce chapitre soit plutôt court, environ huit pages, vous verrez
après l’avoir parcouru que vous comprendrez mieux le phénomène des
index lents.

CHAPITRE 2 - La clause Where


Ce chapitre explique tous les aspects de la clause where, des recherches
simples sur une seule colonne aux recherches complexes avec des
intervalles et des cas spéciaux comme LIKE.

Ce chapitre est le cœur de ce livre. Une fois que vous aurez appris à
utiliser ces techniques, vous devriez écrire des requêtes SQL bien plus
performantes.

vii
Préface : Les développeurs ont besoin d’indexer

CHAPITRE 3 - Performance et scalabilité


Ce chapitre est une petite digression sur les mesures des performances
et sur la scalabilité des bases de données. Cela vous permettra de
comprendre pourquoi ajouter plus de matériel n’est pas la meilleure
solution aux requêtes lentes.

CHAPITRE 4 - L’opération de jointure


Retour au SQL : ici, vous trouverez une explication sur l’utilisation des
index pour obtenir des jointures de tables rapides.

CHAPITRE 5 - Regrouper les données


Vous êtes-vous déjà demandé s’il y avait une différence entre sélec-
tionner une seule colonne ou toutes les colonnes ? Ce chapitre vous
apportera cette réponse, ainsi qu’une astuce pour obtenir encore plus
de performances.

CHAPITRE 6 - Trier et grouper


Même order by et group by peuvent utiliser des index.

CHAPITRE 7 - Résultats partiels


Ce chapitre explique comment bénéficier d’une exécution sérialisée si
vous n’avez pas besoin de l’ensemble complet des résultats.

CHAPITRE 8 - INSERT, DELETE et UPDATE


Comment les index influencent-ils les performances en écriture ?
Les index ne sont pas gratuits, utilisez-les avec précaution !

ANNEXE A - Plans d’exécution


Demandez à la base de données comment elle exécute une requête.

viii
Chapitre 1

Anatomie d’un index

« Un index accélère la requête » est l’explication la plus simple que j’ai


pu voir pour un index. Bien qu’elle décrive parfaitement l’aspect le plus
important d’un index, cela n’est malheureusement pas suffisant pour
ce livre. Ce chapitre décrit la structure d’un index d’une façon moins
superficielle, mais sans trop entrer dans les détails. Il fournit suffisamment
d’informations pour comprendre tous les aspects relatifs aux performances
en SQL, aspects qui seront détaillés tout au long de ce livre.

Un index est une structure séparée dans la base de données, construite


en utilisant l’instruction create index. Il nécessite son propre espace sur
le disque et détient une copie des données de la table. Cela signifie qu’un
index est une redondance. Créer un index ne modifie pas les données de
la table. Cela crée une nouvelle structure de données faisant référence à
la table. En fait, un index de base de données ressemble très fortement à
l’index d’un livre : il occupe de la place, il est redondant et il fait référence
aux informations réelles stockées ailleurs.

Index clusterisés
SQL Server et MySQL (en utilisant InnoDB) ont une vue plus large de
ce qu’est un « index ». Ils font références à des tables qui ont une
structure d’index en tant que index clusterisé. Ces tables sont appelées
des tables organisées en index (en anglais, « Index-Organized Tables »
ou IOT) dans la base de données Oracle.
Chapitre 5, « Regrouper les données » les décrit avec plus de détails et
explique leur avantages et inconvénients.

Rechercher dans un index de base de données ressemble à rechercher


dans un annuaire téléphonique. L’idée est que toutes les informations
sont rangées dans un ordre bien défini. Trouver des informations dans un
ensemble de données triées est rapide et simple car l’ordre de tri détermine
la position de chaque donnée.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 1


chapitre 1 : Anatomie d’un index

Néanmoins, un index de base de données est plus complexe qu’un annuaire


car l’index est constamment modifié. Mettre à jour un annuaire imprimé
pour chaque changement est impossible car il n’y a pas d’espace entre
chaque information pour en ajouter de nouvelles. Un annuaire imprimé
contourne ce problème en gérant des mises à jours accumulées lors
de la prochaine impression. Une base de données ne peut pas attendre
aussi longtemps. Elle doit traiter les commandes INSERT, DELETE et UPDATE
immédiatement, tout en conservant l’ordre de l’index sans déplacer de gros
volumes de données.

La base de données combine deux structures de données pour parvenir à


ce résultat : une liste doublement chaînée et un arbre de recherche. Ces
deux structures expliquent la plupart des caractéristiques de performances
des bases de données.

Les nœuds feuilles d’un index


Le but principal d’un index est de fournir une représentation ordonnée des
données indexées. Néanmoins, il n’est pas possible de stocker les données
séquentiellement car une commande INSERT nécessiterait le déplacement
des données suivantes pour faire de la place à la nouvelle donnée. Déplacer
de gros volumes de données demande beaucoup de temps, ce qui causerait
des lenteurs importantes pour une commande INSERT. La solution à ce
problème revient à établir un ordre logique qui est indépendant de l’ordre
physique en mémoire.

L’ordre logique est établi grâce à une liste doublement chaînée. Chaque
nœud a un lien vers les deux nœuds voisins, un peu comme une chaîne.
Les nouveaux nœuds sont insérés entre deux nœuds existants en mettant à
jour leurs liens pour référencer le nouveau nœud. L’emplacement physique
du nouveau nœud n’a aucune importance car la liste doublement chaînée
maintient l’ordre logique.

Cette structure de données est appelée une liste doublement chaînée car
chaque nœud fait référence aux nœuds précédent et suivant. La base de
données peut ainsi lire l’index en avant et en arrière suivant le besoin. Il
est du coup possible d’insérer de nouvelles entrées sans déplacer de gros
volumes de données, seuls les pointeurs sont changés.

Les listes doublement chaînées sont aussi utilisées pour les collections
(conteneurs) dans de nombreux langages de développement.

2 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Les nœuds feuilles d’un index

Langage de programmation Nom


Java java.util.LinkedList
Framework .NET System.Collections.Generic.LinkedList
C++ std::list

Les bases de données utilisent des listes doublement chaînées pour con-
necter des nœuds feuilles d’index. Chaque nœud feuille est stocké dans un
bloc de la base de données (aussi appelé page), autrement dit, la plus petite
unité de stockage de la base de données. Tous les blocs d’index sont de la
même taille, généralement quelques kilo-octets. La base de données utilise
l’espace dans chaque bloc du mieux possible, et stocke autant d’entrées
d’index que possible dans chaque bloc. Cela signifie que l’ordre de l’index
est maintenu sur deux niveaux différents : les enregistrements de l’index
à l’intérieur de chaque nœud feuille, et les nœuds feuilles entre elles en
utilisant une liste doublement chaînée.

Figure 1.1. Nœuds feuilles de l’index et données de la table


Nœuds feuilles de l'index Table
(triés) (non trié)
2

1
lo 2
lo 3

4
e

e
co nne
co nne

e
nn

nn

nn
D
lo

WI

lo
lo
co

RO

co
co

11 3C AF A 34 1 2
13 F3 91 A 27 5 9
18 6F B2
A 39 2 5
X 21 7 2
21 2C 50
27 0F 1B A 11 1 6
27 52 55
A 35 8 3
X 27 3 2
34 0D 1E
35 44 53 A 18 3 6
39 24 5D A 13 7 4

La Figure 1.1 illustre les nœuds feuilles de l’index et leur connexion


aux données de la table. Chaque enregistrement de l’index consiste en
des colonnes indexées (la clé, la colonne 2) et fait référence à la ligne
correspondante dans la table (via ROWID ou RID). Contrairement à l’index, les
données de la table sont stockées dans une structure appelée heap et n’est
pas du tout triée. Il n’existe aucune relation entre les lignes stockées dans
le même bloc de table, pas plus qu’il n’y a de connexions entre les blocs.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 3


chapitre 1 : Anatomie d’un index

L’arbre de recherche (B-Tree)


Les nœuds feuilles de l’index sont stockés dans un ordre arbitraire en ce
sens que la position sur le disque ne correspond pas à la position logique
suivant l’ordre de l’index. C’est comme un annuaire téléphonique avec les
pages mélangées. Si vous recherchez « Dupont » mais que vous ouvrez
l’annuaire à « Canet », il n’est pas garanti que Dupont suive Canet. Une base
de données a besoin d’une deuxième structure pour trouver rapidement
une donnée parmi des pages mélangées : un arbre de recherche équilibré (en
anglais, un « Balanced Tree Search ») ou, plus court, un B-tree.

Figure 1.2. La structure du B-tree


Nœud branche Nœuds feuilles

eu s
e
ud nch

s
ille
ne

a
Nœ aci

Nœ br
40 4A 1B

sf
r

s
ud

ud

43 9F 71
46 A2 D2 11 3C AF
13 F3 91
18 6F B2

21 2C 50
18 27 0F 1B
27 27 52 55
39

46 8B 1C 34 0D 1E
35 44 53
39 24 5D

53 A0 A1 40 4A 1B

53 0D 79
43 9F 71
46 A2 D2

46 46 8B 1C
53 A0 A1
53 0D 79

53 39
46
53
57 55 9C F6

57 55 9C F6 83
98
83 57 B1 C1
57 50 29

83 57 B1 C1 67 C4 6B
83 FF 9D
83 AF E9

57 50 29 84 80 64
86 4C 2F
88 06 5B

89 6A 3E
88 90 7D 9A

67 C4 6B
94 94 36 D4
98

95 EA 37

83 FF 9D 98 5E B2
98 D8 4F

83 AF E9

La Figure 1.2 montre un exemple d’index avec 30 entrées. La liste double-


ment chaînée établit l’ordre logique entre les nœuds feuilles. La racine
et les nœuds branches permettent une recherche rapide parmi les nœuds
feuilles.

Le graphique distingue le nœud branche et les nœuds feuilles auxquels il


fait référence. Chaque entrée de nœud branche correspond à la valeur la
plus grosse dans le nœud feuille référencé. Par exemple, la valeur 46 dans

4 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


L’arbre de recherche (B-Tree)

le premier nœud feuille pour que la première entrée du nœud branche


soit aussi 46. Ceci est toujours vrai pour les autres nœuds feuilles. À la
fin, le nœud branche a les valeurs 46, 53, 57 et 83. Suivant cette méthode,
un niveau de branche est construit jusqu’à ce que tous les nœuds feuilles
soient couverts par un nœud branche.

Le prochain niveau est construit de façon similaire, mais au-dessus du


premier niveau de branche. La procédure se répète jusqu’à ce que toutes
les clés remplissent un nœud seul, le nœud racine. La structure est un
arbre de recherche équilibré car la profondeur de l’arbre est identique à
chaque position. La distance entre le nœud racine et les nœuds feuilles est
identique partout.

Note
Un B-tree est un arbre équilibré — pas un arbre binaire.

Une fois créée, la base de données maintient automatiquement l’index. Elle


applique chaque commande INSERT, DELETE et UPDATE à l’index et conserve la
propriété équilibrée de l’arbre, ce qui cause une surcharge de maintenance
pour les opérations d’écriture. Le Chapitre 8, « Modifier les données »
explique cela en détails.

Figure 1.3. Parcours d’un B-Tree

46 8B 1C
53 A0 A1
46 53 0D 79
39
53
83
57
98 55 9C F6
83
57 B1 C1
57 50 29

La Figure 1.3 montre un fragment d’index pour illustrer une recherche sur
la clé 57. Le parcours de l’arbre commence au nœud racine du côté gauche.
Chaque entrée est traitée dans l’ordre ascendant jusqu’à ce qu’une valeur
identique ou plus grande (>=) que la valeur recherchée (57) soit trouvée.
Dans ce graphique, il s’agit de la valeur 83. La base de données suit la
référence au nœud branche correspondant et répète la même procédure
jusqu’à ce que le parcours atteigne un nœud feuille.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 5


chapitre 1 : Anatomie d’un index

Important
L’index B-tree permet de trouver un nœud feuille rapidement.

Le parcours de l’arbre est une opération très efficace. C’est même si


efficace que j’en parle comme de la première puissance de l’indexation. C’est
presque instantané, y compris sur de très gros volumes de données. Ceci
est principalement dû à la propriété équilibrée d’un arbre, ce qui permet
d’accéder à tous les éléments avec le même nombre d’étapes, mais aussi au
grossissement logarithmique de la profondeur de l’arbre. Cela signifie que
la profondeur de l’arbre grossit très lentement en comparaison au nombre
de nœuds feuilles. De vrais index avec des millions d’enregistrements ont
une profondeur d’arbre de quatre ou cinq. Il est très rare de rencontrer
une profondeur de cinq ou six dans un arbre. L’encart « Complexité
logarithmique » décrit cela en détails.

Index lents, partie I


Malgré l’efficacité du parcours de l’arbre, il existe des cas où une recherche
via l’index ne sera pas aussi rapide que souhaité. Cette contradiction est
la raison d’être du mythe de l’index dégénéré. Le mythe affirme qu’une
reconstruction d’index est la solution miracle. La vraie raison pour laquelle
des requêtes basiques peuvent être lentes, même en utilisant un index, peut
se trouver sur les bases des sections précédentes.

Le premier ingrédient rendant une recherche lente via l’index est la chaîne
de nœuds feuilles. Considérez de nouveau la recherche de la valeur 57 dans
la Figure 1.3. Il existe deux entrées correspondantes dans l’index. Au moins
deux entrées sont identiques, pour être plus précis : le nœud feuille suivant
pourrait contenir de nouvelles entrées 57. La base de données doit lire le
prochain nœud feuille pour savoir s’il existe des entrées correspondantes.
Cela signifie qu’une recherche d’index doit non seulement faire le parcours
de l’arbre, mais il doit aussi suivre la chaîne des nœuds feuilles.

Le deuxième ingrédient pour une recherche d’index lente est d’avoir à


accéder à la table. Même un nœud feuille simple peut contenir plusieurs
fois la valeur recherchée, parfois même des centaines de fois. Les données
correspondantes de la table sont généralement réparties sur un grand
nombre de blocs (voir la Figure 1.1, « Nœuds feuilles de l’index et données
de la table »). Cela signifie qu’il y a un accès supplémentaire à la table pour
chaque valeur trouvée dans l’index.

6 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Index lents, partie I

Complexité logarithmique
En mathématique, le logarithme d’un nombre sur une base donnée
est la puissance ou l’exposant avec laquelle la base doit être élevée
1
pour produire le résultat [Wikipedia ].
Dans un arbre de recherche, la base correspond au nombre d’entrées
par nœud branche et l’exposant à la profondeur de l’arbre. L’index
en exemple dans Figure 1.2 contient jusqu’à quatre entrées par nœud
et a une profondeur d’arbre de 3. Cela signifie que l’index peut
3
contenir jusqu’à 64 entrées (4 ). S’il grossit d’un niveau, il peut déjà
4
contenir 256 entrées (4 ). À chaque ajout d’un niveau, le nombre
maximum d’entrées quadruple. Le logarithme inverse cette fonction.
La profondeur de l’arbre est donc log4(nombre-d-entrées-index).
L’augmentation logarithmique permet
Profondeur Entrées
de rechercher parmi un million d’en-
de l’arbre d’index
trées dans dix niveaux de l’arbre, mais
un index réel est encore plus efficace. 3 64
Le facteur principal qui influe sur la 4 256
profondeur de l’arbre, et du coup ces 5 1,024
performances, est le nombre d’entrées 6 4,096
dans chaque nœud de l’arbre. Ce 7 16,384
nombre correspond, mathématique- 8 65,536
ment, à la base du logarithme. Plus
9 262,144
grande est la base, plus large sera
10 1,048,576
l’arbre et plus rapide sera le parcours.
Les bases de données exploitent ce concept le plus possible et
placent autant d’entrées que possible dans chaque nœud, souvent des
centaines. Cela signifie que chaque nouveau niveau d’index supporte
une centaine de fois plus d’entrées.

1
https://fr.wikipedia.org/wiki/Logarithme

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 7


chapitre 1 : Anatomie d’un index

Une recherche dans un index suit trois étapes : (1) le parcours de l’arbre ;
(2) la suite de la chaîne de nœuds feuilles ; (3) la récupération des données
de la table. Le parcours de l’arbre est la seule étape qui accède à un nombre
limité de blocs, qui correspond à la profondeur de l’index. Les deux autres
étapes doivent avoir accès à de nombreux blocs. Elles sont la cause des
lenteurs lors d’une recherche par index.

L’origine du mythe des index lents est la croyance erronée qu’une recherche
d’index ne fait que parcourir l’arbre, et donc l’idée qu’un index lent est
causé par un arbre « cassé » ou « non équilibré ». En fait, vous pouvez
demander à la plupart des bases de données comment elles utilisent un
index. La base de données Oracle est assez verbeuse sur cet aspect. Elle a
trois opérations distinctes qui décrivent une recherche basique par index :

INDEX UNIQUE SCAN


Un INDEX UNIQUE SCAN réalise seulement le parcours de l’arbre. La base
de données Oracle utilise cette opération sur une contrainte unique qui
assure que le critère de recherche correspondra à une seule entrée.

INDEX RANGE SCAN


Un INDEX RANGE SCAN fait le parcours de l’arbre et suit la chaîne de
nœuds feuilles pour trouver toutes les entrées correspondantes. C’est
l’opération la plus sûre si le critère peut se révéler vrai pour plusieurs
entrées.

TABLE ACCESS BY INDEX ROWID


Une opération TABLE ACCESS BY INDEX ROWID récupère la ligne de la table.
Cette opération est (souvent) réalisée pour chaque enregistrement
récupéré à partir d’un précédent parcours d’index.

Le point important est qu’un INDEX RANGE SCAN peut potentiellement lire
une large part d’un index. S’il existe plus d’un accès de table pour chaque
ligne, la requête peut devenir lente même en utilisant un index.

8 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Chapitre 2

La clause Where
Le chapitre précédent a décrit la structure des index et a expliqué la cause
des mauvaises performances avec les index. Dans ce nouveau chapitre,
nous apprendrons comment trouver et éviter ces problèmes dans les
requêtes SQL. Nous allons commencer en regardant la clause where.

La clause where définit la condition de recherche d’une requête SQL et, de ce


fait, elle tombe dans le domaine fonctionnel principal d’un index : trouver
des données rapidement. Bien que la clause where ait un impact important
sur les performances, elle est souvent mal écrite, si bien que la base de
données doit parcourir une grande partie de l’index. Le résultat : une clause
where mal écrite est la première raison d’une requête lente.

Ce chapitre explique comment les différents opérateurs affectent


l’utilisation d’un index et comment s’assurer qu’un index est utilisable
pour autant de requêtes que possible. La dernière section montre des
contre-exemples et proposent des alternatives qui donnent de meilleures
performances.

L’opérateur d’égalité
L’opérateur d’égalité est l’opérateur SQL le plus trivial et le plus fréquem-
ment utilisé. Les erreurs d’indexation qui affectent les performances sont
toujours très communes et les clauses where qui combinent plusieurs
conditions sont particulièrement vulnérables.

Cette section montre comment vérifier l’utilisation de l’index et


explique comment les index concaténés peuvent optimiser les conditions
combinées. Pour faciliter la compréhension, nous allons analyser une
requête lente pour voir l’impact réel des causes expliquées dans le
Chapitre 1.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 9


chapitre 2 : La clause Where

Clés primaires
Nous commençons avec la clause where la plus simple et la plus commune :
la recherche d’une clé primaire. Pour les exemples de ce chapitre, nous
utilisons la table EMPLOYES qui se définie ainsi :

CREATE TABLE employes (


id_employe NUMBER NOT NULL,
prenom VARCHAR2(1000) NOT NULL,
nom VARCHAR2(1000) NOT NULL,
date_de_naissance DATE NOT NULL,
numero_telephone VARCHAR2(1000) NOT NULL,
CONSTRAINT employes_pk PRIMARY KEY (id_employe)
)

La base de données crée automatiquement un index pour la clé primaire.


Cela signifie qu’il existe un index sur la colonne ID_EMPLOYE, même si nous
n’avons pas exécuté de commande create index.

La requête suivante utilise la clé primaire pour récupérer le nom d’un


employé :

SELECT prenom, nom


FROM employes
WHERE id_employe = 123

La clause where ne peut pas correspondre à plusieurs enregistrements car


la clé primaire assure l’unicité des valeurs de la colonne ID_EMPLOYE. La base
de données n’a pas besoin de suivre les nœuds feuilles de l’index. Il suffit de
parcourir l’arbre de l’index. Nous pouvons utiliser le plan d’exécution pour
vérifier :

--------------------------------------------------------------
|Id |Operation | Name | Rows | Cost |
--------------------------------------------------------------
| 0 |SELECT STATEMENT | | 1 | 2 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES | 1 | 2 |
|*2 | INDEX UNIQUE SCAN | EMPLOYES_PK | 1 | 1 |
--------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("ID_EMPLOYE"=123)

10 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Clés primaires

Le plan d’exécution Oracle affiche un INDEX UNIQUE SCAN, ce qui corres-


pond à l’opération qui ne fait que parcourir l’arbre de l’index. Il utilise
complètement la complexité logarithmique de l’index pour trouver l’entrée
très rapidement, pratiquement indépendamment de la taille de la table.

Astuce
Le plan d’exécution (quelque fois appelé plan explain ou plan de la
requête) montre les étapes réalisées par la base pour exécuter une
requête SQL. L’Annexe A à la page 165 explique comment récupérer
et lire les plans d’exécution avec d’autres bases de données.

Après avoir accédé à l’index, la base de données doit réaliser une autre
étape pour récupérer les données demandées (PRENOM, NOM) à partir du
stockage de la table : il s’agit de l’opération TABLE ACCESS BY INDEX ROWID.
Cette opération peut devenir un goulet d’étranglement, comme expliqué
dans la « Index lents, partie I », mais ce risque est inexistant avec un
INDEX UNIQUE SCAN. Cette opération ne peut pas renvoyer plus d’une entrée
donc elle ne peut pas déclencher plus d’un accès à la table. Cela signifie
que les ingrédients d’une requête lente ne sont pas présents avec un
INDEX UNIQUE SCAN.

Clés primaires sans index unique


Une clé primaire n’a pas nécessairement besoin d’un index unique.
Vous pouvez aussi utiliser un index non unique. Dans ce cas, la
base de données Oracle n’utilise pas l’opération INDEX UNIQUE SCAN
mais utilise à la place l’opération INDEX RANGE SCAN. Néanmoins, la
contrainte maintiendra l’unicité des clés pour que la recherche dans
l’index renvoie au plus une entrée.
Les contraintes différables sont une des raisons pour utiliser des index
non uniques pour les clés primaires. En opposition aux contraintes
standards, qui sont validées lors de l’exécution de la requête, la
base de données repousse la validation des contraintes différables
jusqu’au moment où la transaction est validée. Les contraintes
différées sont requises pour insérer des données dans des tables ayant
des dépendances circulaires.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 11


chapitre 2 : La clause Where

Index concaténés
Même si la base de données crée automatiquement l’index pour la clé
primaire, il est toujours possible de réaliser des améliorations manuelles si
la clé contient plusieurs colonnes. Dans ce cas, la base de données crée un
index sur toutes les colonnes de la clé primaire, ce qu’on appelle un index
concaténé (aussi connu sous le nom d’index multi-colonnes, composite ou
combiné). Notez que l’ordre des colonnes d’un index concaténé a un grand
impact sur ses capacités à être utilisé. Donc l’ordre des colonnes doit être
choisi avec attention.

Pour les besoins de la démonstration, supposons qu’il y ait une fusion de


sociétés. Les employés de l’autre société sont ajoutés à notre table EMPLOYES
qui devient alors dix fois plus grosses. Il y a ici un problème : la colonne
ID_EMPLOYE n’est pas unique entre les deux sociétés. Nous avons besoin
d’étendre la clé primaire par un identifiant supplémentaire. Du coup, la
nouvelle clé primaire a deux colonnes : la colonne ID_EMPLOYE comme
auparavant et la colonne ID_SUPPLEMENTAIRE pour rétablir l’unicité.

L’index pour la nouvelle clé primaire est donc défini de la façon suivante :

CREATE UNIQUE INDEX employe_pk


ON employes (id_employe, id_supplementaire);

Une requête pour un employé en particulier doit prendre en compte la clé


primaire complète. Autrement dit, la colonne ID_SUPPLEMENTAIRE doit aussi
être utilisée :

SELECT prenom, nom


FROM employes
WHERE id_employe = 123
AND id_supplementaire = 30
--------------------------------------------------------------
|Id |Operation | Name | Rows | Cost |
--------------------------------------------------------------
| 0 |SELECT STATEMENT | | 1 | 2 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES | 1 | 2 |
|*2 | INDEX UNIQUE SCAN | EMPLOYES_PK | 1 | 1 |
--------------------------------------------------------------

Predicate Information (identified by operation id):


---------------------------------------------------
2 - access("ID_EMPLOYE"=123 AND "ID_SUPPLEMENTAIRE"=30)

12 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Index concaténés

Quand une requête utilise la clé primaire complète, la base de données


peut utiliser une opération INDEX UNIQUE SCAN, quel que soit le nombre de
colonnes de l’index. Mais qu’arrive-t-il quand seulement une des colonnes
clés, par exemple, est utilisée pour rechercher tous les employés d’une
société ?

SELECT prenom, nom


FROM employes
WHERE id_supplementaire = 20

---------------------------------------------------
| Id | Operation | Name | Rows | Cost |
---------------------------------------------------
| 0 | SELECT STATEMENT | | 106 | 478 |
|* 1 | TABLE ACCESS FULL| EMPLOYES | 106 | 478 |
---------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("ID_SUPPLEMENTAIRE"=20)

Le plan d’exécution révèle que la base de données ne doit pas utiliser l’index.
À la place, il réalise un FULL TABLE SCAN. En résultat, la base de données
lit la table entière et évalue chaque ligne par rapport à la clause where.
La durée d’exécution grandit avec la taille de la table : si la table décuple,
l’opération FULL TABLE SCAN prendra dix fois plus longtemps. Le danger de
cette opération est qu’elle est souvent suffisamment rapide dans un petit
environnement de développement, mais elle cause de sérieux problèmes de
performance en production.

Parcours complet de table


L’opération TABLE ACCESS FULL, aussi connue sous le nom de parcours
complet de table, peut être l’opération la plus efficace dans certains
cas, en particulier quand il faut récupérer une large portion de la
table.
Ceci est dû en partie à la surcharge pour une recherche dans l’index,
ce qui n’arrive pas pour une opération TABLE ACCESS FULL. En fait,
quand une recherche par index lit un bloc après l’autre, la base de
données ne sait pas quel bloc sera lu après tant que le traitement
du bloc courant n’est pas terminé. Un FULL TABLE SCAN doit lire la
table complète de toute façon, donc la base de données peut lire de
plus gros morceaux à la fois (lecture multi-blocs). Bien que la base de
données lise plus de données, il pourrait être nécessaire d’exécuter
moins d’opérations de lecture.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 13


chapitre 2 : La clause Where

La base de données n’utilise pas l’index car elle ne peut pas utiliser une
colonne à partir d’un index concaténé. C’est beaucoup plus clair en faisant
plus attention à la structure de l’index.

Un index concaténé est un index B-tree comme n’importe quel autre, qui
conserve les données indexées dans une liste triée. La base de données
considère chaque colonne suivant sa position dans la définition de l’index
pour trier les enregistrements. La première colonne est le critère principal
de tri et la deuxième colonne détermine l’ordre seulement si deux enregis-
trements ont la même valeur dans la première colonne. Et ainsi de suite.

Important
Un index concaténé est un index sur plusieurs colonnes.

L’ordre d’un index à deux colonnes ressemble à l’ordre d’un annuaire


téléphonique : il est tout d’abord trié par le nom, puis par le prénom. Cela
signifie qu’un index à deux colonnes ne permet pas une recherche sur la
deuxième colonne seule. Cela reviendrait à rechercher dans un annuaire en
ayant seulement le prénom.

Figure 2.1. Index concaténé IR


E

Arbre d'index
TA
EN
YE

EM
E

LO

PL
IR
E

MP

UP
TA
IR

_E

_S
EN
TA

ID

ID
YE

EM
EN

LO

PL
YE

EM

123 20 ROWID
MP

UP
LO

PL

_E

_S
MP

UP

ID

ID

123 21
_E

_S

ROWID
ID

ID

123 18 123 27 ROWID


121 25
123 27
126 30
125 30
131 11 124 10 ROWID
126 30
124 20 ROWID
125 30 ROWID

L’exemple d’index dans la Figure 2.1 montre que les enregistrements pour
l’identifiant supplémentaire 20 ne sont pas stockés les uns à côté des autres.
On remarque également qu’il n’y a pas d’enregistrements pour lesquels
ID_SUPPLEMENTAIRE = 20 dans l’arbre, bien qu’ils existent dans les nœuds
feuilles. Du coup, l’arbre est inutile pour cette requête.

14 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Index concaténés

Astuce
Visualiser un index aide à comprendre les requêtes supportées par cet
index. Vous pouvez exécuter une requête sur la base de données pour
retrouver tous les enregistrements dans l’ordre de l’index (syntaxe
SQL:2008, voir la page 144 pour des solutions propriétaires utilisant
LIMIT, TOP ou ROWNUM) :

SELECT <COLONNES LISTE D'INDEX>


FROM <TABLE>
ORDER BY <COLONNES LISTE D'INDEX>
FETCH FIRST 100 ROWS ONLY;

Si vous placez la définition de l’index et le nom de la table dans la


requête, vous obtiendrez un extrait de l’index. Demandez-vous si les
lignes demandées sont groupées en un point central. Dans le cas
contraire, l’arbre de l’index ne peut pas vous aider à trouver cette
place.

Bien sûr, nous pouvons ajouter un autre index sur ID_SUPPLEMENTAIRE pour
améliorer la rapidité de la requête. Néanmoins, il existe une meilleure
solution, tout du moins si nous supposons que la recherche sur ID_EMPLOYE
seul n’a pas de sens.

Nous pouvons profiter du fait que la première colonne de l’index est


toujours utilisable pour la recherche. Encore une fois, cela fonctionne
comme un annuaire téléphonique : vous n’avez pas besoin de connaître le
prénom pour chercher le nom. L’astuce revient donc à inverser l’ordre des
colonnes de l’index pour que ID_SUPPLEMENTAIRE soit en première position :

CREATE UNIQUE INDEX EMPLOYES_PK


ON EMPLOYES (ID_SUPPLEMENTAIRE, ID_EMPLOYE);

Les deux colonnes ensemble sont toujours uniques. Ainsi les requêtes sur
la clé primaire complète peuvent toujours utiliser un INDEX UNIQUE SCAN
mais la séquence des enregistrements d’index est complètement différente.
La colonne ID_SUPPLEMENTAIRE est devenue le critère principal de tri. Cela
signifie que tous les enregistrements pour une société sont consécutifs
dans l’index, et la base de données peut utiliser l’index B-tree pour trouver
leur emplacement.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 15


chapitre 2 : La clause Where

Important
Le point le plus important à considérer lors de la définition d’un
index concaténé est le choix de l’ordre des colonnes pour qu’il puisse
supporter autant de requêtes SQL que possible.

Le plan d’exécution confirme que la base de données utilise l’index


inversé. La colonne ID_SUPPLEMENTAIRE seule n’est plus unique, donc
la base de données doit suivre les nœuds feuilles pour trouver tous
les enregistrements correspondants : du coup, il utilise une opération
INDEX RANGE SCAN.

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

Predicate Information (identified by operation id):


---------------------------------------------------
2 - access("ID_SUPPLEMENTAIRE"=20)

En général, une base de données peut utiliser un index concaténé lors de


la recherche des colonnes les plus à gauche. Un index à trois colonnes
peut être utilisé pour une recherche sur la première colonne, pour une
recherche sur les deux premières colonnes et pour une recherche sur toutes
les colonnes.

Même si la solution à deux index délivre des performances très bonnes


pour les SELECT, la solution à un seul index est préférable. Non seulement
cela permet d’économiser de l’espace de stockage, mais aussi la surcharge
due à la maintenance pour le deuxième index. Moins une table a d’index,
meilleures sont les performances des commandes INSERT, DELETE et UPDATE.

Pour définir un index optimal, vous devez comprendre un peu plus que le
simple fonctionnement des index. Vous devez également savoir comment
l’application demande les données, c’est-à-dire connaître les combinaisons
de colonnes qui apparaissent dans les clauses WHERE.

16 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Index concaténés

Du coup, définir un index optimal est très difficile pour des consultants
externes car ils n’ont pas de vue globale des chemins d’accès de
l’application. Les consultants peuvent généralement prendre en compte
une seule requête. Ils n’exploitent pas les bénéfices supplémentaires qu’un
index peut fournir pour les autres requêtes. Les administrateurs de bases de
données sont dans une position similaire. Ils peuvent connaître le schéma
de la base de données mais ils n’ont pas de connaissances étendues des
chemins d’accès.

La seule personne qui doit avoir la connaissance technique de la base de


données et la connaissance fonctionnelle du métier est le développeur. Les
développeurs ont une idée des données et connaissent les chemins d’accès
aux données. Ils peuvent indexer les données correctement de telle manière
à obtenir les meilleures performances pour l’application complète sans trop
d’efforts.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 17


chapitre 2 : La clause Where

Index lents, partie II


La section précédente a expliqué comment gagner en performance à
partir d’un index existant en changeant l’ordre de ses colonnes mais
l’exemple portait seulement sur deux requêtes. Néanmoins, changer un
index pourrait affecter toutes les requêtes sur la table indexée. Cette section
explique comment les bases de données choisissent un index et démontre
les effets de bord possibles dû à un changement dans la définition d’un
index.

L’index EMPLOYE_PK adopté améliore les performances de toutes les requêtes


qui cherchent par société seulement. Il est aussi utilisable pour toutes les
requêtes qui cherchent par ID_SUPPLEMENTAIRE et par tout autre critère de
recherche supplémentaire. Cela signifie que l’index devient utilisable pour
les requêtes qui utilisaient un autre index pour une autre partie de la clause
where. Dans ce cas, si plusieurs chemins d’accès sont possibles, c’est à
l’optimiseur de choisir le meilleur.

L’optimiseur de requêtes
L’optimiseur de requêtes, ou le planificateur de requêtes, est le
composant de la base de données qui transforme une requête SQL
en un plan d’exécution. Ce processus est aussi connu sous le nom de
compilation ou analyse. Il existe deux types distincts d’optimiseurs.
Un optimiseur basé sur le coût (CBO) génère un grand nombre de plans
d’exécution et calcule un coût pour chaque plan. Le calcul du coût
est basé sur les opérations utilisées et les estimations de nombres
de lignes. À la fin, la valeur du coût sert d’information de base pour
choisir le « meilleur » plan d’exécution.
Un optimiseur basé sur des règles (RBO) génère le plan d’exécution
utilisant un ensemble de règles codées en dur. Ce type d’optimiseur
est moins flexible et très peu utilisé de nos jours.

Changer un index peut aussi avoir des effets de bord déplaisants. Dans notre
exemple, l’application du répertoire téléphonique est devenue très lente
depuis la fusion. La première analyse a identifié la requête suivante comme
cause principale des lenteurs.

18 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Index lents, partie II

SELECT prenom, nom, id_supplementaire, numero_telephone


FROM employes
WHERE nom = 'WINAND'
AND id_supplementaire = 30

Le plan d’exécution est le suivant :

Exemple 2.1. Plan d’exécution avec l’index de clé primaire revu

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

Predicate Information (identified by operation id):


---------------------------------------------------
1 - filter("NOM"='WINAND')
2 - access("ID_SUPPLEMENTAIRE"=30)

Le plan d’exécution utilise un index. Il a un coût global de 30. Pour


l’instant, tout va bien. Néanmoins, il est étonnant de voir qu’il utilise l’index
que nous venons de changer. C’est une raison suffisante pour suspecter
que le changement d’index a causé le problème de performances, tout
particulièrement si on se rappelle de l’ancienne définition de l’index. Elle
commençait avec la colonne ID_EMPLOYE qui ne fait pas du tout partie de la
clause where. La requête ne pouvait pas utiliser cet index avant.

Pour aller plus loin dans l’analyse, il serait bon de comparer le plan
d’exécution avant et après changement. Pour obtenir le plan d’exécution
original, nous pourrions remettre en place l’ancien index. Néanmoins,
la plupart des bases de données offre un moyen simple pour empêcher
l’utilisation d’un index pour une requête particulière. L’exemple suivant
utilise un marqueur pour l’optimiseur Oracle (appelé « hint ») pour gérer
ce cas.

SELECT /*+ NO_INDEX(EMPLOYES EMPLOYE_PK) */


prenom, nom, id_supplementaire, numero_telephone
FROM employes
WHERE nom = 'WINAND'
AND id_supplementaire = 30

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 19


chapitre 2 : La clause Where

Le plan d’exécution qui était utilisé avant la modification de l’index


n’utilisait pas d’index du tout :

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

Predicate Information (identified by operation id):


---------------------------------------------------
1 - filter("NOM"='WINAND' AND "ID_SUPPLEMENTAIRE"=30)

Même si l’opération TABLE ACCESS FULL doit lire et traiter la table entière,
elle semble plus rapide qu’utiliser l’index dans ce cas. Ceci est plutôt
inhabituel car la requête ne récupère qu’une ligne. Utiliser un index pour
trouver une seule ligne devrait être bien plus rapide qu’un parcours de table
complet. Mais cela ne se passe pas ainsi ici. L’index semble être lent.

Dans ce cas, il est préférable de vérifier chaque étape du plan problé-


matique. La première étape est l’opération INDEX RANGE SCAN sur l’index
EMPLOYEE_PK. Cet index ne couvre pas la colonne NOM. INDEX RANGE SCAN peut
seulement traiter le filtre sur ID_SUPPLEMENTAIRE ; la base de données Oracle
affiche cette information sur le deuxième élément dans la partie « Predicate
Information » du plan d’exécution. Vous pouvez donc voir les conditions
appliquées à chaque opération.

Astuce
L’Annexe A, « Plans d’exécution » explique comment trouver la partie
« Predicate Information » pour chaque base de données.

Le INDEX RANGE SCAN avec l’identifiant d’opération 2 (Exemple 2.1 à la


page 19) applique seulement le filtre ID_SUPPLEMENTAIRE=30. Cela signifie
qu’il parcourt l’arbre de l’index pour trouver le premier enregistrement
pour lequel ID_SUPPLEMENTAIRE vaut 30. Ensuite, il suit la chaîne de
nœuds feuilles pour trouver tous les enregistrements pour cette société.
Le résultat de l’opération INDEX RANGE SCAN est une liste d’identifiants
de lignes (généralement appelés ROWID) qui remplissent la condition
ID_SUPPLEMENTAIRE : suivant la taille de la société, cela peut aller de
quelques-uns à plusieurs centaines.

La prochaine étape est l’opération TABLE ACCESS BY INDEX ROWID. Elle uti-
lise les ROWID trouvés à l’étape précédente pour récupérer les lignes, toutes

20 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Index lents, partie II

colonnes comprises, de la table. Une fois que la colonne NOM est disponible,
la base de données peut évaluer le reste de la clause where. Cela signifie
que la base de données doit récupérer toutes les lignes de la table pour
lesquelles la clause ID_SUPPLEMENTAIRE=30 est vraie avant d’appliquer le
filtre NOM.

La durée d’exécution de la requête ne dépend pas du nombre de lignes du


résultat mais du nombre d’employés dans la société visée. Si cette société
a peu d’employés, l’opération INDEX RANGE SCAN donnera de meilleures
performances. Néanmoins, un TABLE ACCESS FULL peut se révéler plus
rapide sur une grosse société car il peut lire dans ce cas de larges parties
de la table en une fois (voir « Parcours complet de table » à la page 13).

La requête est lente parce que la recherche dans l’index renvoie de


nombreux ROWID, un pour chaque employé de la société originale, et la base
de données doit les récupérer un par un. C’est la combinaison parfaite des
deux ingrédients qui rend l’index lent : la base de données lit un grand
nombre d’éléments dans l’index et doit récupérer chaque ligne séparément.

Choisir le meilleur plan d’exécution dépend aussi de la distribution des


données dans la table. Du coup, l’optimiseur utilise les statistiques sur
le contenu de la base de données. Dans notre exemple, un histogramme
contenant la distribution des employés par société est utilisé. Cela permet
à l’optimiseur d’estimer le nombre de lignes renvoyées à partir de la
recherche de l’index. Le résultat est utilisé pour le calcul du coût.

Statistiques
Un optimiseur basé sur les coûts utilise les statistiques sur les tables,
colonnes et index. La plupart des statistiques sont récupérées au
niveau des colonnes : le nombre de valeurs distinctes, la plus petite
et la plus grande valeur (intervalle de données), le nombre de valeurs
NULL et l’histogramme de la colonne (distribution des données). La
valeur statistique la plus importante pour une table est sa volumétrie
(en lignes et en blocs).
Les statistiques les plus importantes pour un index sont la profondeur
de l’arbre, le nombre de nœuds feuilles, le nombre de clés distinctes
et le facteur d’ordonnancement (voir le Chapitre 5, « Regrouper les
données »).
L’optimiseur utilise ces valeurs pour estimer la sélectivité des
prédicats de la clause where.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 21


chapitre 2 : La clause Where

Si aucune statistique n’est disponible, par exemple parce qu’elles ont été
supprimées, l’optimiseur utilise des valeurs par défaut. Les statistiques
par défaut de la base de données Oracle font référence à un petit index
avec une sélectivité moyenne. Elles amènent à supposer qu’un INDEX RANGE
SCAN renverra 40 lignes. Le plan d’exécution affiche cette estimation dans
la colonne « Rows » (encore une fois, voir l’Exemple 2.1 à la page 19).
Évidemment, c’est très fortement sous-estimé car 1000 employés travaillent
pour cette société.

Si nous fournissons des statistiques correctes, l’optimiseur fait un meilleur


travail. Le plan d’exécution suivant montre la nouvelle estimation :
1000 lignes pour l’INDEX RANGE SCAN. En conséquence, il calcule un coût
supérieur pour les accès suivant à la table.

---------------------------------------------------------------
|Id |Operation | Name | Rows | Cost |
---------------------------------------------------------------
| 0 |SELECT STATEMENT | | 1 | 680 |
|*1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES | 1 | 680 |
|*2 | INDEX RANGE SCAN | EMPLOYES_PK | 1000 | 4 |
---------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("NOM"='WINAND')
2 - access("ID_SUPPLEMENTAIRE"=30)

Un coût de 680 est même plus important que le coût pour le plan
d’exécution utilisant l’opération FULL TABLE SCAN (477, voir la page 20).
Du coup, l’optimiseur préférera automatiquement l’opération FULL TABLE
SCAN.

Cet exemple d’un index lent ne devrait pas cacher le fait qu’une bonne
indexation est la meilleure solution. Bien sûr, chercher le nom est très bien
supporté par un index sur NOM :

CREATE INDEX nom_emp ON employes (nom);

22 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Index lents, partie II

En utilisant le nouvel index, l’optimiseur calcule un coût de 3 :

Exemple 2.2. Plan d’exécution avec un index dédié

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

Predicate Information (identified by operation id):


---------------------------------------------------
1 - filter("ID_SUPPLEMENTAIRE"=30)
2 - access("NOM"='WINAND')

L’accès à l’index ramène, suivant les estimations de l’optimiseur, une


seule ligne. Du coup, la base de données doit récupérer uniquement cette
ligne dans la table : ceci sera à coup sûr plus rapide qu’une opération
FULL TABLE SCAN. Un index correctement défini est toujours meilleur qu’un
parcours complet de table.

Les deux plans d’exécution provenant de l’Exemple 2.1 (page 19) et de


l’Exemple 2.2 sont pratiquement identiques. La base de données réalise les
mêmes opérations et l’optimiseur calcule des coûts similaires. Néanmoins,
le deuxième plan est bien plus performant. L’efficacité d’une opération
INDEX RANGE SCAN peut varier dans un grand intervalle, tout spécialement
s’il est suivi d’un accès à la table. Utiliser un index ne signifie pas
automatiquement qu’une requête est exécutée de la meilleure façon qu’il
soit.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 23


chapitre 2 : La clause Where

Fonctions
L’index sur NOM a amélioré considérablement les performances mais son
utilisation n’est possible que si vous recherchez en utilisant la même
casse (majuscule/minuscule) que celle stockée dans la base de données.
Cette section explique comment enlever cette restriction sans perdre en
performance.

Note
MySQL 5.6 ne supporte pas l’indexation du résultat de fonctions
comme décrit ci-dessous. À partir de la version 5.7, MySQL peut créer
1
des index sur des colonnes générées .

Recherche insensible à la
casse en utilisant UPPER ou LOWER
Ignorer la casse dans une clause where est très simple. Par exemple, vous
pouvez convertir en majuscule les deux côtés de la comparaison :

SELECT prenom, nom, numero_telephone


FROM employes
WHERE UPPER(nom) = UPPER('winand')

Quelle que soit la capitalisation utilisée dans le terme de la recherche et


dans la colonne NOM, la fonction UPPER les fait correspondre.

Note
Une autre façon de faire des recherches insensibles à la casse est
d’utiliser une « collation » différente. Les collations par défaut
utilisées par SQL Server et MySQL ne distinguent pas entre lettres
minuscules et majuscules. Elles sont donc insensibles à la casse par
défaut.

1
https://dev.mysql.com/doc/refman/5.7/en/generated-column-index-optimizations.html

24 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Recherche insensible à la casse en utilisant UPPER ou LOWER

La logique de cette requête est parfaitement raisonnable mais le plan de


l’exécution ne l’est pas :

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

Predicate Information (identified by operation id):


---------------------------------------------------
1 - filter(UPPER("NOM")='WINAND')

C’est de nouveau le parcours complet de table. Bien qu’il existe un index


sur NOM, il est inutilisable parce que la recherche n’est pas sur NOM mais sur
UPPER(NOM). De la perspective de la base de données, cela est complètement
différent.

Ce piège est très fréquent. Nous faisons immédiatement la relation entre


NOM et UPPER(NOM) et nous nous attendons à ce que la base de données fasse
de même. En fait, l’optimiseur voit plutôt ceci :

SELECT prenom, nom, numero_telephone


FROM employes
WHERE BOITENOIRE(...) = 'WINAND';

La fonction UPPER est tout simplement une boîte noire. Les paramètres de
cette fonction n’ont pas d’importance car il n’y a aucune relation entre les
paramètres de la fonction et son résultat.

Astuce
Remplacez le nom de la fonction par BOITENOIRE pour comprendre le
point de vue de l’optimiseur.

Évaluation du temps de compilation


L’optimiseur peut évaluer l’expression sur le côté droit lors de
la compilation parce qu’il a tous les paramètres en entrée. Du
coup, le plan d’exécution Oracle (section « Predicate Information »)
affiche seulement la notation majuscule du terme de recherche. Le
comportement est très similaire à celui d’un compilateur qui évalue
les expressions constantes au moment de la compilation.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 25


chapitre 2 : La clause Where

Pour que l’index puisse être utilisé sur cette requête, nous avons besoin
d’un index qui couvre l’expression recherchée. Cela signifie que nous avons
besoin d’un index sur UPPER(NOM) et non pas sur NOM :

CREATE INDEX nom_maj_emp


ON employes (UPPER(nom))

Un index dont la définition contient des fonctions ou des expressions est


appelé un index fonctionnel (function based index (FBI), en anglais). Au lieu
de copier directement les données de la colonne dans l’index, un index
fonctionnel applique tout d’abord la fonction, puis place le résultat dans
l’index. Le résultat est un index qui contient tous les noms en majuscule.

La base de données peut utiliser un index fonctionnel si l’expression exacte


de la définition d’index apparaît dans une requête SQL, comme dans
l’exemple ci-dessus. Le plan d’exécution confirme cela :

--------------------------------------------------------------
|Id |Operation | Name | Rows | Cost |
--------------------------------------------------------------
| 0 |SELECT STATEMENT | | 100 | 41 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES | 100 | 41 |
|*2 | INDEX RANGE SCAN | NOM_MAJ_EMP | 40 | 1 |
--------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access(UPPER("NOM")='WINAND')

Il s’agit d’une opération INDEX RANGE SCAN standard, décrite dans le


Chapitre 1. La base de données parcourt l’index B-tree et suit la chaîne de
nœuds feuilles. Il n’y a pas d’opérations dédiées ou de mots clés pour les
index fonctionnels.

Avertissement
Quelques fois, les ORM utilisent UPPER et LOWER sans que le déve-
loppeur en soit conscient. Par exemple, Hibernate injecte un LOWER
implicite pour faire des recherches insensibles à la casse.

Le plan d’exécution n’est pas le même que dans la section précédente


sans le UPPER ; l’estimation du nombre de lignes est trop élevée. Il est
tout particulièrement étrange que l’optimiseur s’attende à récupérer plus
de lignes de la table que ce que va réellement récupérer l’opération

26 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Recherche insensible à la casse en utilisant UPPER ou LOWER

INDEX RANGE SCAN. Comment peut-il récupérer 100 lignes de la table si le


parcours d’index précédent n’en renvoie que 40 ? Le fait est que cela
n’est pas possible. Des estimations contradictoires indiquent souvent des
problèmes de statistiques. Dans ce cas particulier, cela est dû au fait que
la base de données Oracle ne met pas à jour les statistiques de la table
lors de la création d’un nouvel index (voir aussi « Statistiques Oracle pour
les index fonctionnels » à la page 28).

Après mise à jour des statistiques, l’optimiseur calcule des estimations plus
précises :

--------------------------------------------------------------
|Id |Operation | Name | Rows | Cost |
--------------------------------------------------------------
| 0 |SELECT STATEMENT | | 1 | 3 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES | 1 | 3 |
|*2 | INDEX RANGE SCAN | NOM_MAJ_EMP | 1 | 1 |
--------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access(UPPER("NOM")='WINAND')

Note
Les statistiques pour les index fonctionnels et pour les index multi-
colonnes ont été ajoutées dans la version 11g d’Oracle.

Bien que les statistiques mises à jour n’améliorent pas les performances
en exécution dans ce cas (le bon index a été correctement utilisé), il est
toujours préférable de vérifier les estimations de l’optimiseur. Le nombre
de lignes traitées par chaque opération (estimation de cardinalité) est une
information particulièrement importante qui est aussi affichée dans les
plans d’exécution de SQL Server et PostgreSQL.

Astuce
L’Annexe A, « Plans d’exécution » décrit les estimations du nombre de
lignes dans les plans d’exécution pour SQL Server et PostgreSQL.

SQL Server et MySQL ne supportent pas les index fonctionnels comme


décrits ci-dessus, mais offrent tous les deux un contournement avec les

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 27


chapitre 2 : La clause Where

colonnes calculées ou générées. Pour les utiliser, vous devez tout d’abord
ajouter une colonne calculée dans la table qui doit être indexée :

ALTER TABLE employes ADD nom_maj AS UPPER(nom)


CREATE INDEX nom_maj_emp ON employes (nom_maj)

SQL Server et MySQL sont capables d’utiliser cet index quand l’expression
indexée apparaît dans l’instruction. Dans certains cas simples, SQL Server
2
et MySQL peuvent utiliser cet index même si la requête n’est pas modifiée.
Parfois néanmoins, la requête doit être modifiée pour faire référence au
nom des nouvelles colonnes présentes dans l’index. N’hésitez pas à vérifier
le plan d’exécution en cas de doute.

Statistiques Oracle pour


les index fonctionnels
La base de données Oracle maintient l’information du nombre de
valeurs distinctes par colonne avec les autres statistiques de la
table. Ces informations sont réutilisées si une colonne fait partie de
plusieurs index.
Les statistiques étendues et les statistiques multi-colonnes sont aussi
conservées au niveau de la table en tant que colonnes virtuelles. Bien
que la base de données Oracle récupère les statistiques des index
automatiquement pour les nouveaux index (depuis la version 10g),
il ne met pas à jour les statistiques de la table. Pour cette raison, la
documentation Oracle recommande de mettre à jour les statistiques
de la table après création d’un index fonctionnel :

Après création d’un index fonctionnel, récupérez les


statistiques sur l’index et sa table en utilisant le package
DBMS_STATS. Ces statistiques seront activées par la base de
données Oracle pour décider de l’utilisation de l’index.
—Oracle Database SQL Language Reference
Ma recommandation personnelle va encore plus loin : après chaque
changement d’index, il faut mettre à jour les statistiques de la table
et de ses index. Néanmoins, cela peut avoir des effets de bord
indésirables. Coordonnez cette activité avec les administrateurs de la
base de données et faites une sauvegarde des statistiques avant leur
mise à jour.

2
https://dev.mysql.com/doc/refman/5.7/en/generated-column-index-optimizations.html

28 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Fonctions définies par l’utilisateur

Fonctions définies par l’utilisateur


L’indexation fonctionnelle est une approche très générique. En dehors de
fonctions comme UPPER, vous pouvez aussi indexer des expressions comme
A + B et même utiliser des fonctions utilisateurs dans la définition de
l’index.

Il existe cependant une exception importante. Par exemple, il n’est pas


possible de faire référence à l’heure actuelle dans une définition d’index,
que ce soit directement ou indirectement. En voici un exemple :

CREATE FUNCTION obtient_age(date_de_naissance DATE)


RETURN NUMBER
AS
BEGIN
RETURN
TRUNC(MONTHS_BETWEEN(SYSDATE, date_de_naissance)/12);
END;
/

La fonction OBTIENT_AGE utilise la date actuelle (avec SYSDATE) pour calculer


l’âge en se basant sur la date de naissance fournie. Vous pouvez utiliser
cette fonction dans toute requête, par exemple dans les clauses select et
where :

SELECT prenom, nom, obtient_age(date_de_naissance)


FROM employes
WHERE obtient_age(date_de_naissance) = 42;

La requête obtient la liste des employés âgés de 42 ans. Utiliser un index


fonctionnel est une idée évidente pour optimiser cette requête mais vous
ne pouvez pas utiliser la fonction OBTIENT_AGE dans une définition d’index
car cette fonction n’est pas déterministe. Cela signifie que le résultat de
cette fonction n’est pas seulement déterminé par ses paramètres. Seules
les fonctions qui renvoient toujours le même résultat pour les mêmes
paramètres (les fonctions déterministes) peuvent être indexées.

La raison derrière cette limitation est simple. Lors de l’insertion d’une


nouvelle ligne, la base de données appelle la fonction et stocke le résultat
dans l’index où il restera inchangé. Aucun processus périodique ne met
à jour l’index. La base de données ne met à jour l’âge indexé que si la
date de naissance est modifiée par une requête update. Après le prochain
anniversaire, l’âge stocké dans l’index sera mauvais.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 29


chapitre 2 : La clause Where

En dehors d’être déterministe, les bases de données PostgreSQL et Oracle


exigent que les fonctions soient déclarées comme étant déterministes
lorsqu’elles sont utilisées dans un index. Vous devez utiliser le mot clé
DETERMINISTIC (Oracle) ou IMMUTABLE (PostgreSQL).

Attention
Les bases de données PostgreSQL et Oracle font confiance aux
déclarations DETERMINISTIC ou IMMUTABLE. Cela signifie qu’elles font
confiance au développeur.
Vous pouvez déclarer la fonction OBTIENT_AGE comme étant déter-
ministe et l’utiliser dans une définition d’index. Quelle que soit
la déclaration, cela ne fonctionnera pas comme attendu car l’âge
enregistré dans l’index ne sera pas mis à jour au fil des années ;
les employés ne deviendront pas plus vieux, tout du moins pas dans
l’index.

D’autres exemples de fonctions ne pouvant pas être indexés sont les


générateurs de nombres aléatoires et les fonctions qui dépendent de
variables d’environnement.

Réflexion
Comment pouvez-vous utiliser un index pour optimiser une requête
sur les employés de 42 ans ?

30 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Sur-indexation

Sur-indexation

Si le concept d’index fonctionnel est nouveau pour vous, vous pourriez


être tenté d’indexer tout mais c’est en fait la dernière chose à faire.
Chaque index nécessite une maintenance continue. Les index fonctionnels
sont particulièrement problématiques car ils facilitent la création d’index
redondants.

La recherche insensible à la casse ci-dessus peut aussi être implémentée


avec la fonction LOWER :

SELECT nom, prenom, numero_telephone


FROM employes
WHERE LOWER(nom) = LOWER('winand');

Un seul index ne peut pas supporter les deux méthodes permettant


d’ignorer la casse. Bien sûr, nous pourrions créer un deuxième index sur
LOWER(nom) pour accélérer cette requête, mais cela signifierait que la base
de données doive maintenir deux index à chaque requête insert, update
et delete (voir aussi le Chapitre 8, « Modifier les données »). Pour qu’un
seul index suffise, vous devez utiliser la même fonction dans toute votre
application.

Astuce
Unifiez le chemin d’accès pour qu’un index puisse être utilisé par
plusieurs requêtes.

Astuce
Avoir toujours comme but d’indexer la donnée originale car elle est
souvent l’information la plus utile à placer dans un index.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 31


chapitre 2 : La clause Where

Requêtes avec paramètres

Cette section couvre un thème qui est souvent ignoré dans les livres sur le
SQL ; les requêtes avec paramètres et les paramètres liés.

Les paramètres liés, aussi appelés paramètres dynamiques ou variables liées


(« bind parameters » ou « bind variables » en anglais) sont une autre
façon de passer des données à la base de données. Au lieu de placer les
valeurs directement dans la requête SQL, vous pouvez utiliser un marqueur
comme ?, :nom ou @nom, et fournir les vraies valeurs en utilisant un appel
API séparé.

Il n’y a rien de mal à écrire les valeurs directement dans les requêtes ;
néanmoins, il y a deux bonnes raisons pour utiliser les paramètres liés dans
des programmes :

Sécurité
Les variables liées sont le meilleur moyen pour éviter les injections
3
SQL .

Performance
Les bases de données avec un cache de plan d’exécution comme
SQL Server et Oracle peuvent réutiliser un plan d’exécution si la
même requête est exécutée plusieurs fois. Cela permet d’éviter la
reconstruction du plan d’exécution mais cela ne fonctionne que si la
requête SQL est strictement identique. Si vous placez différentes valeurs
dans la requête SQL, la base de données la gère comme une nouvelle
requête et crée un nouveau plan d’exécution.

Lors de l’utilisation de paramètres liés, vous ne pouvez pas écrire les


vraies valeurs. À la place, vous insérez des marqueurs dans la requête
SQL. De cette façon, les requêtes ne changent pas quand elles sont
exécutées avec des valeurs différentes.

3
https://fr.wikipedia.org/wiki/Injection_SQL

32 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Requêtes avec paramètres

Naturellement, il existe des exceptions. Par exemple, si le volume de


données impliquées dépend des valeurs réelles:

99 rows selected.
SELECT prenom, nom
FROM employes
WHERE id_supplementaire = 20;

---------------------------------------------------------------
|Id | Operation | Name | Rows | Cost |
---------------------------------------------------------------
| 0 | SELECT STATEMENT | | 99 | 70 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES | 99 | 70 |
|*2 | INDEX RANGE SCAN | EMPLOYE_PK | 99 | 2 |
---------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------

2 - access("ID_SUPPLEMENTAIRE"=20)

Une recherche par index apporte les meilleures performances pour des
petites sociétés mais une opération TABLE ACCESS FULL peut être encore
meilleure que l’index pour les grosses sociétés :

1000 rows selected.

SELECT prenom, nom


FROM employes
WHERE id_supplementaire = 30;
----------------------------------------------------
| Id | Operation | Name | Rows | Cost |
----------------------------------------------------
| 0 | SELECT STATEMENT | | 1000 | 478 |
|* 1 | TABLE ACCESS FULL| EMPLOYES | 1000 | 478 |
----------------------------------------------------

Predicate Information (identified by operation id):


---------------------------------------------------
1 - filter("ID_SUPPLEMENTAIRE"=30)

Dans ce cas, l’histogramme sur ID_SUPPLEMENTAIRE remplit son but.


L’optimiseur l’utilise pour déterminer la fréquence des identifiants
supplémentaires mentionnés dans la requête SQL. En conséquence, il
obtient deux estimations différentes du nombre de lignes pour les deux
requêtes.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 33


chapitre 2 : La clause Where

Le calcul de coût suivant va du coup résulter en deux valeurs de coût. Quand


l’optimiseur sélectionne enfin un plan d’exécution, il prend le plan ayant
le plus petit coût. Pour les plus petites sociétés, il s’agit du plan utilisant
l’index.

Le coût d’une opération TABLE ACCESS BY INDEX ROWID est très dépendant de
l’estimation du nombre de lignes. Sélectionner dix fois plus de lignes élèvera
le coût par ce même facteur. Le coût complet utilisant l’index est alors
plus important qu’un parcours de table complet. Du coup, l’optimiseur
sélectionnera l’autre plan d’exécution pour une plus grosse société.

Lors de l’utilisation de paramètres liés, l’optimiseur ne dispose pas de


valeurs concrètes pour déterminer leur fréquences. Du coup, il suppose une
distribution identique et obtient toujours la même estimation du nombre
de lignes et du coût. Au final, il sélectionnera toujours le même plan
d’exécution.

Astuce
Les histogrammes des colonnes sont principalement utiles si les
valeurs ne sont pas uniformément distribuées.
Pour les colonnes ayant une distribution uniforme, il suffit
fréquemment de diviser le nombre de valeurs distinctes par le
nombre de lignes dans la table. Cette méthode fonctionne aussi lors
de l’utilisation de paramètres liés.

Si nous comparons l’optimiseur à un compilateur, les variables liées sont


comme des variables de programme mais si vous écrivez les valeurs
directement dans la requête, elles sont comme des constantes. La base
de données peut utiliser les valeurs à partir de la requête SQL lors de
l’optimisation tout comme un compilateur peut évaluer les expressions
constantes à la compilation. Pour le dire simplement, les paramètres liés
ne sont pas visibles à l’optimiseur tout comme les valeurs des variables ne
sont pas connues du compilateur.

À partir de là, il est un peu paradoxal que les paramètres liés puissent
améliorer les performances si ne pas les utiliser permet à l’optimiseur de
toujours choisir le meilleur plan d’exécution. Mais la question est à quel
prix ? Générer et évaluer toutes les variantes des plans d’exécution est un
gros travail qui n’a aucun intérêt si, au final, on obtient le même résultat.

34 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Requêtes avec paramètres

Astuce
Ne pas utiliser les paramètres liés est comme recompiler un
programme chaque fois qu’on veut l’utiliser.

Décider de la construction d’un plan spécialisé ou générique est un


dilemme pour la base de données. Soit un effort est fait pour évaluer
tous les plans possibles pour chaque exécution dans le but d’obtenir le
meilleur plan d’exécution possible, soit on évite la surcharge occasionnée
par l’optimisation en utilisant un plan d’exécution mis en cache quand
cela est possible, tout en acceptant le risque d’utiliser un plan d’exécution
non optimal. Le dilemme est que la base de données ne sait pas si le cycle
d’optimisation complet délivre un plan d’exécution différent sans réaliser
réellement l’optimisation complète. Les concepteurs de bases de données
essaient de résoudre ce dilemme avec des méthodes heuristiques... dont le
succès reste très limité.

En tant que développeur, vous devez utiliser les paramètres liés de façon
délibérée pour aider à résoudre ce dilemme. Autrement dit, vous devez
toujours utiliser les paramètres liés sauf pour les valeurs qui pourraient
influencer le plan d’exécution.

Les codes de statut non équitablement distribués, comme « à faire » et


« fait » sont un excellent exemple. Le nombre d’enregistrements marqués
« fait » excède généralement très fortement le nombre d’enregistrements
« à faire ». Utiliser un index a du sens pour rechercher les enregistrements
« à faire » dans ce cas. Le partitionnement est un autre exemple, notam-
ment si vous divisez des tables et index sur plusieurs espaces de stockage.
Les valeurs réelles peuvent alors influencer les partitions à parcourir. Les
performances des requêtes LIKE peuvent aussi souffrir de l’utilisation des
paramètres liés comme nous le verrons dans la prochaine section.

Astuce
En fait, il n’existe que quelques cas pour lesquels les valeurs réelles
affectent le plan d’exécution. Du coup, vous devez utiliser les
paramètres liés en cas de doute, ne serait-ce que pour empêcher les
injections SQL.

Les astuces de code suivant montrent comment utiliser les paramètres liés
dans différents langages de programmation.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 35


chapitre 2 : La clause Where

C#
Sans paramètres liés :

int id_supplementaire;
SqlCommand cmd = new SqlCommand(
"select prenom, nom"
+ " from employes"
+ " where id_supplementaire = " + id_supplementaire
, connection);

Avec un paramètre lié :

int id_supplementaire;
SqlCommand cmd = new SqlCommand(
"select prenom, nom"
+ " from employees"
+ " where id_supplementaire = @id_supplementaire
, connection);
cmd.Parameters.AddWithValue( "@id_supplementaire"
, id_supplementaire);

Voir aussi la documentation de la classe SqlParameterCollection.

Java
Sans paramètre lié :

int id_supplementaire;
Statement cmd = connection.createStatement(
"select prenom, nom"
+ " from employes"
+ " where id_supplementaire = " + id_supplementaire
);

Avec paramètre lié :

int id_supplementaire;
PreparedStatement cmd = connection.prepareStatement(
"select prenom, nom"
+ " from employes"
+ " where id_supplementaire = ?"
);
cmd.setInt(1, id_supplementaire);

Voir aussi la documentation de la classe PreparedStatement.

36 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Requêtes avec paramètres

Perl
Sans paramètre lié :

my $id_supplementaire;
my $sth = $dbh->prepare(
"select prenom, nom"
. " from employes"
. " where id_supplementaire = $id_supplementaire"
);
$sth->execute();

Avec paramètre lié :

my $id_supplementaire;
my $sth = $dbh->prepare(
"select prenom, nom"
. " from employes"
. " where id_supplementaire = ?"
);
$sth->execute($id_supplementaire);

Voir : Programming the Perl DBI.

PHP
En utilisant MySQL, sans paramètre lié :

$mysqli->query(
"select prenom, nom"
. " from employes"
. " where id_supplementaire = " . $id_supplementaire);

Avec paramètre lié :

if ($stmt = $mysqli->prepare("select prenom, nom"


. " from employes"
. " where id_supplementaire = ?"))
{
$stmt->bind_param("i", $id_supplementaire);
$stmt->execute();
} else {
/* handle SQL error */
}

Voir aussi la documentation de la classe mysqli_stmt::bind_param et


le chapitre « Prepared statements and stored procedures » de la
documentation PDO.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 37


chapitre 2 : La clause Where

Ruby
Sans paramètre lié :

dbh.execute("select prenom, nom"


+ " from employes"
+ " where id_supplementaire = {id_supplementaire}");

Avec paramètre lié :

dbh.prepare("select prenom, nom"


+ " from employes"
+ " where id_supplementaire = ?");
dbh.execute(id_supplementaire);

Voir aussi le chapitre « Quoting, Placeholders, and Parameter Binding »


dans le tutoriel DBI de Ruby.

Le point d’interrogation (?) est le seul caractère marqueur que le standard


SQL définit. Les points d’interrogation sont des paramètres de position.
Cela signifie que les points d’interrogation sont comptés de la gauche vers
la droite. Pour lier une valeur à un point d’interrogation particulier, vous
devez indiquer son numéro. Néanmoins, cela peut se révéler peu pratique
à cause des changements de numéros lors de l’ajout et de la suppression
des marqueurs. La plupart des bases de données propose une extension
propriétaire aux paramètres nommés pour résoudre ce problème, c’est-à-
dire en utilisant un symbole (@nom) ou le signe deux-points (:nom).

Note
Les paramètres liés ne peuvent pas changer la structure d’une
requête SQL.
Cela signifie que vous ne pouvez pas utiliser les paramètres liés pour
les noms de tables ou de colonnes. Les paramètres liés suivants ne
fonctionnent pas:

String sql = prepare("SELECT * FROM ? WHERE ?");


sql.execute('employes', 'id_employe = 1');

Si vous devez changer la structure d’une requête SQL à l’exécution,


utilisez du SQL dynamique.

38 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Rechercher des intervalles

Partage de curseur et
paramétrisation automatique
Plus l’optimiseur et la requête SQL sont complexes, plus la mise en
cache des plans d’exécution devient important. Les bases de données
SQL Server et Oracle ont des fonctionnalités permettant de remplacer
automatiquement les valeurs littérales dans une chaîne SQL avec des
paramètres liés. Ces fonctionnalités sont appelées CURSOR_SHARING
(Oracle) ou paramétrisation forcée (SQL Server).

Ces deux fonctionnalités sont des contournements pour les appli-


cations qui n’utilisent pas du tout les paramètres liés. Activer ces
fonctionnalités empêche les développeurs d’utiliser volontairement
des valeurs littérales.

Rechercher des intervalles


Les opérateurs d’inégalité comme <, > et between peuvent utiliser des index
tout comme l’opérateur d’égalité expliqué ci-dessus. Même un filtre LIKE
peut, sous certaines conditions, utiliser un index tout comme les conditions
d’intervalle le font.

Utiliser ces opérations limite le choix de l’ordre des colonnes dans les index
multi-colonnes. Cette limitation peut même empêcher toute indexation.
Certaines requêtes ne vous permettent pas de définir un ordre correct des
colonnes.

Plus grand, plus petit et entre


Le plus gros risque en terme de performances d’un INDEX RANGE SCAN est
le parcours de nœuds feuilles. Du coup, la règle d’or de l’indexation est de
conserver les intervalles parcourus aussi petits que possible. Vous pouvez
le vérifier en vous demandant où le parcours d’un index commence et où
il se termine.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 39


chapitre 2 : La clause Where

Il est facile de répondre à cette question si la requête SQL mentionne


explicitement les conditions de départ et d’arrivée :

SELECT prenom, nom, date_de_naissance


FROM employes
WHERE date_de_naissance >= TO_DATE(?, 'YYYY-MM-DD')
AND date_de_naissance <= TO_DATE(?, 'YYYY-MM-DD')

Un index sur DATE_DE_NAISSANCE est seulement parcourue sur l’intervalle


indiqué. Le parcours commence à la première date et se termine à la
deuxième date. Nous ne pouvons pas diminuer l’intervalle parcouru.

Les conditions de départ et d’arrêt sont moins évidentes si une deuxième


colonne est utilisée :

SELECT prenom, nom, date_de_naissance


FROM employes
WHERE date_de_naissance >= TO_DATE(?, 'YYYY-MM-DD')
AND date_de_naissance <= TO_DATE(?, 'YYYY-MM-DD')
AND id_supplementaire = ?

Bien sûr, un index idéal doit couvrir les deux colonnes mais la question est
de connaître l’ordre idéal.

Les graphes suivants montrent l’effet de l’ordre de la colonne sur l’intervalle


parcouru dans l’index. Pour cette illustration, nous recherchons tous les
er
employés de la société 27, nés entre le 1 janvier et le 9 janvier 1971.

La Figure 2.2 affiche un détail de l’index sur DATE_DE_NAISSANCE et


ID_SUPPLEMENTAIRE, dans cet ordre. Par quel nœud feuille la base de données
va-t-elle commencer son parcours ? ou autrement dit, à quel nœud feuille
le parcours va-t-il s’arrêter ?

L’index est tout d’abord ordonné sur les dates de naissances. Ce n’est
que si deux employés sont nés le même jour que ID_SUPPLEMENTAIRE est
utilisé pour trier les enregistrements. Néanmoins, la requête couvre un
intervalle de date. L’ordre de la colonne ID_SUPPLEMENTAIRE est inutile lors du
parcours de l’arbre. Cela devient évident si vous comprenez qu’il n’y a pas
d’enregistrements pour la société 27 dans les nœuds branches, bien qu’il
y en ait dans les nœuds feuilles. Du coup, le filtre sur DATE_DE_NAISSANCE
est la seule condition qui limite l’intervalle parcouru pour l’index. Cela
commence avec la première entrée correspondant à la date de l’intervalle
et se termine avec la dernière. Cela correspond aux cinq nœuds feuilles
indiquées dans la Figure 2.2.

40 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Plus grand, plus petit et entre

Figure 2.2. Parcours d’intervalle dans un index DATE_DE_NAISSANCE,


ID_SUPPLEMENTAIRE

E
NC

IR

NC

IR
SA

TA

SA

TA
IS

EN

IS

EN
NA

EM

NA

EM
E_

PL

E_

PL
_D

UP

_D

UP
TE

_S

TE

_S
DA

ID

DA

ID
27-DEC-70 19 28-DEC-70 4 ROWID
01-JAN-71 6 01-JAN-71 3 ROWID
05-JAN-71 3 01-JAN-71 6 ROWID

02-JAN-71 1 ROWID
04-JAN-71 1 ROWID

Intervalle parcouru de l'index


05-JAN-71 3 ROWID

06-JAN-71 4 ROWID
06-JAN-71 11 ROWID
08-JAN-71 6 ROWID

08-JAN-71 6 08-JAN-71 27 ROWID


09-JAN-71 17 09-JAN-71 10 ROWID
12-JAN-71 3 09-JAN-71 17 ROWID

09-JAN-71 17 ROWID
09-JAN-71 30 ROWID
12-JAN-71 3 ROWID

Le graphe est complètement différent si on inverse l’ordre des colonnes.


La Figure 2.3 illustre le parcours si l’index commence avec la colonne
ID_SUPPLEMENTAIRE.

La différence tient dans le fait que l’opérateur d’égalité limite la première


colonne d’index à une seule valeur. Dans l’intervalle pour cette valeur
(ID_SUPPLEMENTAIRE 27), l’index est trié suivant la deuxième colonne, la date
de naissance. Il n’y a donc pas besoin de visiter le premier nœud feuille car
le nœud branche indique déjà qu’il n’y a pas d’employés pour la société 27,
qui serait né après le 25 juin 1969 dans le premier nœud feuille.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 41


chapitre 2 : La clause Where

Figure 2.3. Parcours d’intervalle avec l’index sur ID_SUPPLEMENTAIRE,


DATE_DE_NAISSANCE

E
IR

NC

IR

NC
TA

SA

TA

SA
EN

IS

EN

IS
EM

NA

EM

NA
PL

E_

PL

E_
UP

_D

UP

_D
_S

TE

_S

TE
ID

DA

ID

DA
26 12-SEP-60 26 01-SEP-83 ROWID
27 25-JUN-69 27 23-NOV-64 ROWID
27 26-SEP-72 27 25-JUN-69 ROWID

27 23-SEP-69 ROWID
Intervalle 27 08-JAN-71 ROWID
parcouru de l'index
27 26-SEP-72 ROWID

27 04-OCT-73 ROWID
27 18-DEC-75 ROWID
27 16-AUG-76 ROWID

27 16-AUG-76 27 23-AUG-76 ROWID


27 14-SEP-84 27 30-JUL-78 ROWID
30 30-SEP-53 27 14-SEP-84 ROWID

27 09-MAR-88 ROWID
27 08-OCT-91 ROWID
30 30-SEP-53 ROWID

Le parcours de l’arbre amène directement au deuxième nœud feuille. Dans


ce cas, toutes les conditions de la clause where limitent l’intervalle d’index
parcouru, pour que le parcours se termine sur le même nœud feuille.

Astuce
Règle d’or : indexer pour l’égalité, puis pour les intervalles.

La différence réelle de performances dépend des données et du critère de


recherche. La différence est négligeable si le filtre sur DATE_DE_NAISSANCE
est très sélectif. Plus l’intervalle de date est important, plus la différence
de performances le sera aussi.

42 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Plus grand, plus petit et entre

Avec cet exemple, nous pouvons aussi démonter ce mythe qui cherche
à faire croire que la colonne la plus sélective doit être à la première
colonne d’un index. Si nous regardons les graphes et si nous considérons
la sélectivité de la première colonne seulement, nous voyons que les
deux conditions correspondent à 13 enregistrements. C’est le cas que
l’on filtre par la colonne DATE_DE_NAISSANCE seulement ou par la colonne
ID_SUPPLEMENTAIRE seulement. La sélectivité n’a pas d’utilité ici mais un
ordre de colonnes apporte plus de performances que l’autre.

Pour optimiser les performances, il est très important de connaître


l’intervalle d’index parcouru. Avec la plupart des bases de données, vous
pouvez le voir dans le plan d’exécution... si vous savez ce que vous cherchez.
Le plan d’exécution suivant provenant de la base de données Oracle
indique sans ambiguïté que l’index EMP_TEST commence avec la colonne
DATE_DE_NAISSANCE.

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

Predicate Information (identified by operation id):


---------------------------------------------------
1 - filter(:END_DT >= :START_DT)
3 - access(DATE_DE_NAISSANCE >= :START_DT
AND DATE_DE_NAISSANCE <= :END_DT)
filter(ID_SUPPLEMENTAIRE = :SUBS_ID)

L’information du prédicat pour l’opération INDEX RANGE SCAN donne l’infor-


mation recherchée. Elle identifie les conditions de la clause where comme
des prédicats accès ou filtre. C’est de cette façon que la base de données
nous indique comment elle utilise chaque condition.

Note
Le plan d’exécution a été simplifié pour qu’il soit plus clair. L'annexe
en page 170 explique les détails de la section « Predicate
Information » dans un plan d’exécution Oracle.

Les conditions sur la colonne DATE_DE_NAISSANCE sont celles listées comme


prédicat accès ; elles se limitent à l’intervalle d’index parcouru. Du coup, la

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 43


chapitre 2 : La clause Where

colonne DATE_DE_NAISSANCE est la première colonne de l’index EMP_TEST. La


colonne ID_SUPPLEMENTAIRE est utilisée seulement comme un filtre.

Important
Les prédicats accès sont les conditions de départ et d’arrivée pour une
recherche par index. Ils définissent l’intervalle parcouru sur l’index.
Les prédicats filtres d’un index sont appliqués seulement lors du
parcours des nœuds feuilles. Ils ne peuvent pas diminuer l’intervalle
parcouru de l’index.
L’Annexe A explique comment reconnaître les prédicats index dans
d’autres bases de données.

La base de données peut utiliser toutes les conditions comme prédicats


accès si nous changeons la définition de l’index :

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

Predicate Information (identified by operation id):


---------------------------------------------------
1 - filter(:END_DT >= :START_DT)
3 - access(ID_SUPPLEMENTAIRE = :SUBS_ID
AND DATE_DE_NAISSANCE >= :START_DT
AND DATE_DE_NAISSANCE <= :END_T)

Enfin, il existe un opérateur between. Il vous permet de spécifier les limites


haute et basse en une seule condition :

DATE_DE_NAISSANCE BETWEEN '01-JAN-71'


AND '10-JAN-71'

Notez que between inclut toujours les valeurs indiquées, tout comme les
opérateurs inférieur ou égal (<=) et supérieur ou égal (>=) :

DATE_DE_NAISSANCE >= '01-JAN-71'


AND DATE_DE_NAISSANCE <= '10-JAN-71'

44 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Indexer les filtres LIKE

Indexer les filtres LIKE


L’opérateur SQL LIKE est la cause très fréquente de comportements
inattendus au niveau des performances car certains termes de recherche
empêchent une utilisation efficace des index. Cela signifie que certains
termes de recherche peuvent être très bien indexés et d’autres non. La
position des caractères joker fait toute la différence.

L’exemple suivant utilise le caractère joker % au milieu du terme de


recherche :

SELECT prenom, nom, date_de_naissance


FROM employes
WHERE UPPER(nom) LIKE 'WIN%D'
---------------------------------------------------------------
|Id | Operation | Name | Rows | Cost |
---------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 4 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES | 1 | 4 |
|*2 | INDEX RANGE SCAN | NOM_MAJ_EMP | 1 | 2 |
---------------------------------------------------------------

Les filtres LIKE peuvent seulement utiliser les caractères avant le premier
caractère joker lors du parcours de l’arbre. Les caractères qui suivent sont
simplement des prédicats filtres qui ne peuvent pas diminuer l’intervalle
parcouru de l’index. Une expression LIKE seule peut donc contenir deux
types de prédicat : (1) la partie avant le caractère joker comme prédicat
accès ; (2) les autres caractères comme prédicat filtre.

Attention
Pour la base de données PostgreSQL, vous pouvez avoir besoin de
spécifier une classe d’opérateur (par exemple, varchar_pattern_ops)
pour utiliser les expressions LIKE comme prédicats accès. Référez-
vous à « Classes et familles d’opérateurs » dans la documentation
PostgreSQL pour plus de détails.

Plus le préfixe avant le première caractère joker est sélectif, et plus petit
sera l’intervalle parcouru de l’index. Cela a comme conséquence de rendre
la recherche via l’index plus rapide. La Figure 2.4 illustre la relation entre
trois expressions LIKE différentes. Toutes les trois sélectionnent la même
ligne mais l’intervalle parcouru de l’index, et donc les performances, est
très différent.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 45


chapitre 2 : La clause Where

Figure 2.4. Différentes recherches LIKE

LIKE 'WI%ND' LIKE 'WIN%D' LIKE 'WINA%'


WIAW WIAW WIAW
WIBLQQNPUA WIBLQQNPUA WIBLQQNPUA
WIBYHSNZ WIBYHSNZ WIBYHSNZ
WIFMDWUQMB WIFMDWUQMB WIFMDWUQMB
WIGLZX WIGLZX WIGLZX
WIH WIH WIH
WIHTFVZNLC WIHTFVZNLC WIHTFVZNLC
WIJYAXPP WIJYAXPP WIJYAXPP
WINAND WINAND WINAND
WINBKYDSKW WINBKYDSKW WINBKYDSKW
WIPOJ WIPOJ WIPOJ
WISRGPK WISRGPK WISRGPK
WITJIVQJ WITJIVQJ WITJIVQJ
WIW WIW WIW
WIWGPJMQGG WIWGPJMQGG WIWGPJMQGG
WIWKHLBJ WIWKHLBJ WIWKHLBJ
WIYETHN WIYETHN WIYETHN
WIYJ WIYJ WIYJ

La première expression dispose de deux caractères avant le caractère joker.


Ils limitent l’intervalle parcouru dans l’index à 18 lignes. Seule une d’entre
elles correspond à l’expression LIKE entière. Les 17 autres sont récupérées
puis abandonnées. La deuxième expression a un préfixe plus long qui
diminue l’intervalle à deux lignes. Avec cette expression, la base de données
lit seulement une ligne en trop, une ligne qui n’est pas pertinente pour le
résultat. La dernière expression n’a pas de prédicat accès du tout : la base
de données ne fait que lire l’enregistrement qui correspond à l’expression
LIKE complète.

Important
Seule la partie avant le premier caractère joker sert de prédicat accès.
Le reste des caractères ne peut pas diminuer l’intervalle parcouru
dans l’index. Les enregistrements qui ne correspondent pas à la
recherche sont juste ignorés pour le résultat final.

Le cas opposé est aussi possible : une expression LIKE qui commence avec
un caractère joker. Une telle expression LIKE ne peut pas servir comme
prédicat accès. La base de données doit parcourir la table entière s’il n’y a
pas d’autres conditions qui permettent d’accéder à des prédicats accès.

46 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Indexer les filtres LIKE

Astuce
Évitez les expressions LIKE ayant des caractères joker en début (par
exemple, '%TERM').

La position des caractères jokers affecte l’utilisation des index, au moins


en théorie. En fait, l’optimiseur crée un plan d’exécution générique quand
le terme de la recherche est fourni avec les paramètres liés. Dans ce cas,
l’optimiseur doit deviner si la majorité des exécutions auront un caractère
en tête ou non.

La plupart des bases de données supposent qu’il n’y aura pas de caractère
joker au début lors de l’optimisation d’une condition LIKE avec un
paramètre lié, mais cette supposition est fausse si l’expression LIKE est
utilisée pour la recherche plein texte. Malheureusement, il n’existe pas
de moyens directs pour préciser que la condition LIKE sera utilisée pour
de la recherche plein texte. L’encart « Indiquer les expressions LIKE
pour la recherche plein texte » montre ce qui ne fonctionne pas. Ne
pas utiliser de paramètre lié est la solution la plus évidente, mais cela
augmente le travail de l’optimiseur et la vulnérabilité à une injection SQL
est toujours présente. Une solution efficace, sécurisée et portable est de
cacher volontairement la condition LIKE. « Combiner des colonnes » à la
page 70 explique cela en détail.

Indiquer les expressions LIKE


pour la recherche plein texte
Lors de l’utilisation de l’opérateur LIKE pour une recherche plein
texte, vous pouvez séparer les caractères joker du terme de la
recherche :

WHERE colonne_texte LIKE '%' || ? || '%'

Les caractères joker sont directement écrits dans la requête


SQL mais nous utilisons un paramètre lié pour le terme de la
recherche. L’expression LIKE finale est construite par la base de
données en utilisant l’opérateur de concaténation de chaînes de
caractères || (Oracle, PostgreSQL). Bien qu’on utilise un paramètre
lié, l’expression LIKE finale commencera toujours avec un caractère
joker. Malheureusement, les bases de données ne reconnaissent
pas ça.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 47


chapitre 2 : La clause Where

Pour la base de données PostgreSQL, le problème est différent parce que


PostgreSQL suppose qu’il y a toujours un caractère joker lors de l’utilisation
de paramètres liés pour une expression LIKE. PostgreSQL n’utilise tout
simplement pas d’index dans ce cas. La seule façon d’obtenir un parcours
d’index pour une expression LIKE est de rendre le terme de recherche visible
à l’optimiseur. Si vous n’utilisez pas un paramètre lié, mais que vous placez
le terme de recherche directement dans la requête SQL, vous devez prendre
d’autres dispositions pour vous protéger contre les attaques par injection
SQL.
Même si la base de données optimise le plan d’exécution pour un caractère
joker au début, cela peut offrir des performances insuffisantes. Vous pouvez
utiliser une autre partie de la clause where pour accéder efficacement aux
données. Voir aussi « Prédicats de filtre utilisés intentionnellement sur des
index » à la page 112. S’il n’existe pas d’autres chemins d’accès, vous
pouvez utiliser une des solutions de recherche plein texte suivantes.

MySQL
MySQL propose les mots-clés match et against pour la recherche plein
texte. À partir de MySQL 5.6, vous pouvez créer des index de recherche
plein texte pour les tables InnoDB. Auparavant, cela était seulement
possible avec les tables MyISAM. Voir le chapitre « Full-Text Search
Functions » de la documentation MySQL.

Oracle Database
La base de données Oracle propose le mot-clé contains. Voir le
document « Oracle Text Application Developer’s Guide. »

PostgreSQL
PostgreSQL propose l’opérateur @@ pour ajouter les recherches plein
texte. Voir le chapitre « Recherche plein texte » dans la documentation
PostgreSQL.
4
Une autre option revient à utiliser l’extension WildSpeed pour
optimiser directement les expressions LIKE. L’extension stocke le texte
dans toutes les rotations possibles, pour que chaque caractère soit une
fois au début du texte. Cela signifie que le texte indexé n’est pas stocké
une fois, mais autant de fois qu’il y a de caractères. Il nécessite donc
beaucoup d’espace disque.

SQL Server
SQL Server propose le mot-clé contains. Voir le chapitre « Full-Text
Search » dans la documentation de SQL Server.
4
http://www.sai.msu.su/~megera/wiki/wildspeed

48 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Fusion d’index

Réflexion
Comment pouvez-vous indexer une recherche LIKE qui a seulement
un caractère joker au début du terme de recherche ('%TERME') ?

Fusion d’index
C’est une des questions les plus fréquentes sur l’indexation : est-il
préférable de créer un index pour chaque colonne ou un index seul pour
toutes les colonnes d’une clause where ? La réponse est très simple dans la
plupart des cas : un index avec plusieurs colonnes est mieux.

Néanmoins, pour certaines requêtes, un index seul ne peut pas faire un


travail parfait, quelle que soit la définition de l’index. Par exemple, les
requêtes avec au moins deux conditions différentes d’intervalle comme
dans l’exemple suivant :

SELECT prenom, nom, date_de_naissance


FROM employes
WHERE UPPER(nom) < ?
AND date_de_naissance < ?

Il est impossible de définir un index B-tree supportant cette requête sans


passer par des prédicats filtres. L’explication est simple : souvenez-vous
qu’un index est une liste chaînée.

Si vous définissez l’index comme UPPER(NOM), DATE_DE_NAISSANCE (dans cet


ordre), la liste commence avec A et se termine avec Z. La date de naissance
n’est considérée que si deux employés ont le même nom. Si vous définissez
l’index dans l’ordre inverse, il commencera avec les employés les plus âgés
et finira avec les plus jeunes. Dans ce cas, les noms auront un impact
moindre sur l’ordre de tri.

Peu importe comment vous personnalisez la définition de l’index, les


enregistrements sont toujours rangés dans une chaîne. À un bout, vous
avez les plus petits enregistrements et à l’autre les plus gros. Du coup,
un index peut seulement supporter une condition d’intervalle comme
prédicat d’accès. Supporter deux conditions d’intervalles indépendantes
nécessiterait un deuxième axe, comme par exemple un échiquier. Mais un
index n’est pas comme un échiquier. Il s’agit d’une liste chaînée.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 49


chapitre 2 : La clause Where

Vous pouvez bien sûr accepter le prédicat filtre et néanmoins utiliser un


index multi-colonnes. C’est la meilleure solution dans de nombreux cas de
toute façon. La définition de l’index doit alors mentionner en premier la
colonne la plus sélective pour qu’il soit utilisable avec un prédicat accès.
Cela pourrait être à l’origine du mythe « la plus sélective d’abord » mais
cette règle n’est vraie que si vous pouvez éviter un prédicat filtre.

L’autre option est d’utiliser deux index séparés, un pour chaque colonne. La
base de données doit parcourir les deux index, puis combiner leur résultat.
La recherche d’index dupliqué demande déjà plus d’efforts car la base de
données doit parcourir deux arbres d’index. De plus, la base de données a
besoin de beaucoup de mémoire et de temps processeur pour combiner les
résultats intermédiaires.

Note
Parcourir un index est plus rapide qu’en parcourir deux.

Les bases de données utilisent deux méthodes pour combiner les index.
Tout d’abord, il y a la jointure d’index. Le Chapitre 4, Opération de jointure
explique les algorithmes en question en détail. La deuxième approche
revient à utiliser des fonctionnalités du monde des entrepôts de données.

Les logiciels d’entrepôt de données sont le berceau de toutes les requêtes


personnalisées. Il suffit de quelques clics pour combiner des conditions
arbitraires dans la requête de votre choix. Il est impossible de prédire les
combinaisons de colonnes qui pourraient apparaître dans la clause where et
cela rend l’indexation, tel qu’expliqué jusqu’ici, pratiquement impossible.

Les entrepôts de données ont un type d’index particulier pour résoudre


ce problème : les index bitmap. L’avantage des index bitmap est qu’ils
peuvent être combinés plutôt facilement. Cela signifie que vous obtenez des
performances décentes lors de l’indexation individuelle de chaque colonne.
Si vous connaissez la requête à l’avance, vous pouvez créer un index B-tree
multi-colonnes très spécialisé, il sera toujours plus rapide que de combiner
plusieurs index bitmap.

La plus grande faiblesse des index bitmap est la scalabilité ridicule face
aux insert, update et delete. Les opérations d’écritures concurrentes sont
virtuellement impossibles. Ce n’est pas un problème dans un entrepôt car
les processus de chargement sont planifiés les uns après les autres. Dans
les applications en ligne, les index bitmap sont pratiquement inutiles.

50 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Index partiels

Important
Les index bitmap sont pratiquement inutilisables pour les traite-
ments en ligne (OLTP).

Beaucoup de bases de données proposent une solution hybride entre les


index B-tree et les index bitmap. En l’absence d’un meilleur chemin d’accès,
ils convertissent les résultats du parcours de plusieurs index B-tree en des
structures bitmap en mémoire. Elles peuvent être combinées efficacement.
Les structures bitmap sont temporaires car annulées dès l’exécution de
la requête terminée. Cela permet d’éviter le problème de la scalabilité.
L’inconvénient est que cela utilise beaucoup de mémoire et de temps
processeur. En fait, cette méthode est un acte désespéré de l’optimiseur
pour obtenir de meilleures performances.

Index partiels
Pour l’instant, nous avons seulement discuté des colonnes à ajouter dans
un index. Avec les index partiels (PostgreSQL) ou filtrés (SQL Server), vous
pouvez aussi spécifier les lignes à indexer.

Attention
La base de données Oracle a une approche unique pour l’indexation
partielle. La prochaine section l’explique en se basant sur cette
section.

Un index partiel est utile pour les conditions where fréquentes qui utilisent
des valeurs constantes, comme un code de statut dans l’exemple suivant :

SELECT message
FROM messages
WHERE traite = 'N'
AND destinataire = ?

Les requêtes de cette forme sont très habituelles dans les systèmes
gérant des queues. La requête récupère tous les messages non traités
pour un destinataire particulier. Les messages déjà traités sont rarement
intéressants. Dans le cas où il faut les récupérer, ils sont généralement
accédés via un critère plus spécifique comme la clé primaire.

Nous pouvons optimiser cette requête avec un index à deux colonnes. En


considérant cette requête seule, l’ordre des colonnes n’a pas d’importance
car il n’y a pas de conditions à intervalle.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 51


chapitre 2 : La clause Where

CREATE INDEX messages_atraiter


ON messages (destinataire, traite)

L’index remplit son but mais il inclut des lignes qui ne seront jamais
recherchées (tous les messages qui ont déjà été traités). En raison de la
complexité logarithmique, l’index rend la requête très rapide même s’il fait
perdre beaucoup d’espace disque.

Avec une indexation partielle, vous pouvez limiter l’index pour qu’il inclue
seulement les messages non traités. Pour cela, la syntaxe est très simple :
une clause where.

CREATE INDEX messages_atraiter


ON messages (destinataire)
WHERE traite = 'N'

L’index contient seulement les lignes qui satisfont la clause where. Dans
ce cas particulier, nous pouvons même supprimer la colonne TRAITE car
elle vaut toujours 'N'. Cela signifie que l’index est réduit en taille sur
deux dimensions : horizontalement car il contient moins de lignes, et
verticalement car une colonne a été supprimée.

Du coup, l’index est plus petit. Pour une queue, cela peut même vouloir
dire que la taille de l’index reste inchangée bien que la table grossisse sans
limite. L’index ne contient pas tous les messages, juste ceux qui n’ont pas
été traités.

La clause where d’un index partiel peut devenir arbitrairement complexe.


La seule limitation fondamentale est sur les fonctions : vous pouvez
seulement utiliser des fonctions déterministes comme partout ailleurs dans
la définition d’un index. Néanmoins, SQL Server a des règles plus restrictives
et n’autorise ni les fonctions ni l’opérateur OR dans les prédicats d’un index.

Une base de données peut utiliser un index partiel à chaque fois qu’une
clause where apparaît dans une requête.

Réflexion
Quelle particularité a l’index le plus petit pour la requête suivante :

SELECT message
FROM messages
WHERE traite = 'N';

52 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


NULL dans la base de données Oracle

NULL dans la base de données Oracle


Les NULL en SQL sont une source fréquente de confusion. Bien que l’idée
de base d’un NULL (représenter une valeur manquante) soit assez simple,
cela donne lieu à quelques spécificités. Par exemple, vous devez utiliser
IS NULL à la place de = NULL. De plus, la base de données Oracle a certains
comportements étranges avec NULL, tout d’abord parce qu’elle ne gère pas
toujours NULL comme le spécifie le standard, mais aussi parce qu’elle a une
gestion très « spéciale » de NULL dans les index.

Le standard SQL ne définit pas NULL comme une valeur mais plutôt comme
un marqueur pour une valeur manquante ou inconnue. En conséquence,
aucune valeur ne peut être NULL. Étonnement, la base de données Oracle
traite les chaînes de caractères vides comme des NULL :

SELECT '0 EST NULL???' AS "qu'est-ce qui est NULL ?" FROM dual
WHERE 0 IS NULL
UNION ALL
SELECT '0 n''est pas NULL' FROM dual
WHERE 0 IS NOT NULL
UNION ALL
SELECT ''''' EST NULL???' FROM dual
WHERE '' IS NULL
UNION ALL
SELECT ''''' n''est pas null' FROM dual
WHERE '' IS NOT NULL

qu'est-ce qui est NULL ?


----------------------------
0 n'est pas NULL
'' EST NULL???

Pour ajouter à la confusion, il existe même un cas où la base de données


Oracle traite NULL comme une chaîne de caractères vide :

SELECT dummy
, dummy || ''
, dummy || NULL
FROM dual
D D D
- - -
X X X

Concaténer la colonne DUMMY (contenant toujours 'X') avec NULL devrait


renvoyer NULL.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 53


chapitre 2 : La clause Where

Le concept du NULL est utilisé dans un grand nombre de langages de


programmation. Peu importe où vous regardez, une chaîne de caractères
vide n’est jamais NULL… sauf dans la base de données Oracle. En fait, il est
impossible d’enregistrer une chaîne vide dans un champ VARCHAR2. Si vous
essayez, la base de données Oracle enregistre à la base un NULL.

Cette spécificité n’est pas seulement étrange, elle est aussi dangereuse. De
plus, le comportement bizarre de la base de données Oracle avec les NULL
ne s’arrête pas là. Il continue avec l’indexation.

Indexer NULL
La base de données Oracle n’inclut pas les lignes dans un index si toutes les
colonnes indexées sont NULL. Cela signifie que chaque index est un index
partiel, comme si la clause where suivante était ajoutée :

CREATE INDEX idx


ON tbl (A, B, C, ...)
WHERE A IS NOT NULL
OR B IS NOT NULL
OR C IS NOT NULL
...;

Considérez l’index EMP_DDN. Il a une seule colonne : DATE_DE_NAISSANCE. Une


ligne qui n’a pas de valeur pour la colonne DATE_DE_NAISSANCE n’est pas
ajoutée dans cet index.

INSERT INTO employes ( id_supplementaire, id_employe


, prenom , nom
, numero_telephone)
VALUES ( ?, ?, ?, ?, ? );

La requête insert n’initialise pas la valeur de la colonne DATE_DE_NAISSANCE,


donc par défaut elle vaut NULL. De ce fait, l’enregistrement n’est pas
ajouté dans l’index EMP_DDN. En conséquence, l’index ne peut pas supporter
une requête qui recherche les enregistrements pour lesquels la colonne
DATE_DE_NAISSANCE est NULL :

SELECT prenom, nom


FROM employes
WHERE date_de_naissance IS NULL

54 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Indexer NULL

----------------------------------------------------
| Id | Operation | Name | Rows | Cost |
----------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 477 |
|* 1 | TABLE ACCESS FULL| EMPLOYES | 1 | 477 |
----------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("DATE_DE_NAISSANCE" IS NULL)

Néanmoins, l’enregistrement est inséré dans un index concaténé si au


moins une colonne de l’index n’est pas NULL :

CREATE INDEX demo_null


ON employes (id_supplementaire, date_de_naissance);

La ligne créée ci-dessus est ajoutée dans l’index car ID_SUPPLEMENTAIRE n’est
pas NULL. Du coup, cet index peut supporter une requête récupérant les
lignes pour lesquelles tous les employés sont d’une société spécifique mais
n’ont pas de valeur sur DATE_DE_NAISSANCE :

SELECT prenom, nom


FROM employes
WHERE id_supplementaire = ?
AND date_de_naissance IS NULL
--------------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
--------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 2 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES | 1 | 2 |
|* 2 | INDEX RANGE SCAN | DEMO_NULL | 1 | 1 |
--------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("ID_SUPPLEMENTAIRE"=TO_NUMBER(?)
AND "DATE_DE_NAISSANCE" IS NULL)

Notez que l’index couvre complètement la clause where ; tous les filtres sont
utilisés comme prédicats d’accès lors de l’opération INDEX RANGE SCAN.

Nous pouvons étendre ce concept à la requête originale pour trouver tous


les enregistrements où DATE_DE_NAISSANCE est NULL. Pour cela, la colonne
DATE_DE_NAISSANCE doit être la colonne la plus à gauche dans la définition
de l’index pour qu’elle puisse être utilisée comme prédicat d’accès. Bien

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 55


chapitre 2 : La clause Where

que nous n’ayons pas besoin d’une deuxième colonne dans l’index pour
cette requête, nous ajoutons une autre colonne qui ne peut jamais être NULL
pour s’assurer que l’index a toutes les lignes. Nous pouvons utiliser toute
colonne qui a une contrainte NOT NULL, comme ID_SUPPLEMENTAIRE.

Autrement, nous pouvons utiliser une expression constante qui ne peut


jamais être NULL. Cela nous assure que l’index a toutes les lignes, même si
DATE_DE_NAISSANCE est NULL.

DROP INDEX emp_ddn;


CREATE INDEX emp_ddn ON employes (date_de_naissance, '1');

Techniquement, cet index est un index fonctionnel. De plus, cet exemple


démontre que le mythe selon lequel la base de données Oracle ne peut pas
indexer les NULL est erroné.

Astuce
Ajouter une colonne qui ne peut pas être NULL pour indexer NULL
comme toutes les autres valeurs.

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

56 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Contraintes 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_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;

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 57


chapitre 2 : La clause Where

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. 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.

58 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Contraintes NOT NULL

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

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 59


chapitre 2 : La clause Where

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

Émuler des index partiels


La façon étrange avec laquelle la base de données Oracle gère le NULL dans
les index peut être utilisée pour émuler les index partiels. Pour cela, nous
devons juste utiliser NULL pour les lignes que nous ne devons pas indexer.

Pour le démontrer, nous émulons l’index partiel suivant :

CREATE INDEX messages_atraiter


ON messages (destinataire)
WHERE traite = 'N'

Tout d’abord, nous avons besoin d’une fonction qui renvoie la valeur
seulement si colonne TRAITE vaut 'N'.

CREATE OR REPLACE
FUNCTION pi_traites(traite CHAR, destinataire NUMBER)
RETURN NUMBER
DETERMINISTIC
AS BEGIN
IF traite IN ('N') THEN
RETURN destinataire;
ELSE
RETURN NULL;
END IF;
END;
/

La fonction doit être déterministe pour qu’elle soit utilisable dans une
définition d’index.

60 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Émuler des index partiels

Maintenant, nous pouvons traiter un index qui contient seulement des


lignes ayant TRAITE='N'.

CREATE INDEX messages_atraiter


ON messages (pi_traites(traite, destinataire));

Pour utiliser l’index, vous devez utiliser l’expression indexée dans la


requête :

SELECT message
FROM messages
WHERE pi_traites(traite, destinataire) = ?

--------------------------------------------------------------
|Id | Operation | Name | Cost |
--------------------------------------------------------------
| 0 | SELECT STATEMENT | | 5330 |
| 1 | TABLE ACCESS BY INDEX ROWID| MESSAGES | 5330 |
|*2 | INDEX RANGE SCAN | MESSAGES_ATRAITER | 5303 |
--------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("PI_TRAITES"("TRAITE","DESTINATAIRE")=:X)

Index partiels, partie II


À partir de la version 11g, il existe une seconde approche, toute aussi
effrayante, pour émuler les index partiels dans la base de données
Oracle, en utilisant une partition d’index cassé intentionnellement et
le paramètre SKIP_UNUSABLE_INDEX.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 61


chapitre 2 : La clause Where

Conditions cachées
Les sections suivantes montrent quelques méthodes populaires pour
cacher des conditions. Les conditions cachées sont des clauses where
écrites d’une façon qui empêche une utilisation correcte des index. Cette
section est une collection de mauvaises pratiques que tout développeur
doit connaître et éviter.

Types date
La plupart de ces pratiques impliquent les types DATE. La base de données
Oracle est particulièrement vulnérable car elle n’a qu’un type DATE qui
inclut toujours en plus un composant horaire.

Une pratique fréquente revient à utiliser la fonction TRUNC pour supprimer


le composant horaire. En vérité, cela ne le supprime pas, cela le configure
plutôt à minuit car la base de données Oracle n’a pas de vrai type DATE.
Pour supprimer le composant horaire pour une recherche, vous pouvez
utiliser la fonction TRUNC des deux côtés d’une comparaison, par exemple
pour rechercher les ventes de la veille :

SELECT ...
FROM ventes
WHERE TRUNC(date_vente) = TRUNC(sysdate - INTERVAL '1' DAY)

Cette requête est parfaitement valide et correcte mais elle ne peut pas
utiliser un index sur DATE_VENTE. Cela correspond à l’explication dans
« Recherche insensible à la casse en utilisant UPPER ou LOWER » à la page 24 ;
TRUNC(date_vente) est complètement différent de DATE_VENTE. Les fonctions
sont des boîtes noires pour la base de données.

Il existe une solution assez simple pour ce problème : un index fonctionnel.

CREATE INDEX nom_index


ON ventes (TRUNC(date_vente))

Mais du coup, vous devez toujours utiliser TRUNC(date_vente) dans la clause


where. Si ce n’est pas le cas, parfois avec, parfois sans TRUNC, vous aurez
alors besoin de deux index !

62 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Types date

Le problème survient aussi avec les bases de données qui ont des types de
date purs si vous recherchez une période plus longue comme le montre la
requête MySQL suivante :

SELECT ...
FROM ventes
WHERE DATE_FORMAT(date_vente, "%Y-%M")
= DATE_FORMAT(now() , "%Y-%M')

La requête utilise un format de date qui contient seulement l’année et le


mois : encore une fois, cette requête est tout à fait correcte mais elle a le
même problème que précédemment. Néanmoins, la solution ci-dessus ne
s’applique pas à MySQL avant la version 5.7 parce que MySQL ne supporte
pas les index fonctionnels avant cette version.

L’alternative revient à utiliser une condition d’intervalle explicite. Voici une


solution générique qui fonctionne pour toutes les bases de données :

SELECT ...
FROM ventes
WHERE date_vente BETWEEN DEB_TRIMESTRE(?)
AND FIN_TRIMESTRE(?)

Si vous avez fait vos devoirs, vous reconnaissez probablement la signature


de l’exercice sur les employés de 42 ans.

Un index sur DATE_VENTE est suffisant pour optimiser cette requête. Les
fonctions DEB_TRIMESTRE et FIN_TRIMESTRE calculent les dates limites. Le
calcul peut devenir un peu complexe car l’opérateur between inclut toujours
les valeurs limites. Du coup, la fonction FIN_TRIMESTRE doit renvoyer un
horodatage juste avant le premier jour du prochain trimestre si DATE_VENTE
a une composante horaire. Cette logique peut se cacher dans la fonction.

Les exemples sur les pages suivantes montrent l’implémentation des


fonctions DEB_TRIMESTRE et FIN_TRIMESTRE pour les différentes bases de
données.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 63


chapitre 2 : La clause Where

MySQL

CREATE FUNCTION deb_trimestre(dt DATETIME)


RETURNS DATETIME DETERMINISTIC
RETURN CONVERT
(
CONCAT
( CONVERT(YEAR(dt),CHAR(4))
, '-'
, CONVERT(QUARTER(dt)*3-2,CHAR(2))
, '-01'
)
, datetime
);
CREATE FUNCTION fin_trimestre(dt DATETIME)
RETURNS DATETIME DETERMINISTIC
RETURN DATE_ADD
( DATE_ADD ( deb_trimestre(dt), INTERVAL 3 MONTH )
, INTERVAL -1 MICROSECOND);

Oracle Database

CREATE FUNCTION deb_trimestre(dt IN DATE)


RETURN DATE
AS
BEGIN
RETURN TRUNC(dt, 'Q');
END;
/
CREATE FUNCTION fin_trimestre(dt IN DATE)
RETURN DATE
AS
BEGIN
-- le type DATE Oracle a une résolution à la seconde
-- soustraire une seconde à la première date du
-- trimestre suivant
RETURN TRUNC(ADD_MONTHS(dt, +3), 'Q')
- (1/(24*60*60));
END;
/

64 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Types date

PostgreSQL

CREATE FUNCTION deb_trimestre(dt timestamp with time zone)


RETURNS timestamp with time zone AS $$
BEGIN
RETURN date_trunc('quarter', dt);
END;
$$ LANGUAGE plpgsql;

CREATE FUNCTION fin_trimestre(dt timestamp with time zone)


RETURNS timestamp with time zone AS $$
BEGIN
RETURN date_trunc('quarter', dt)
+ interval '3 month'
- interval '1 microsecond';
END;
$$ LANGUAGE plpgsql;

SQL Server

CREATE FUNCTION deb_trimestre (@dt DATETIME )


RETURNS DATETIME
BEGIN
RETURN DATEADD (qq, DATEDIFF (qq, 0, @dt), 0)
END
GO
CREATE FUNCTION fin_trimestre (@dt DATETIME )
RETURNS DATETIME
BEGIN
RETURN DATEADD
( ms
, -3
, DATEADD(mm, 3, dbo.deb_trimestre(@dt))
);
END
GO

Vous pouvez utiliser des fonctions auxiliaires similaires pour les autres
périodes. La plupart seront moins complexes que les exemples ci-dessus,
tout spécialement lors de l’utilisation des conditions supérieur ou égal
(>=) ou plus petit que (<) à la place de l’opérateur between. Bien sûr,
vous pourriez calculer les dates limites dans votre application si vous le
souhaitez.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 65


chapitre 2 : La clause Where

Astuce
Écrivez des périodes continues pour des conditions d’intervalle
explicites. Faites-le même pour un seul jour. Par exemple, avec la base
de données Oracle :

date_vente >= TRUNC(sysdate)


AND date_vente < TRUNC(sysdate + INTERVAL '1' DAY)

Un autre problème fréquent est de comparer les dates en tant que chaînes
de caractères, comme indiqué dans l’exemple PostgreSQL suivant:

SELECT ...
FROM ventes
WHERE TO_CHAR(date_vente, 'YYYY-MM-DD') = '1970-01-01'

Encore une fois, le problème réside en la conversion de la colonne


date_vente. Ce genre de conditions survient souvent de la croyance que
vous ne pouvez pas passer d’autres types que des nombres et des chaînes
à la base de données. Néanmoins, les paramètres liés supportent tous
les types de données. Cela signifie par exemple que vous pouvez utiliser
un objet java.util.Date comme paramètre lié. C’est un autre bénéfice
qu’apportent les paramètres liés.

Si vous ne pouvez pas le faire, vous devez seulement convertir le terme de


recherche plutôt que la colonne de la table:

SELECT ...
FROM ventes
WHERE date_vente = TO_DATE('1970-01-01', 'YYYY-MM-DD')

Cette requête peut utiliser un index simple sur DATE_VENTE. De plus, elle
convertit la chaîne en entrée une seule fois. La requête précédente doit
convertir toutes les dates stockées dans la table avant de pouvoir les
comparer avec le terme de recherche.

Quelle que soit la modification que vous faites, en utilisant un paramètre lié
ou en convertissant l’autre côté de la comparaison, vous pouvez facilement
introduire un bug si DATE_VENTE a une composante horaire. Vous devez
utiliser une condition d’intervalle explicite dans ce cas :

66 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Types date

SELECT ...
FROM ventes
WHERE date_vente >= TO_DATE('1970-01-01', 'YYYY-MM-DD')
AND date_vente < TO_DATE('1970-01-01', 'YYYY-MM-DD')
+ INTERVAL '1' DAY

Prenez toujours en compte l’utilisation d’une condition d’intervalle


explicite lors de la comparaison de dates.

LIKE sur les types date


Le problème suivant est assez vicieux :

date_vente LIKE SYSDATE

Cela ne semble pas poser problème au premier coup d’œil car aucune
fonction n’est utilisée.
Néanmoins, l’opérateur LIKE force une comparaison de chaîne de
caractères. Suivant la base de données, cela peut amener une erreur
ou forcer une conversion implicite de type des deux côtés. La section
« Predicate Information » du plan d’exécution montre ce que fait la
base de données Oracle :

filter( INTERNAL_FUNCTION(DATE_VENTE)
LIKE TO_CHAR(SYSDATE@!))

La fonction INTERNAL_FUNCTION convertit le type de la colonne


DATE_VENTE. Le deuxième effet de bord est l’impossibilité d’utiliser un
index simple sur COLONNE_DATE comme avec toute autre fonction.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 67


chapitre 2 : La clause Where

Chaînes numériques
Les chaînes numériques sont des nombres enregistrées dans des colonnes
textes. Bien que cette pratique soit très mauvaise, elle ne rend pas l’index
inutilisable si vous les traitez en permanence comme des chaînes :

SELECT ...
FROM ...
WHERE chaine_numerique = '42'

Bien sûr, cette requête peut utiliser un index sur CHAINE_NUMERIQUE.


Néanmoins, si vous la comparez à un nombre, la base de données ne peut
plus utiliser cette condition comme prédicat d’accès à la page 44.

SELECT ...
FROM ...
WHERE chaine_numerique = 42

Notez les guillemets manquants. Bien que certaines bases de données


renvoient une erreur (par exemple PostgreSQL), certaines bases de données
ajoutent simplement une conversion implicite à cause de la différence de
type.

SELECT ...
FROM ...
WHERE TO_NUMBER(chaine_numerique) = 42

Nous avons ici le même problème qu’avant. Un index sur CHAINE_NUMERIQUE


ne peut pas être utilisé à cause de l’appel de fonction. La solution est
identique : ne convertissez pas la colonne de la table, mais convertissez le
terme de la recherche.

SELECT ...
FROM ...
WHERE chaine_numerique = TO_CHAR(42)

Vous pourriez vous demander pourquoi la base de données ne le fait pas


ainsi automatiquement ? C’est dû au fait que convertir une chaîne en
nombre donne toujours un résultat non ambigu. Le contraire n’est pas vrai.
Un nombre, formaté en texte, peut contenir des espaces, de la ponctuation
et des zéros en tête. Une seule valeur peut être écrite de plusieurs façons :

68 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Chaînes numériques

42
042
0042
00042
...

La base de données ne connaît pas le format numérique utilisé dans la


colonne CHAINE_NUMERIQUE, donc selon la logique inverse, la base de données
convertit les chaînes en nombres car la transformation n’est pas ambigüe.

La fonction TO_CHAR renvoie seulement une représentation textuelle du


nombre. Du coup, cela correspondra seulement à la première des chaînes
listées ci-dessus. Si nous utilisons TO_NUMBER, cela correspond à toutes
les chaînes. Cela signifie qu’il n’y a pas seulement une différence
de performances entre les deux variantes mais aussi une différence
sémantique !

Utiliser des chaînes numériques est généralement problématique : cela


cause surtout des problèmes de performances dus à la conversion implicite
mais cela introduit aussi un risque d’erreurs de conversion pour les
nombres invalides. Même la requête la plus triviale qui n’utilise aucune
fonction dans la clause where peut déclencher une annulation sur une
erreur de conversion s’il y a ne serait-ce qu’un seul nombre invalide stocké
dans la table.

Astuce
Utilisez les types numériques pour stocker des nombres.

Notez que le problème n’existe pas dans l’autre sens :

SELECT ...
FROM ...
WHERE nombre_numerique = '42'

La base de données va transformer de façon cohérente la chaîne en un


nombre. Elle n’applique pas une fonction sur la colonne potentiellement
indexée : du coup, un index standard fonctionnera. Néanmoins, il est
possible de mal faire une conversion manuelle :

SELECT ...
FROM ...
WHERE TO_CHAR(nombre_numerique) = '42'

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 69


chapitre 2 : La clause Where

Combiner des colonnes


Cette section porte sur un problème fréquent qui affecte les index
concaténés.

Encore une fois, le premier exemple est sur les types date et heure. La
requête MySQL suivante combine une colonne date et heure pour appliquer
un filtre d’intervalle sur les deux.

SELECT ...
FROM ...
WHERE ADDTIME(colonne_date, colonne_heure)
> DATE_ADD(now(), INTERVAL -1 DAY)

Elle sélectionne tous les enregistrements des dernières 24 heures.


La requête ne peut pas utiliser un index concaténé (COLONNE_DATE,
COLONNE_HEURE) correctement parce que la recherche n’est pas faite sur les
colonnes indexées mais sur des données dérivées.

Vous pouvez éviter ce problème en utilisant un type de données qui a les


deux composants date et heure (par exemple, le DATETIME de MySQL). Ainsi,
vous pouvez utiliser cette colonne sans un appel de fonction :

SELECT ...
FROM ...
WHERE colonne_dateheure
> DATE_ADD(now(), INTERVAL -1 DAY)

Malheureusement, il est souvent impossible de modifier la table lorsqu’on


rencontre ce problème.

L’option suivante est un index fonctionnel si la base de données le supporte


bien que cela ait tous les inconvénients discutés précédemment. De plus,
avec MySQL, les index fonctionnels ne sont pas une option.

Il est toujours possible d’écrire la requête de telle façon que la base de


données puisse utiliser un index concaténé sur COLONNE_DATE, COLONNE_HEURE
avec un prédicat d’accès à la page 44, au moins partiellement. Pour cela,
nous ajoutons une condition supplémentaire sur COLONNE_DATE.

WHERE ADDTIME(colonne_date, colonne_heure)


> DATE_ADD(now(), INTERVAL -1 DAY)
AND colonne_date
>= DATE(DATE_ADD(now(), INTERVAL -1 DAY))

70 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Combiner des colonnes

La nouvelle condition est complètement redondante mais elle est un


filtre direct sur COLONNE_DATE, et donc elle peut être utilisée comme
prédicat d’accès. Même si cette technique n’est pas parfaite, il s’agit d’une
approximation suffisante.

Astuce
Utilisez une condition redondante sur la colonne la plus significative
lorsqu’une condition d’intervalle combine plusieurs colonnes.
Pour PostgreSQL, il est préférable d’utiliser la syntaxe des lignes
décrite sur la page 151.

Vous pouvez aussi utiliser cette technique lors du stockage de dates et


heures dans des colonnes texte, mais vous devez utiliser les formats date et
heure qui amènent un ordre chronologique lors d’un tri lexical, autrement
dit comme suggéré par le standard ISO 8601 (YYYY-MM-DD HH:MM:SS).
L’exemple suivant utilise la fonction TO_CHAR de la base de données Oracle
dans ce but :

SELECT ...
FROM ...
WHERE chaine_date || chaine_heure
> TO_CHAR(sysdate - 1, 'YYYY-MM-DD HH24:MI:SS')
AND chaine_date
>= TO_CHAR(sysdate - 1, 'YYYY-MM-DD')

Nous rencontrons de nouveau le problème de l’application d’une condition


d’intervalle sur plusieurs colonnes dans la section titrée « Parcourir les
résultats ». Nous allons aussi utiliser la même méthode d’approximation.

Quelques fois, nous avons le cas inverse et voulons cacher une condition
de façon intentionnelle pour qu’elle ne soit plus utilisée comme prédicat
d’accès. Nous avons déjà regardé ce problème lors de la discussion sur
les effets des paramètres liés sur les conditions LIKE. Prenons l’exemple
suivant :

SELECT nom, prenom, id_employe


FROM employes
WHERE id_supplementaire = ?
AND nom LIKE ?

En supposant qu’il y ait un index sur ID_SUPPLEMENTAIRE et un autre sur NOM,


lequel est préférable pour cette requête ?

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 71


chapitre 2 : La clause Where

Sans connaître la position du caractère joker dans le terme de recherche,


il est impossible de donner une réponse justifiée. L’optimisateur n’a pas
d’autres choix que de deviner. Si vous savez qu’il y a toujours un caractère
joker en début, vous pouvez changer la condition LIKE de façon à ce que
l’optimiseur ne considère plus la possibilité d’utiliser un index sur NOM.

SELECT nom, prenom, id_employe


FROM employes
WHERE id_supplementaire = ?
AND nom || '' LIKE ?

Il suffit d’ajouter une chaîne vide à la colonne NOM. Néanmoins, c’est la


solution de la dernière chance. Il ne faut le faire qu’en cas d’absolue
nécessité.

Logique intelligente

Une des fonctionnalités clés des bases de données SQL est leur support
des requêtes personnalisées : de nouvelles requêtes peuvent être exécutées
à tout moment. Ceci est seulement possible parce que l’optimiseur de
requêtes (planificateur de requêtes) fonctionne à l’exécution ; il analyse
chaque requête à sa réception et génère un plan d’exécution raisonnable
immédiatement. La surcharge introduite par l’optimisation à l’exécution
peut être minimisée avec les paramètres liés.

L’intérêt de ceci est que les bases de données sont optimisées pour le SQL
dynamique. Utilisez-le si vous en avez besoin.

Néanmoins, une pratique fréquemment répondue est d’éviter le SQL


dynamique et de privilégier le SQL statique, principalement à cause du
mythe indiquant que « le SQL dynamique est lent ». Cette pratique fait plus
de mal que de bien si la base de données utilise un cache partagé de plan
d’exécution comme DB2, Oracle ou SQL Server.

Pour cette démonstration, imaginons une application qui exécute des


requêtes sur la table EMPLOYES. L’application permet la recherche par
identifiant de société, identifiant d’employé et nom (non sensible à la
casse), toute combinaison acceptée. Il est possible d’écrire une seule
requête qui couvre tous les cas en utilisant la logique « intelligente ».

72 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Logique intelligente

SELECT prenom, nom, id_supplementaire, id_employe


FROM employes
WHERE ( id_supplementaire = :id_supp OR :id_supp IS NULL )
AND ( id_employe = :id_emp OR :id_emp IS NULL )
AND ( UPPER(nom) = :nom OR :nom IS NULL )

La requête utilise les variables liées nommées pour faciliter la lecture.


Toutes les expressions possibles de filtres sont codées statiquement dans
la requête. Quand un filtre n’est plus nécessaire, vous pouvez simplement
utiliser NULL à la place du terme de recherche : cela désactive la condition
via la logique OR.

Cette requête SQL est parfaitement raisonnable. L’utilisation de NULL est


même en ligne avec sa définition suivant la logique à trois valeurs du SQL.
Néanmoins, c’est un des pires problèmes de performances.

La base de données ne peut pas optimiser le plan d’exécution pour un filtre


particulier parce que tous peuvent être annulés à l’exécution. La base de
données a besoin de préparer la requête pour le pire des cas : si tous les
filtres sont désactivés :

----------------------------------------------------
| Id | Operation | Name | Rows | Cost |
----------------------------------------------------
| 0 | SELECT STATEMENT | | 2 | 478 |
|* 1 | TABLE ACCESS FULL| EMPLOYES | 2 | 478 |
----------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter((:NOM IS NULL OR UPPER("NOM")=:NOM)
AND (:ID_EMP IS NULL OR "ID_EMPLOYE"=:ID_EMP)
AND (:ID_SUPP IS NULL OR "ID_SUPPLEMENTAIRE"=:ID_SUPP))

En conséquence, la base de données utilise un parcours de table complet


même s’il existe un index pour chaque colonne.

Ce n’est pas que la base de données ne peut pas résoudre la logique


« intelligente ». Elle crée un plan d’exécution générique à cause de
l’utilisation des variables liées pour qu’elle puisse être mise en cache
et ré-utilisée avec d’autres valeurs plus tard. Si nous n’utilisons pas de
paramètres liés mais qu’à la place, nous écrivons les valeurs réelles dans la
requête SQL, l’optimiseur sélectionne le bon index pour le filtre actif.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 73


chapitre 2 : La clause Where

SELECT prenom, nom, id_supplementaire, id_employe


FROM employes
WHERE( id_supplementaire = NULL OR NULL IS NULL )
AND( id_employe = NULL OR NULL IS NULL )
AND( UPPER(nom) = 'WINAND' OR 'WINAND' IS NULL )
---------------------------------------------------------------
|Id | Operation | Name | Rows | Cost |
---------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 2 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES | 1 | 2 |
|*2 | INDEX RANGE SCAN | NOM_MAJ_EMP | 1 | 1 |
---------------------------------------------------------------

Predicate Information (identified by operation id):


---------------------------------------------------
2 - access(UPPER("NOM")='WINAND')

Néanmoins, ce n’est pas une solution. Cela prouve seulement que la base
de données peut résoudre ces conditions.

Avertissement
Utiliser des valeurs littérales rend votre application vulnérable
aux attaques par injection SQL et peut causer des problèmes de
performances à cause de la surcharge du à une optimisation accrue.

La solution évidente pour les requêtes dynamiques est le SQL dynamique.


5
Suivant le principe KISS , dites simplement à la base de données ce dont
vous avez besoin maintenant et rien d’autres.

SELECT prenom, nom, id_supplementaire, id_employe


FROM employes
WHERE UPPER(nom) = :nom

Notez que la requête utilise un paramètre lié.

Astuce
Utilisez le SQL dynamique si vous avez besoin de clauses where
dynamiques.
Utilisez toujours des paramètres liés lors de la génération de SQL
dynamique. Dans le cas contraire, le mythe du « SQL dynamique
lent » devient réalité.

5
https://fr.wikipedia.org/wiki/Principe_KISS

74 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Logique intelligente

Le problème décrit dans cette section est très répandu. Toutes les bases
de données qui utilisent un cache partagé de plans d’exécution ont une
fonctionnalité pour essayer de contourner ce problème. Cela introduit
souvent de nouveaux problèmes, voire des bugs.

MySQL
MySQL ne souffre pas de ce problème particulier car il ne dispose
pas d’un cache de plans d’exécution. Une demande d’ajout de
fonctionnalité datant de 2009 discute de l’impact de la mise en
cache du plan d’exécution. Il semble que l’optimiseur de MySQL est
suffisamment simple pour que la mise en cache du plan d’exécution ne
soit pas d’un grand intérêt.

Oracle Database
La base de données Oracle utilise un cache partagé de plans d’exécution
(« SQL area ») et est complètement exposée au problème décrit dans
cette section.

Oracle a introduit une fonctionnalité appelée bind peeking avec la


version 9i. Le « Bind peeking » permet à l’optimiseur d’utiliser les
vraies valeurs de la première exécution lors de la préparation d’un plan
d’exécution. Le problème de cette approche est son comportement non
déterministe : les valeurs provenant de la première exécution affectent
toutes les exécutions. Le plan d’exécution peut changer à chaque
redémarrage de la base de données ou, de façon moins prévisible, le
plan en cache expire et l’optimiseur le recrée en utilisant différentes
valeurs la prochaine fois que la requête est exécutée.

La version 11g a introduit le partage de curseur adaptatif pour améliorer


encore plus la situation. Cette fonctionnalité rend possible la mise en
cache par la base de données de plusieurs plans d’exécution pour la
même requête SQL. De plus, l’optimiseur récupère les paramètres liés
et stockent leur sélectivité estimée avec le plan d’exécution. Quand
le cache est accédé par la suite, la sélectivité des valeurs liées doit
faire partie de l’échelle de sélectivité d’un plan d’exécution pour cette
requête. Si un tel plan d’exécution existe déjà, la base de données
le remplace avec le nouveau plan d’exécution qui couvre aussi les
estimations de sélectivité des valeurs liées actuelles. Dans le cas
contraire, elle met en cache une nouvelle variante du plan d’exécution
pour cette requête, avec les estimations de sélectivité.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 75


chapitre 2 : La clause Where

PostgreSQL
Le cache de plans d’exécutions de PostgreSQL fonctionne seulement
pour les requêtes ouvertes, autrement dit aussi longtemps que le
PreparedStatement est ouvert. Le problème décrit ci-dessus survient
seulement lors de la réutilisation d’un pointeur de requête. Notez que
le connecteur JDBC de PostgreSQL active le cache seulement après la
cinquième exécution.

SQL Server
SQL Server utilise une méthode appelée écoute de paramètres. L’écoute
de paramètre permet à l’optimiseur d’utiliser les valeurs liées réelles de
la première exécution lors de l’analyse. Le problème de cette approche
est son comportement non déterministe : les valeurs provenant de la
première exécution affectent toutes les exécutions. Le plan d’exécution
peut changer à chaque redémarrage de la base de données ou, de
façon moins prévisible, le plan en cache expire et l’optimiseur le recrée
en utilisant différentes valeurs la prochaine fois que la requête est
exécutée.

SQL Server 2005 a ajouté de nouvelles options de requêtes pour avoir


un contrôle plus fin sur l’écoute de paramètres et sur la recompilation.
L’astuce de requête RECOMPILE contourne le cache de plans pour une
requête particulière. OPTIMIZE FOR permet la spécification de vraies
valeurs pour les paramètres, utilisées seulement pour l’optimisation.
Enfin, vous pouvez fournir un plan d’exécution complet avec l’option
USE PLAN.

L’implémentation originale de l’option OPTION(RECOMPILE) avait un bug.


Du coup, il ne considérait pas toutes les variables liées. La nouvelle
implémentation proposée avec SQL Server 2008 a un autre bug, rendant
6
la situation très confuse. Erland Sommarskog a collectionné toutes les
informations intéressantes sur les différentes versions de SQL Server.

Bien que les méthodes heuristiques puissent améliorer le problème de la


« logique intelligente » jusqu’à un certain point, elles ont été conçues
pour gérer les problèmes des paramètres liés en connexion avec les
histogrammes de colonnes et les expressions LIKE.

La méthode la plus fiable pour arriver au meilleur plan d’exécution est


d’éviter les filtres inutiles dans la requête SQL.

6
http://www.sommarskog.se/dyn-search-2008.html

76 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Mathématiques

Mathématiques
Il existe un dernier type de problème intelligent qui empêche l’utilisation
correcte des index. Au lieu d’utiliser les expressions logiques, cela passe par
les calculs.

Considérons la requête suivante. Peut-elle utiliser un index sur


NOMBRE_NUMERIQUE ?

SELECT nombre_numerique
FROM nom_table
WHERE nombre_numerique - 1000 > ?

De façon similaire, la requête suivante peut-elle utiliser un index sur A et


B (vous choisissez l’ordre) ?

SELECT a, b
FROM nom_table
WHERE 3*a + 5 = b

Prenons un angle de vue différent. Si vous développez un moteur de bases


de données SQL, ajouteriez-vous un module de résolution d’équations ? La
plupart des développeurs de bases de données diront simplement non. Et
du coup, aucun des deux exemples ci-dessus ne peut utiliser un index.

Vous pouvez même utiliser les mathématiques pour rendre difficile


l’utilisation d’un index sur une condition, de façon intentionnelle, comme
nous l’avions fait précédemment pour la recherche plein texte avec LIKE. Il
suffit d’ajouter zéro, comme dans cet exemple :

SELECT nombre_numerique
FROM nom_table
WHERE nombre_numerique + 0 = ?

Néanmoins, nous pouvons indexer ces expressions avec un index


fonctionnel si nous utilisons des calculs d’une façon intelligente et si nous
transformons la clause where comme une équation :

SELECT a, b
FROM nom_table
WHERE 3*a - b = -5

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 77


chapitre 2 : La clause Where

Nous avons déplacé les références à la table d’un côté et les constantes
de l’autre. Du coup, nous pouvons créer un index fonctionnel pour le côté
gauche de l’équation :

CREATE INDEX math ON nom_table (3*a - b)

78 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Chapitre 3

Performance et scalabilité
Ce chapitre porte sur les performances et la scalabilité des bases de
données.

Dans ce contexte, j’utilise la définition suivante de la scalabilité :

Le mot anglais « scalability » désigne la capacité d’un produit à


s’adapter à un changement d’ordre de grandeur de la demande
(montée en charge). En particulier sa capacité à maintenir ses
fonctionnalités et ses performances en cas de forte demande.
1
—Wikipedia

Vous voyez ici deux définitions. La première concerne les effets d’une
charge grandissante et la seconde concerne un système qui grossit pour
supporter une charge plus importante.

La deuxième définition est bien plus populaire que la première. Quand une
personne parle de scalabilité, cela concerne presque toujours la possibilité
d’utiliser plus de matériel. Les termes Scale-up et scale-out sont les mots
clés respectifs de ces deux définitions. Ils ont été complétés récemment par
des mots très tendance comme web-scale.

En gros, la scalabilité concerne l’impact des performances par rapport


à des changements dans l’environnement. Le matériel est un paramètre
environnemental qui peut changer. Ce chapitre couvre d’autres paramètres
comme le volume de données et la charge du système.

1
https://fr.wikipedia.org/wiki/Scalability

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 79


chapitre 3 : Performance et scalabilité

L’impact du volume de données


sur les performances
La quantité de données enregistrées dans une base de données a un gros
impact sur ses performances. Il est généralement accepté qu’une requête
devienne lente quand il y a plus de données dans la base de données. Mais
quel est le niveau de l’impact sur les performances quand le volume de
données double ? Et comment pouvons-nous améliorer ce ratio ? Voici les
questions clés autour de la scalabilité des bases de données.

Comme exemple, nous allons analyser le temps de réponse de la requête


suivante lors de l’utilisation de deux index différents. Les définitions des
index resteront inconnues pour l’instant, mais elles seront révélées au fil
de la discussion.

SELECT count(*)
FROM grosses_donnees
WHERE section = ?
AND id2 = ?

La colonne SECTION a un but précis dans cette requête : elle contrôle le


volume de données. Plus le nombre SECTION devient grand, plus la requête
sélectionne de lignes. La Figure 3.1 montre le temps de réponse pour une
petite SECTION.

Figure 3.1. Comparaison de performances

0.10 0.10
Temps de réponse [sec]

Temps de réponse [sec]

0.08 0.08
0.06 0.06
0.04 0.04
0.02 0.02
0.00 0.00
rapide lent
0.029s 0.055s

Il existe une différence considérable sur les performances avec les deux
variantes d’index. Ces temps de réponse sont toujours bien en-dessous
d’un dixième de seconde, donc même la requête la plus lente est assez
rapide dans la plupart des cas. Néanmoins, le graphe des performances

80 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


L’impact du volume de données sur les performances

montre seulement un point de test. Discuter de la scalabilité signifie


regarder l’impact des performances lors de changements de paramètres
environnementaux, comme le volume de données.

Important
La scalabilité montre la dépendance des performances sur des
facteurs tels que le volume de données.
Une valeur de performance est un simple point de données sur un
graphe de scalabilité.

La Figure 3.2 montre le temps de réponse par rapport à la SECTION,


autrement dit pour un volume de données grossissant.

Figure 3.2. Scalabilité par le volume des données


lent rapide
1.2 1.2
Temps de réponse [sec]

Temps de réponse [sec]


1.0 1.0

0.8 0.8

0.6 0.6

0.4 0.4

0.2 0.2

0.0 0.0
0 20 40 60 80 100
Volume de données [section]

Le graphe affiche un temps de réponse augmentant pour chaque index.


Sur le côté droit du graphe, quand le volume de données est cent fois plus
important, la requête la plus rapide a besoin de deux fois plus de temps
alors que le temps de réponse de la requête la plus lente a augmenté d’un
facteur de 20 (plus d’une seconde).

Le temps de réponse d’une requête SQL dépend de nombreux facteurs. Le


volume de données en est un. Si une requête est suffisamment rapide sous
certaines conditions de tests, cela ne signifie pas pour autant qu’elle sera
suffisamment rapide en production. Ceci est tout spécialement vrai dans
le cas des environnements de développement qui disposent généralement
d’une fraction des données du système de production.

Néanmoins, il n’est pas surprenant que les requêtes soient plus lentes
quand le volume de données augmente. Mais la différence entre les deux
index est parfois inattendue. Quelle est la raison de cette différence ?

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 81


chapitre 3 : Performance et scalabilité

Trouver la raison devrait être simple en comparant les plans d’exécution.

------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 972 |
| 1 | SORT AGGREGATE | | 1 | |
|* 2 | INDEX RANGE SCAN| SCALE_SLOW | 3000 | 972 |
------------------------------------------------------

------------------------------------------------------
| Id Operation | Name | Rows | Cost |
------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 13 |
| 1 | SORT AGGREGATE | | 1 | |
|* 2 | INDEX RANGE SCAN| SCALE_FAST | 3000 | 13 |
------------------------------------------------------

Les plans d’exécution sont pratiquement identiques. La seule différence


réside dans l’index sélectionné. Même si les coûts reflètent la différence de
performances, la raison n’est pas visible dans le plan d’exécution.

Il semble que nous faisons face à un « index lent » ; la requête est lente bien
qu’elle utilise un index. Néanmoins, nous ne croyons plus dans le mythe de
l’ « index cassé ». Rappelons-nous plutôt des deux ingrédients qui rendent
une recherche par index lente : (1) l’accès à la table, et (2) le parcours d’un
gros intervalle dans l’index.

Aucun des plans d’exécution n’affiche une opération TABLE ACCESS BY


INDEX ROWID, donc un des plans d’exécution doit parcourir un intervalle plus
grand qu’un autre. Où un plan d’exécution affiche-t-il l’intervalle d’index
parcouru ? Dans les informations de prédicats !

Astuce
Faites attention aux informations de prédicats.

L’information du prédicat n’est pas du tout un détail inutile que vous


pouvez ignorer. Un plan d’exécution sans information de prédicat est
incomplet. Sans lui, vous ne pouvez pas comprendre la différence de
performances dans les plans affichés ci-dessus. Si nous regardons les plans
d’exécution complets, nous pouvons voir la différence.

82 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


L’impact du volume de données sur les performances

------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 972 |
| 1 | SORT AGGREGATE | | 1 | |
|* 2 | INDEX RANGE SCAN| SCALE_SLOW | 3000 | 972 |
------------------------------------------------------

Predicate Information (identified by operation id):


2 - access("SECTION"=TO_NUMBER(:A))
filter("ID2"=TO_NUMBER(:B))
------------------------------------------------------
| Id Operation | Name | Rows | Cost |
------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 13 |
| 1 | SORT AGGREGATE | | 1 | |
|* 2 | INDEX RANGE SCAN| SCALE_FAST | 3000 | 13 |
------------------------------------------------------
Predicate Information (identified by operation id):
2 - access("SECTION"=TO_NUMBER(:A) AND "ID2"=TO_NUMBER(:B))

Note
Le plan d’exécution a été simplifié pour qu’il soit plus clair. L'annexe
en page 170 explique les détails de la section « Predicate
Information » dans un plan d’exécution Oracle.

La différence est évidente maintenant : seule la condition sur SECTION est


un prédicat d’accès lors de l’utilisation de l’index SCALE_SLOW. La base de
données doit lire toutes les lignes de la section, puis doit ignorer ceux qui
ne correspondent pas au prédicat de filtre sur ID2. Le temps de réponse
grossit avec le nombre de lignes dans la section. Avec l’index SCALE_FAST, la
base de données utilise toutes les conditions comme prédicats d’accès. Le
temps de réponse grandit avec le nombre de lignes sélectionnées.

Important
Les prédicats de filtre sont comme des explosifs non déclenchés. Ils
peuvent exploser à tout moment.

Les dernières pièces manquantes du puzzle sont les définitions d’index.


Pouvons-nous deviner les définitions d’index à partir des plans d’exécution ?

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 83


chapitre 3 : Performance et scalabilité

La définition de l’index SCALE_SLOW doit commencer avec la colonne SECTION.


Dans le cas contraire, il ne pourrait pas être utilisé comme prédicat d’accès.
La condition sur ID2 n’est pas un prédicat d’accès, donc elle ne peut
pas suivre SECTION dans la définition de l’index. Cela signifie que l’index
SCALE_SLOW doit avoir au moins trois colonnes où SECTION est la première et
ID2 n’est pas la deuxième. C’est le cas dans la définition de l’index utilisé
pour ce test :

CREATE INDEX scale_slow ON grosses_donnees (section, id1, id2);

La base de données ne peut pas utiliser ID2 comme prédicat d’accès à cause
de la colonne ID1 en deuxième position.

La définition de l’index SCALE_FAST doit avoir les colonnes SECTION et ID2


dans les deux premières positions car elles sont toutes les deux utilisées
pour des prédicats d’accès. Néanmoins, nous ne pouvons rien dire sur leur
ordre. L’index qui a été utilisé pour le test commence avec la colonne
SECTION et a la colonne supplémentaire ID1 en troisième position :

CREATE INDEX scale_fast ON grosses_donnees (section, id2, id1);

La colonne ID1 a été ajoutée uniquement pour que cet index ait la même
taille que SCALE_SLOW. Sinon, vous auriez pu avoir l’impression que la
différence de performance était due à la différence de taille.

84 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Impact de la charge système sur les performances

Impact de la charge système


sur les performances
Les considérations sur la définition d’un index multi-colonnes s’arrêtent
souvent dès que l’index est utilisé pour une requête en cours d’optimisa-
tion. Néanmoins, la raison pour laquelle l’optimiseur n’utilise pas un index
n’est généralement pas parce qu’il ne s’agit pas du bon index pour cette
requête. Habituellement, il ne l’utilise pas parce qu’il est plus efficace de
faire un parcours complet de table. Cela ne signifie pas que l’index est
optimal pour la requête.

L’exemple précédent a montré des difficultés pour reconnaître le bon ordre


des colonnes dans un index à partir du plan d’exécution. Très souvent, les
informations de prédicat sont bien cachées, donc vous devez les rechercher
spécifiquement pour vérifier l’utilisation optimale des index.

Par exemple, SQL Server Management Studio montre seulement les infor-
mations de prédicat dans une infobulle lorsque vous déplacez le curseur de
la souris sur l’opération d’index. Le plan d’exécution suivant utilise l’index
SCALE_SLOW ; du coup, il montre la condition sur ID2 comme un prédicat de
filtre (juste « Predicate », sans le terme « Seek »). Obtenir l’information de
prédicat à partir du plan d’exécution de MySQL ou PostgreSQL est encore
plus bizarre. L’Annexe A à la page 165 dispose des détails.

Figure 3.3. Information de prédicat avec un infobulle

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 85


chapitre 3 : Performance et scalabilité

Peu importe comment est affichée l’information du prédicat, cette infor-


mation a un impact important sur les performances, tout spécialement
quand le système grossit. Rappelez-vous que ce n’est pas seulement le
volume de données qui grossit, mais aussi le nombre d’accès. Ceci est
encore un autre paramètre de la scalabilité.

La Figure 3.4 graphe le temps de réponse comme une fonction du temps


d’accès. Le volume de données reste constant. Cela montre le temps
d’exécution de la même requête et utilise toujours la section avec le plus
grand volume de données. Cela signifie que le dernier point de la Figure 3.2
à la page 81 correspond au premier point dans ce graphe.
Figure 3.4. Scalabilité par rapport à la charge système
lent rapide
30 30
Temps de réponse [sec]

Temps de réponse [sec]


25 25

20 20

15 15

10 10

5 5

0 0
0 5 10 15 20 25
Charge [requêtes en parallèle]

La ligne pointillée trace le temps de réponse lors de l’utilisation de l’index


SCALE_SLOW. Elle atteint jusqu’à 32 secondes si 25 requêtes s’exécutent
en même temps. En comparant avec le temps de réponse sans charge
en arrière-plan (cela pourrait être le cas dans votre environnement de
développement), cela prend 30 fois plus de temps. Même si vous avez
une copie complète de la base de données en production dans votre
environnement de développement, la charge de fond en production peut
encore causer plus de lenteur à l’exécution de la requête.

La ligne solide montre le temps de réponse en utilisant l’index SCALE_FAST.


Il n’y a pas de prédicat de filtre. Le temps de réponse reste bien en-dessous
des deux secondes même si 25 requêtes sont exécutées en même temps.

Note
Une étude attentive du plan d’exécution amène plus de confiance que
des tests superficiels de performances.
Un test complet est toujours utile mais les coûts affichés par les plans
sont importants.

86 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Temps de réponse et bande passante

Des temps de réponse suspects sont souvent pris à la légère lors du


développement. Ceci est principalement dû au fait que nous nous atten-
dons à ce que nous ayons du « matériel plus performant en production »,
ce qui impliquerait de meilleures performances. Généralement, on
observe l’inverse car l’infrastructure en production est plus complexe et
accumule les latences qui interviennent peu dans l’environnement de
développement. Même en testant sur une infrastructure équivalente à celle
en production, la charge en tâche de fond peut toujours causer des temps
de réponse différents. Dans la prochaine section, nous verrons qu’il n’est
pas raisonnable, en général, de s’attendre à de meilleurs temps de réponse
avec un plus gros matériel.

Temps de réponse et bande passante


Un plus gros matériel n’est pas toujours plus rapide mais il peut gérer
une charge plus importante. Un plus gros matériel se compare plus à une
autoroute avec plus de voies qu’à une voiture plus rapide : vous ne pouvez
pas conduire plus rapidement (en tout cas, vous n’y êtes pas autorisé)
parce qu’il y a plus de voies. C’est la raison principale pour laquelle plus de
matériel n’améliore pas automatiquement les requêtes SQL lentes.

Nous ne sommes plus en 1990. La puissance de calcul d’un seul cœur


s’améliorait rapidement à cette époque. La plupart des problèmes de temps
de réponse disparaissaient avec du nouveau matériel, tout simplement
grâce à de meilleurs processeurs. C’était comme de nouveaux modèles de
voitures qui allaient jusqu’à deux fois plus vite que les anciens... chaque
année ! Néanmoins, la puissance d’un cœur de processeur a atteint sa
limite lors des premières années du 21ème siècle. Il n’y a pratiquement
plus eu d’améliorations sur cet axe. Pour avoir des processeurs toujours
plus puissants, les vendeurs sont passés à une stratégie multi-cœurs. Même
si cela permet à de nombreuses tâches de s’exécuter en même temps,
cela n’améliore pas pour autant les performances s’il n’y a qu’une tâche à
exécuter. Les performances ont plus d’une dimension.

La scalabilité horizontale (ajouter plus de serveurs) a des limitations simi-


laires. Bien qu’avoir plus de serveurs permette de traiter plus de requêtes,
cela n’améliore pas le temps de réponse pour une requête particulière. Pour
rendre la recherche plus rapide, vous avez besoin d’un arbre de recherche
efficace, même dans les systèmes non relationnels comme CouchDB et
MongoDB.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 87


chapitre 3 : Performance et scalabilité

Important
Une bonne indexation est le meilleur moyen de réduire les temps de
réponse des requêtes, dans les bases de données relationnelles mais
aussi dans les systèmes non relationnels.

Une bonne indexation a pour but d’exploiter complètement la scalabilité


logarithmique d’un index B-tree. Malheureusement, l’indexation est
généralement mal faite. Le graphe de « L’impact du volume de données
sur les performances » rend très apparent la mauvaise indexation.

Figure 3.5. Temps de réponse en fonction du volume de données


lent rapide
1.2 1.2
Temps de réponse [sec]

Temps de réponse [sec]


1.0 1.0

0.8 0.8

0.6 0.6

0.4 0.4

0.2 0.2

0.0 0.0
0 20 40 60 80 100
Volume de données [section]

La différence du temps de réponse entre un bon et un mauvais index est


impressionnante. Il est difficile de compenser cet effet en ajoutant plus de
matériel. Même si vous réussissez à descendre le temps de réponse avec
du matériel, on peut toujours se demander s’il s’agit de la bonne réponse
à ce problème.

Un grand nombre de systèmes appelés NoSQL clament toujours pouvoir


résoudre les problèmes de performances avec de la scalabilité horizontale.
Néanmoins, cette scalabilité est limitée principalement pour les opérations
en écriture et est accomplie avec le modèle de cohérence éventuelle.
Les bases de données SQL utilisent un modèle de cohérence stricte qui
ralentit les opérations en écriture mais cela n’implique pas forcément
une mauvaise bande passante. Apprenez-en plus dans la partie intitulée
« Cohérence éventuelle et le théorème CAP » à la page 90.

Plus de matériel ne va typiquement pas améliorer les temps de réponse.


En fait, cela pourrait même rendre le système plus lent à cause de la
complexité supplémentaire, les latences s’accumulent. Les latences réseau
ne sont pas un problème si l’application et la base de données travaillent
sur le même ordinateur mais cette configuration est peu fréquente dans les

88 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Temps de réponse et bande passante

environnements de production où la base de données et l’application sont


généralement installées sur du matériel dédié. Les politiques de sécurité
pourraient même nécessiter un pare-feu entre le serveur applicatif et la
base de données, doublant souvent la latence réseau. Plus l’infrastructure
devient complexe, plus les latences s’accumulent et plus les réponses
deviennent lentes. Cet effet amène souvent l’observation étonnante que le
matériel en production, acheté fort cher, est plus lent qu’un PC de bureau
bon marché utilisé pour le développement.

Une autre latence très importante est le temps d’accès du disque. Les
disques durs magnétiques ont besoin d’un long moment pour déplacer la
tête de lecture pour que les données réclamées puissent être lues. Cela
prend généralement quelques millisecondes. Cette latence survient quatre
fois lors du parcours d’un index B-tree de quatre niveaux, soit au total
une douzaine de millisecondes. Bien que cela soit très long à l’échelle d’un
ordinateur, c’est bien en-dessous de notre perception… quand ce n’est fait
qu’une seule fois. Cependant, il est très facile de déclencher des centaines,
voire des milliers, de déplacements de la tête de lecture du disque avec
une simple requête SQL, en particulier si on combine plusieurs tables
avec une jointure. Bien que la mise en cache réduise le problème très
fortement et que de nouvelles technologies comme le SSD diminue les
temps d’accès d’un ordre de grandeur, les jointures sont toujours généra-
lement souspçonnées d’être lentes. Du coup, le chapitre suivant expliquera
comment utiliser des index pour obtenir des jointures de table efficaces.

Solid State Disks (SSD) et la mise en cache


Les disques SSD sont une technologie de stockage de masse qui
n’utilise pas de partie mécanique, et du coup n’occasionne pas de
déplacement de la tête de lecture. Le temps d’accès typique des
SSD est d’un ordre de grandeur plus rapide que celui d’un disque
magnétique. Les SSD deviennent disponibles pour du stockage en
entreprise à partir de 2010 mais, à cause d’un coût important et d’une
durée de vie limitée, ils ne sont pas utilisés fréquemment pour les
bases de données.
Néanmoins, les bases de données mettent en cache les données
fréquemment utilisées dans la mémoire principale. Ceci est très
utile pour les données nécessaires lors de chaque accès d’index, par
exemple le nœud racine d’un index. La base de données peut mettre
en cache complètement les index fréquemment utilisés pour qu’une
recherche via l’index ne déclenche pas un seul déplacement de la tête
de lecture.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 89


chapitre 3 : Performance et scalabilité

Cohérence éventuelle et le théorème CAP


Maintenir une cohérence stricte dans un système distribué requiert
une coordination synchrone parmi les nœuds de toutes les opérations
d’écriture. Ce principe a deux effets de bord indésirables : (1) il
ajoute de la latence et augmente les temps de réponse ; (2) il
réduit la disponibilité complète parce que les membres doivent être
disponibles à tout moment pour terminer une opération d’écriture.
Une base de données SQL distribuée est souvent confondue avec
des groupes d’ordinateurs qui utilisent un système de stockage
partagé ou une réplication maître/esclave. En fait, une base de
données distribuée est plutôt comme un magasin en ligne qui est
intégré dans un système ERP, souvent deux produits différents de
vendeurs différents. La cohérence entre les systèmes est toujours
un but souhaité qui est souvent obtenu en utilisant le protocole
de la validation en deux phases (2PC). Ce protocole permet à des
transactions globales de délivrer le comportement tout ou rien bien
connu parmi plusieurs bases de données. Réaliser une transaction
globale est seulement possible si tous les membres sont disponibles.
Du coup, cela réduit la disponibilité.
Plus un système distribué a de nœuds, plus la cohérence stricte est
problématique. Maintenir une cohérence stricte est pratiquement
impossible si le système a plus que quelques nœuds. D’un autre côté,
supprimer la cohérence stricte résout le problème de disponibilité
et élimine le temps de réponse accru. L’idée de base est de rétablir
la cohérence globale après avoir terminé l’opération d’écriture
sur un sous-ensemble des nœuds. Cette approche laisse un seul
problème non résolu : il est impossible d’empêcher les conflits si deux
nœuds acceptent des changements contradictoires. La cohérence
est éventuellement atteinte par la gestion des conflits, pas en les
empêchant. Dans ce contexte, la cohérence signifie que tous les
nœuds ont les mêmes données - ce n’est pas forcément les bonnes
ou les meilleures données.
Le théorème CAP de Brewer décrit les dépendances générales entre
cohérence (Consistency), disponibilité (Availability) et la résistance
au morcellement (Partition tolerance).

90 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Chapitre 4

Opération de jointure
Une requête SQL arrive dans un bar et voit deux tables.
Elle s’approche des tables et leur demande « Puis-je me joindre à vous ? »
—Source : inconnu

L’opération de jointure transforme un modèle normalisé en une forme


dénormalisée qui convient à un traitement spécifique. Joindre plusieurs
relations est une opération très sensible aux latences d’accès des disques
car cette opération combine des fragments de données réparties sur le
disque. Une bonne indexation est de nouveau la meilleure solution pour
réduire les temps de réponse. Le bon index dépend principalement de
l’algorithme choisi, parmi trois, pour effectuer la jointure.

Cependant, il existe un comportement commun à tous les algorithmes


de jointure : ils traitent seulement deux tables à la fois. Une requête
SQL comprenant plus de tables nécessite plusieurs étapes : tout d’abord
construire un résultat intermédiaire provenant de la jointure de deux
tables, puis joindre ce résultat avec la table suivante, et ainsi de suite.

Même si l’ordre de jointure n’a aucun impact sur le résultat final, il


affecte les performances. Du coup, l’optimiseur va évaluer toutes les
permutations possibles dans l’ordre de jointure et sélectionner le meilleur.
L’optimisation d’une requête complexe peut de ce fait devenir un souci
pour les performances. Plus il y a de tables à joindre et plus il existe de
variantes de plans d’exécution. En langage mathématique : n! (croissance
factorielle). Néanmoins, cela n’est pas un problème lors de l’utilisation de
paramètres liés.

Important
Plus une requête est complexe, plus il est important d’utiliser
les paramètres liés.
Ne pas utiliser les paramètres liés revient à recompiler un
programme à chaque utilisation.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 91


chapitre 4 : Opération de jointure

Envoi des résultats intermédiaires


par pipeline
Malgré l’existence de résultats intermédiaires, cela ne signifie pas
que la base de données doit les matérialiser. Autrement dit, elle n’a
pas besoin d’avoir le résultat complet de la première jointure avant
de commencer la jointure suivante. Les bases de données utilisent
des pipelines pour réduire l’utilisation de la mémoire. Chaque ligne
provenant du résultat intermédiaire est immédiatement envoyée via
le pipeline à la prochaine opération de jointure, évitant ainsi d’avoir
à écrire le résultat intermédiaire.

Boucles imbriquées
La jointure par boucle imbriquée est l’algorithme de jointure le plus
évident. Il fonctionne en utilisant des requêtes imbriquées : la requête
externe pour récupérer les résultats d’une table et la deuxième requête pour
chaque ligne provenant de la première requête, pour récupérer les données
correspondantes dans l’autre table.

En fait, vous pouvez utiliser des extractions imbriquées pour implémenter


vous-même l’algorithme des boucles imbriquées. Néanmoins, ce n’est pas
recommandé car les latences réseau s’ajoutent aux latences des disques, ce
qui aboutit à un temps de réponse encore pire. Les extractions imbriquées
sont toujours très fréquentes car elles sont faciles à faire, même sans s’en
rendre compte. Les ORM « aident » très souvent à obtenir ce type de
comportement, au point que le problème d’extraction N+1 y a gagné sa très
mauvaise notoriété.

Les exemples suivants montrent des exemples de requêtes imbriquées


accidentelles produits par différents ORM. Les exemples recherchent les
employés dont le nom commence par 'WIN' et récupèrent toutes les ventes
(dans la table VENTES) pour ces employés.

Par défaut, les ORM ne génèrent pas des jointures SQL. À la place, elles
exécutent une requête sur la table VENTES avec des extractions imbriquées.
L’effet est connu sous le nom de « problème d’extractions N+1 » ou, plus
court, « problème N+1 » car cela cause l’exécution de N+1 extractions au
total si la première requête renvoie N lignes.

92 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Boucles imbriquées

Java
L’exemple JPA utilise l’interface CriteriaBuilder.

CriteriaBuilder constructeurRequete = em.getCriteriaBuilder();


CriteriaQuery<Employes>
requete = constructeurRequete.createQuery(Employes.class);
Root<Employes> r = requete.from(Employes.class);
requete.where(
constructeurRequete.like(
constructeurRequete.upper(r.get(Employes_.nom)),
"WIN%"
)
);

List<Employes> emp = em.createQuery(requete).getResultList();


for (Employes e: emp) {
// traitement d'un employe
for (Ventes s: e.getVentes()) {
// traitement des ventes pour cet employe
}
}

Hibernate JPA 3.6.0 génère des requêtes d’extraction (select) N+1 :

select employes0_.id_supplementaire as supplementaire1_0_


-- AUTRES COLONNES
from employes employes0_
where upper(employes0_.nom) like ?
select ventes0_.id_supplementaire as supplementaire4_0_1_
-- AUTRES COLONNES
from ventes ventes0_
where ventes0_.id_supplementaire=?
and ventes0_.id_employe=?

select ventes0_.id_supplementaire as supplementaire4_0_1_


-- AUTRES COLONNES
from ventes ventes0_
where ventes0_.id_supplementaire=?
and ventes0_.id_employe=?

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 93


chapitre 4 : Opération de jointure

Perl
L’exemple Perl suivant utilise l’outil DBIx::Class :

my @employes =
$schema->resultset('Employes')
->search({'UPPER(nom)' => {-like=>'WIN%'}});

foreach my $employe (@employes) {


 traitement d'un employe
foreach my $vente ($employe->ventes) {
 traitement des ventes pour cet employe
}
}

DBIx::Class 0.08192 génère des requêtes d’extraction (select) N+1 :

SELECT me.id_employe, me.id_supplementaire


, me.nom, me.prenom, me.date_de_naissance
FROM employes me
WHERE ( UPPER(nom) LIKE ? )

SELECT me.id_vente, me.id_employe, me.id_supplementaire


, me.date_vente, me.valeur_eur
FROM ventes me
WHERE ( ( me.id_employe = ?
AND me.id_supplementaire = ? ) )

SELECT me.id_vente, me.id_employe, me.id_supplementaire


, me.date_vente, me.valeur_eur
FROM ventes me
WHERE ( ( me.id_employe = ?
AND me.id_supplementaire = ? ) )

PHP
L’exemple Doctrine utilise l’interface du constructeur de requêtes :

$qb = $em->createQueryBuilder();
$qb->select('e')
->from('Employes', 'e')
->where("upper(e.nom) like :nom")
->setParameter('nom', 'WIN%');
$r = $qb->getQuery()->getResult();
foreach ($r as $ligne) {
// traitement d'un employe
foreach ($ligne->getVentes() as $vente) {
// traitement des ventes pour cet employe
}
}

94 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Boucles imbriquées

Doctrine 2.0.5 génère des requêtes d’extraction (select) N+1 :

SELECT e0_.id_employe AS id_employe0 -- AUTRES COLONNES


FROM employes e0_
WHERE UPPER(e0_.nom) LIKE ?

SELECT t0.id_vente AS ID_VENTE1 -- AUTRES COLONNES


FROM ventes t0
WHERE t0.id_supplementaire = ?
AND t0.id_employe = ?

SELECT t0.id_vente AS ID_VENTE1 -- AUTRES COLONNES


FROM ventes t0
WHERE t0.id_supplementaire = ?
AND t0.id_employe = ?

Activer la trace des requêtes SQL


Activer la trace des requêtes SQL pendant le développement et vérifier
les requêtes SQL générées.

DBIx::Class
export DBIC_TRACE=1 dans votre shell.

Doctrine
Uniquement possible au niveau du code source... n’oubliez pas
de le désactiver en production. Pensez à développer votre propre
traceur configurable.
$logger = new \Doctrine\DBAL\Logging\EchoSqlLogger;
$config->setSQLLogger($logger);

Hibernate (native)
<property name="show_sql">true</property> dans App.config ou
hibernate.cfg.xml

JPA
Dans persistence.xml mais dépendant du connecteur JPA :
<property name="eclipselink.logging.level" value="FINE"/>
<property name="hibernate.show_sql" value="TRUE"/>
<property name="openjpa.Log" value="SQL=TRACE"/>

La plupart des ORM propose un moyen automatique pour activer la


trace des requêtes SQL. Malheureusement, cela inclut le risque de
déployer accidentellement ce paramètre en production.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 95


chapitre 4 : Opération de jointure

Même si l’approche des extractions imbriquées est mauvaise, elle explique


bien la jointure par boucle imbriquée. La base de données exécute la jointure
exactement comme les outils ORM ci-dessus. L’utilisation d’un index pour
une jointure par boucle imbriquée est exactement la même que pour
les requêtes select montrées ci-dessus. Il est nécessaire d’avoir un index
fonctionnel sur la table EMPLOYES et un index concaténé pour les prédicats
de la jointure sur la table VENTES :

CREATE INDEX emp_nom_maj ON employes (UPPER(nom));


CREATE INDEX ventes_emp ON ventes (id_supplementaire, id_employe);

Néanmoins, une jointure SQL est toujours plus efficace que les requêtes
d’extraction imbriquées, même si des index identiques sont utilisés, car
cette méthode évite beaucoup de communication réseau. Il est même plus
rapide lorsque la quantité totale de données transférées est plus importante
à cause de la duplication des attributs des employés pour chaque vente.
Ceci est dû aux deux dimensions des performances : le temps de réponse
et la bande passante. La bande passante a un impact mineur sur le temps
de réponse mais les latences ont un impact énorme. Cela signifie que le
nombre d’allers/retours vers la base de données est plus important pour le
temps de réponse que la quantité de données réellement transférées.

Astuce
Exécutez les jointures dans la base de données.

La plupart des outils ORM proposent une façon de créer des jointures SQL.
Le mode de chargement à l’avance (eager fetching) est probablement le plus
intéressant. Il se configure généralement au niveau des propriétés dans
les correspondances de l’entité (par exemple pour la propriété employes
dans la classe Ventes). L’outil ORM fera ensuite toujours la jointure avec la
table EMPLOYES lors de l’accès à la table VENTES. Configurer le chargement
à l’avance n’a de sens que si vous avez toujours besoin des détails de
l’employé quand vous regardez les données de ventes.

Par contre, le chargement à l’avance est contre-performant si vous n’avez


pas besoin des enregistrements enfants à chaque fois que vous accédez à
l’objet parent. Pour une application annuaire, charger les enregistrements
de la table VENTES lors de l’affichage des informations sur l’employé n’a
pas de sens. Vous pourriez avoir besoin des données relatives aux ventes
dans d’autres cas, mais pas toujours. Une configuration statique n’est pas
la solution.

96 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Boucles imbriquées

Pour des performances optimales, vous devez regagner le contrôle sur


les jointures. Les exemples suivants vous montrent comment obtenir
une grande flexibilité sur le contrôle du comportement des jointures à
l’exécution.

Java
L’interface JPA CriteriaBuilder fournit la méthode Root<>.fetch()
pour contrôler les jointures. Cela vous permet de spécifier quand et
comment joindre les objets indiqués dans la requête principale. Dans
cet exemple, nous utilisons une jointure gauche pour récupérer tous
les employés même si certains n’ont pas fait de ventes.

Avertissement
JPA et Hibernate renvoient les employés pour chaque vente.
Cela signifie qu’un employé ayant 30 ventes apparaîtra 30 fois.
Bien que cela soit très perturbant, c’est le comportement attendu
(persistence EJB 3.0, paragraphe 4.4.5.3 “Fetch Joins”). Vous
pouvez dé-dupliquer la relation parente ou utiliser la fonction
distinct() comme indiqué dans l’exemple ci-dessous.

CriteriaBuilder qb = em.getCriteriaBuilder();
CriteriaQuery<Employes> requete
= qb.createQuery(Employes.class);
Root<Employes> r = requete.from(Employes.class);
requete.where(qb.like(
qb.upper(r.get(Employes_.nom)),
"WIN%")
);

r.fetch("ventes", JoinType.LEFT);
// necessaire pour eviter la duplication des
// enregistrements Employe
requete.distinct(true);

List<Employes> emp = em.createQuery(requete).getResultList();

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 97


chapitre 4 : Opération de jointure

Hibernate 3.6.0 génère la requête SQL suivante :

select distinct
employes0_.id_supplementaire as supplementaire1_0_0_
, employes0_.id_employe as employe2_0_0_
-- AUTRES COLONNES
, ventes1_.id_vente as vente1_0__
from employes employes0_
left outer join ventes ventes1_
on employes0_.id_supplementaire=sales1_.id_supplementaire
and employes0_.id_employe=sales1_.id_employe
where upper(employes0_.nom) like ?

La requête a bien la jointure gauche attendue mais contient aussi


un mot-clé distinct inutile. Malheureusement, JPA ne fournit pas
d’appels API séparés pour filtrer les enregistrements parents dupliqués
sans en même temps dé-dupliquer les enregistrements enfants. Le
mot-clé distinct dans une requête SQL est alarmant car la plupart
des bases de données vont filtrer d’eux-mêmes les enregistrements
dupliqués. Seulement quelques bases de données se rappellent que les
clés primaires garantissent l’unicité dans ce cas.

L’API native Hibernate résout le problème du côté client en utilisant un


transformateur d’ensembles de résultats :

Criteria c = session.createCriteria(Employes.class);
c.add(Restrictions.ilike("nom", 'Win%'));
c.setFetchMode("ventes", FetchMode.JOIN);
c.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);

List<Employes> result = c.list();

Cela génère la requête suivante :

select this_.id_supplementaire as supplementaire1_0_1_


, this_.id_employe as employe2_0_1_
-- AUTRES COLONNES this_ sur employes
, ventes2_.id_vente as vente1_3_
-- AUTRES COLONNES ventes2_ sur ventes
from employes this_
left outer join ventes ventes2_
on this_.id_supplementaire=sales2_.id_supplementaire
and this_.id_employe=ventes2_.id_employe
where lower(this_.nom) like ?

98 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Boucles imbriquées

Cette méthode produit un SQL propre sans clause inattendue. Notez


que Hibernate utilise lower pour les requêtes de recherche insensible à
la casse, un détail important pour la construction d’index fonctionnels.

Perl
L’exemple suivant utilise l’outil DBIx::Class de Perl :

my @employes =
$schema->resultset('Employes')
->search({ 'UPPER(nom)' => {-like => 'WIN%'}
, {prefetch => ['ventes']}
});

DBIx::Class 0.08192 génère la requête SQL suivante :

SELECT me.id_employe, me.id_supplementaire, me.nom


-- AUTRES COLONNES
FROM employes me
LEFT JOIN ventes ventes
ON (sales.id_employe = me.id_employe
AND sales.id_supplementaire = me.id_supplementaire)
WHERE ( UPPER(nom) LIKE ? )
ORDER BY ventes.id_employe, ventes.id_supplementaire

Notez la clause order by. Ce n’était pas demandé par l’application. La


base de données doit trier l’ensemble de résultats et cela peut prendre
du temps.

PHP
L’exemple suivant utilise l’outil Doctrine de PHP :

$qb = $em->createQueryBuilder();
$qb->select('e,s')
->from('Employes', 'e')
->leftJoin('e.ventes', 's')
->where("upper(e.nom) like :nom")
->setParameter('nom', 'WIN%');
$r = $qb->getQuery()->getResult();

Doctrine 2.0.5 génère la requête SQL suivante :

SELECT e0_.id_employe AS id_employe0


-- AUTRES COLONNES
FROM employes e0_
LEFT JOIN ventes s1_
ON e0_.id_supplementaire = s1_.id_supplementaire
AND e0_.id_employe = s1_.id_employe
WHERE UPPER(e0_.nom) LIKE ?

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 99


chapitre 4 : Opération de jointure

Le plan d’exécution affiche l’opération NESTED LOOPS OUTER :

---------------------------------------------------------------
|Id |Operation | Name | Rows | Cost |
---------------------------------------------------------------
| 0 |SELECT STATEMENT | | 822 | 38 |
| 1 | NESTED LOOPS OUTER | | 822 | 38 |
| 2 | TABLE ACCESS BY INDEX ROWID| EMPLOYES | 1 | 4 |
|*3 | INDEX RANGE SCAN | EMP_NOM_MAJ | 1 | |
| 4 | TABLE ACCESS BY INDEX ROWID| VENTES | 821 | 34 |
|*5 | INDEX RANGE SCAN | VENTES_EMP | 31 | |
---------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
3 - access(UPPER("NOM") LIKE 'WIN%')
filter(UPPER("NOM") LIKE 'WIN%')
5 - access("E0_"."ID_SUPPLEMENTAIRE"="S1_"."ID_SUPPLEMENTAIRE"(+)
AND "E0_"."ID_EMPLOYE" ="S1_"."ID_EMPLOYE"(+))

La base de données récupère le résultat de la table EMPLOYES tout d’abord via


l’index EMP_NOM_MAJ puis récupère les enregistrements correspondant dans
la table VENTES pour chaque employé.

Astuce
Connaissez votre ORM et gagnez le contrôle sur les jointures.

La jointure par boucle imbriquée offre de bonnes performances si la pre-


mière requête renvoie un petit ensemble de résultats. Sinon, l’optimiseur
pourrait choisir un autre algorithme de jointure, comme la jointure par
hachage décrite dans la prochaine section mais ceci n’est possible que si
l’application utilise une jointure pour indiquer à la base les données dont
elle a réellement besoin.

100 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Jointure par hachage

Jointure par hachage


L’algorithme de jointure par hachage vise le point faible de la jointure par
boucle imbriquée : les nombreux parcours d’index B-tree lors de l’exécution
de la requête interne. À la place, il charge les enregistrements candidats
d’un côté de la jointure dans une table de la jointure qui peut être
sondée très rapidement pour chaque ligne provenant de l’autre côté de la
jointure. Configurer une jointure de hachage requiert une autre approche
de l’indexation qu’une jointure par boucle imbriquée. Il est aussi possible
d’améliorer les performances d’une jointure de hachage en sélectionnant
peu de colonnes, ce qui se révèle être un challenge pour la plupart des ORM.

La stratégie d’indexation pour une jointure de hachage est très différente


car il n’est pas nécessaire d’indexer les jointures de colonnes. Les seuls
index améliorant les performances de la jointure de hachage sont ceux qui
portent sur les prédicats where indépendants.

Astuce
Indexez les prédicats where indépendants pour améliorer les
performances des jointures de hachage.

Considérez l’exemple suivant. Il extrait toutes les ventes pour les six
derniers mois avec les détails correspondants de l’employé :

SELECT *
FROM ventes s
JOIN employes e ON (s.id_supplementaire = e.id_supplementaire
AND s.id_employe = e.id_employe)
WHERE s.date_vente > trunc(sysdate) - INTERVAL '6' MONTH

Le filtre DATE_VENTE est la seule clause where indépendante. Cela signifie que
cela fait référence à une seule table et n’appartient pas aux prédicats de
jointure.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 101


chapitre 4 : Opération de jointure

--------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
--------------------------------------------------------------
| 0 | SELECT STATEMENT | | 49244 | 59M| 12049|
|* 1 | HASH JOIN | | 49244 | 59M| 12049|
| 2 | TABLE ACCESS FULL| EMPLOYES | 10000 | 9M| 478|
|* 3 | TABLE ACCESS FULL| VENTES | 49244 | 10M| 10521|
--------------------------------------------------------------

Predicate Information (identified by operation id):


---------------------------------------------------
1 - access("S"."ID_SUPPLEMENTAIRE"="E"."ID_SUPPLEMENTAIRE"
AND "S"."ID_EMPLOYE" ="E"."ID_EMPLOYE")
3 - filter("S"."DATE_VENTE">TRUNC(SYSDATE@!)
-INTERVAL'+00-06' YEAR(2) TO MONTH)

La première étape de l’exécution est un parcours complet de la table pour


charger tous les employés dans une table de hachage (identifiant 2 du
plan). La table de hachage utilise les prédicats de jointure comme clé. À
l’étape suivante, la base de données fait un autre parcours complet de la
table VENTES et ignore toutes les ventes qui ne satisfont pas la condition
sur DATE_VENTE (identifiant 3 du plan). Pour les enregistrements VENTES
correspondants, la base de données accède à la table de hachage pour
charger les détails correspondants des employés.

Le seul but de la table de hachage est d’agir comme une structure tem-
poraire en mémoire pour éviter d’avoir à accéder de nombreuses fois à la
table EMPLOYE. La table de hachage est initialement chargée en une seule
fois, pour qu’il ne soit pas nécessaire d’utiliser un index pour récupérer
efficacement des enregistrements seuls. L’information du prédicat con-
firme qu’aucun filtre n’est appliqué sur la table EMPLOYES (identifiant 2 du
plan). La requête n’a aucun prédicat indépendant sur cette table.

Important
Indexer les prédicats de jointure n’améliore pas les performances
d’une jointure de hachage.

Cela ne signifie pas qu’il est impossible d’indexer une jointure de hachage.
Les prédicats indépendants peuvent être indexés. Ces conditions sont
appliquées lors d’une des deux opérations d’accès de table. Dans l’exemple
ci-dessus, il s’agit du filtre sur DATE_VENTE.

CREATE INDEX date_ventes ON ventes (date_vente);

102 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Jointure par hachage

Le plan d’exécution suivant utilise cet index. Néanmoins, il utilise un


parcours complet de table pour la table EMPLOYES car la requête n’a pas de
prédicat where indépendant sur EMPLOYES.

---------------------------------------------------------------
| Id | Operation | Name | Bytes| Cost|
---------------------------------------------------------------
| 0 | SELECT STATEMENT | | 59M| 3252|
|* 1 | HASH JOIN | | 59M| 3252|
| 2 | TABLE ACCESS FULL | EMPLOYES | 9M| 478|
| 3 | TABLE ACCESS BY INDEX ROWID| VENTES | 10M| 1724|
|* 4 | INDEX RANGE SCAN | DATE_VENTES| | |
--------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("S"."ID_SUPPLEMENTAIRE"="E"."ID_SUPPLEMENTAIRE"
AND "S"."ID_EMPLOYE" ="E"."ID_EMPLOYE" )
4 - access("S"."DATE_VENTE" > TRUNC(SYSDATE@!)
-INTERVAL'+00-06' YEAR(2) TO MONTH)

Indexer une jointure de hachage est symétrique contrairement à la jointure


par boucle imbriquée. Cela signifie que l’ordre de jointure n’influence pas
l’indexation. L’index DATE_VENTES peut être utilisé pour charger la table de
hachage si l’ordre de jointure est inversé.

Note
Indexer une jointure de hachage est indépendant de l’ordre de
jointure.

Une approche assez différente pour optimiser les performances des join-
tures de hachage est de minimiser la taille de la table de hachage. Cette
méthode fonctionne car une jointure de hachage optimale est seulement
possible si la table de hachage entière tient en mémoire. Du coup, l’opti-
miseur utilisera automatiquement le plus petit côté de la jointure pour la
table de hachage. Le plan d’exécution Oracle affiche la mémoire estimée
nécessaire dans la colonne « Bytes ». Dans le plan d’exécution ci-dessus, la
table EMPLOYES nécessite neuf méga-octets et est du coup la plus petite.

Il est aussi possible de réduire la taille de la table de hachage en changeant


la requête SQL, par exemple en ajoutant des conditions supplémentaires
pour que la base de données charge moins d’enregistrements dans la
table de hachage. En continuant l’exemple ci-dessus, cela signifie qu’il faut
ajouter un filtre sur l’attribut DEPARTEMENT pour que seuls soient considérés

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 103


chapitre 4 : Opération de jointure

les employés du service commercial. Ceci améliore les performances de la


jointure par hachage même s’il n’y a pas d’index sur l’attribut DEPARTEMENT
parce que la base de données n’a pas besoin de stocker les employés qui
ne peuvent pas avoir de ventes dans la table de hachage. En faisant cela,
vous devez vous assurer qu’il n’existe pas d’enregistrements dans VENTES
pour les employés qui ne font pas partie du service commercial. Utilisez les
contraintes pour vous en assurer.

Lors de la diminution de la table de hachage, le facteur déterminant n’est


pas le nombre de lignes mais l’empreinte mémoire. En fait, il est même
possible de réduire la taille de la table de hachage en sélectionnant moins
de colonnes, pour n’avoir que les attributs dont vous avez besoin :

SELECT s.date_vente, s.valeur_eur


, e.nom, e.prenom
FROM ventes s
JOIN employes e ON (s.id_supplementaire = e.id_supplementaire
AND s.id_employe = e.id_employe)
WHERE s.date_vente > trunc(sysdate) - INTERVAL '6' MONTH

Cette méthode présente rarement des bugs parce que supprimer la mau-
vaise colonne résultera rapidement en un message d’erreur. Néanmoins, il
est possible de diminuer considérablement la taille de la table de hachage.
Dans ce cas particulier, elle passe de 9 Mo à 234 Ko, une réduction de 97%.

----------------------------------------------------------------
| Id | Operation | Name | Bytes| Cost|
----------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2067K| 2202|
|* 1 | HASH JOIN | | 2067K| 2202|
| 2 | TABLE ACCESS FULL | EMPLOYES | 234K| 478|
| 3 | TABLE ACCESS BY INDEX ROWID| VENTES | 913K| 1724|
|* 4 | INDEX RANGE SCAN | DATE_VENTES | | 133|
----------------------------------------------------------------

Astuce
Sélectionner moins de colonnes pour améliorer les performances de
la jointure de hachage.

Bien qu’au premier regard, il semble simple de supprimer quelques


colonnes à partir d’une requête SQL, cela se révèle un vrai challenge lors
de l’utilisation d’un outil ORM. Le support des objets partiels est très peu
fréquent. Les exemples suivants montrent certaines possibilités.

104 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Jointure par hachage

Java
JPA définit FetchType.LAZY dans l’annotation @Basic. Il peut être
appliqué sur le niveau de propriété :

@Column(name="junk")
@Basic(fetch=FetchType.LAZY)
private String junk;

Les connecteurs JPA sont libres de l’ignorer :

La stratégie LAZY est un conseil donné à l’exécution au


fournisseur, lui indiquant que les données peuvent être
récupérées au coup par coup lors du premier accès.
L’implémentation permet de récupérer les données une par
une si la stratégie LAZY a été spécifiée.
—EJB 3.0 JPA, paragraphe 9.1.18

Hibernate 3.6 implémente la récupération au coup par coup


via l’instrumentation bytecode au moment de la compilation.
L’instrumentation ajoute du code supplémentaire aux classes
compilées qui ne récupère pas les propriétés LAZY jusqu’à leur accès.
L’approche est complètement transparente à l’application mais cela
laisse la porte ouverte à une nouvelle dimension du problème N
+1 : une requête d’extraction (select) pour chaque enregistrement et
pour chaque propriété. C’est tout particulièrement dangereux comme
JPA n’offre pas le contrôle à l’exécution pour récupérer à l’avance si
nécessaire.

Le langage de requête native d’Hibernate, appelé HQL, résout le


problème avec la clause FETCH ALL PROPERTIES :

select v from Ventes v FETCH ALL PROPERTIES


inner join fetch v.employe e FETCH ALL PROPERTIES
where v.dateVente >:dt

La clause FETCH ALL PROPERTIES force Hibernate à récupérer l’entité


en avance, même lors de l’utilisation d’un code instrumenté et de
l’annotation LAZY.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 105


chapitre 4 : Opération de jointure

Une autre option est de charger seulement les options sélectionnées


pour qu’il utilise les objets de transport des données (DTO) au lieu des
entités. Cette méthode fonctionne de la même façon dans HQL et JP-
QL, c’est-à-dire que vous initialisez un objet dans la requête :

select new ParentVentesDTO(v.dateVente , v.valeurEur


,e.prenom, e.nom)
from Ventes v
join v.employe e
where v.dateVente > :dt

La requête sélectionne seulement les données demandées et renvoie


un objet ParentVentesDTO, un simple objet Java (POJO), pas une entité.

Perl
L’outil DBIx::Class n’agit pas comme un gestionnaire d’entités, donc cet
1
héritage ne cause pas des problèmes d’alias . Le manuel supporte cette
approche. La définition suivante du schéma définit la classe Ventes sur
deux niveaux :

package UseTheIndexLuke::Schema::Result::ParentVentes;
use base qw/DBIx::Class::Core/;
__PACKAGE__->table('ventes');
__PACKAGE__->add_columns(qw/id_vente id_employe
id_supplementaire
date_vente valeur_eur/);
__PACKAGE__->set_primary_key(qw/id_vente/);
__PACKAGE__->belongs_to('employe', 'Employes',
{'foreign.id_employe'
=> 'self.id_employe'
,'foreign.id_supplementaire'
=> 'self.id_supplementaire'});
package UseTheIndexLuke::Schema::Result::Ventes;
use base qw/UseTheIndexLuke::Schema::Result::ParentVentes/;

__PACKAGE__->table('ventes');
__PACKAGE__->add_columns(qw/junk/);

La classe Ventes est dérivée de la classe ParentVentes et ajoute l’attribut


manquant. Vous pouvez utiliser les deux classes si besoin. Notez que la
configuration de la table est requise aussi dans la classe dérivée.

1
https://en.wikipedia.org/wiki/Aliasing_%28computing%29

106 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Jointure par hachage

Vous pouvez récupérer tous les employés avec la lecture en avance ou


simplement sélectionner les colonnes comme indiqué ci-dessous :

my @ventes =
$schema->resultset('ParentVentes')
->search($cond
,{ join => 'employe'
,'+columns' => ['employe.prenom'
,'employe.nom']
}
);

Il n’est pas possible de charger seulement les colonnes sélectionnées à


partir de la table parent, ParentVentes dans ce cas.

DBIx::Class 0.08192 génère la requête SQL suivante. Elle récupère toutes


les colonnes de la table VENTES et les attributs sélectionnés à partir
d’EMPLOYES :

SELECT me.id_vente,
me.id_employe,
me.id_supplementaire,
me.date_vente,
me.valeur_eur,
employe.prenom,
employe.nom
FROM sales me
JOIN employes employe
ON ( employe.id_employe = me.id_employe
AND employe.id_supplementaire = me.id_supplementaire)
WHERE(date_vente > ?)

PHP
La version 2 de l’outil Doctrine supporte la sélection des attributs à
l’exécution. La documentation indique que les objets partiellement
chargés pourraient se comporter bizarrement et requièrent le mot-
clé partial pour accepter les risques. De plus, vous devez sélectionner
explicitement les colonnes de la clé primaire :

$qb = $em->createQueryBuilder();
$qb->select('partial v.{id_vente, date_vente, valeur_eur},'
. 'partial e.{employee_id, id_supplementaire, '
. 'prenom , nom}')
->from('Ventes', 'v')
->join('v.employe', 'e')
->where("v.date_vente > :dt")
->setParameter('dt', $dt, Type::DATETIME);

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 107


chapitre 4 : Opération de jointure

La requête SQL générée contient les colonnes demandées ainsi que les
colonnes ID_SUPPLEMENTAIRE et ID_EMPLOYE provenant de la table VENTES.

SELECT v0_.id_vente AS id_vente0,


v0_.date_vente AS date_vente1,
v0_.valeur_eur AS valeur_eur2,
e1_.id_employe AS id_employe3,
e1_.id_supplementaire AS id_supplementaire4,
e1_.prenom AS prenom5,
e1_.nom AS nom6,
v0_.id_supplementaire AS id_supplementaire7,
v0_.id_employe AS id_employe8
FROM ventes v0_
INNER JOIN employes e1_
ON v0_.id_supplementaire = e1_.id_supplementaire
AND v0_.id_employe = e1_.id_employe
WHERE v0_.date_vente > ?

Les objets renvoyés sont compatibles avec des objets complètement


chargés mais les colonnes manquantes ne sont pas renseignées. Y
accéder ne déclenche pas une exception.

Avertissement
MySQL ne connaît pas les jointures par hachage (il s’agit de la
demande de fonctionnalité 59025).

108 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Fusion par tri

Fusion par tri


La jointure « sort-merge » combine deux listes triées comme une fermeture
éclair. Les deux côtés de la jointure doivent être triés par les prédicats de
jointure.

Une jointure « sort-merge » a besoin des mêmes index que la jointure par
hachage, autrement dit un index sur les conditions indépendantes pour lire
tous les enregistrements candidats d’un coup. L’indexation des prédicats
de jointure est inutile. Cela ressemble à une jointure par hachage pour
l’instant. Néanmoins, il y a un aspect unique à ce type de jointure : la
symétrie absolue. L’ordre de jointure ne fait aucune différence, même pas
au niveau des performances. Cette propriété est très utile pour les jointures
externes. Pour les autres algorithmes, la direction des jointures externes
(gauche ou droite) impose l’ordre de jointure, mais pas pour les jointures
par fusion. La jointure par fusion peut même faire une jointure externe
gauche ou droite en même temps. Cela s’appelle une jointure externe
complète.

Bien que la jointure par fusion est plutôt performante une fois que les
éléments en entrée sont triés, elle est rarement utilisée car le tri de chaque
côté coûte cher. D’un autre côté, la jointure par hachage a besoin de ne
prétraiter qu’un seul côté.

La force d’une jointure par fusion apparaît immédiatement si les entrées


sont déjà triées. Il est possible d’exploiter l’ordre d’un index pour éviter le
tri manuel. Le Chapitre 6, « Trier et grouper » explique ce concept en détail.
Néanmoins, l’algorithme de jointure par hachage est supérieur dans bien
des cas.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 109


110 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>
Chapitre 5

Regrouper les données


La deuxième puissance de l’indexation
Le terme cluster est utilisé dans différents cas. Par exemple, un amas
stellaire (amas stellaire en français) est un groupe d’étoiles. Un cluster
d’ordinateurs est par contre un groupe d’ordinateurs qui fonctionnent
de concert, soit pour résoudre un problème complexe (cluster de haute
performance) soit pour augmenter sa disponibilité (cluster de failover). Plus
généralement, les clusters sont des groupes d’objets.

Au niveau informatique, il existe un autre type de cluster, souvent mal


compris : le « data cluster », traduisible en regroupement de données.
Regrouper les données signifie stocker ensemble les données accédées
consécutivement pour que l’accès à ces données nécessite moins d’opéra-
tions d’entrées/sorties. Les regroupements de données sont très importants
au niveau de l’optimisation des bases de données. D’un autre côté, les
groupes d’ordinateurs sont aussi très communs dans le contexte de la
base de données. Cela rend le terme cluster d’autant plus ambigu. Dire
« Utilisons un cluster pour améliorer les performances de la base de
données » en est un bon exemple : cela peut faire référence à un groupe
d’ordinateurs comme à un regroupement de données. Dans ce chapitre,
cluster fait référence à un regroupement de données.

Le plus simple regroupement de données dans une base de données SQL


est la ligne. Les bases de données enregistrent toutes les colonnes d’une
ligne dans le même bloc d’une base si possible. Des exceptions surviennent
si une ligne ne tient pas sur un seul bloc, par exemple lorsque des données
de taille variable sont à prendre en compte.

Stockage en colonne
Les bases de données orientées colonnes, ou entrepôts colonne, orga-
nisent les tables en colonne. Ce modèle est intéressant lors de l’accès
à de nombreuses lignes mais à peu de colonnes à la fois. Il s’avère que
ce modèle est très commun parmi les entrepôts de données (OLAP).

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 111


chapitre 5 : Regrouper les données

Les utilisateurs peuvent regrouper les données grâce aux index. Cela a
déjà été expliqué dans le Chapitre 1, « Anatomie d’un index » : les nœuds
feuilles de l’index stockent les colonnes indexées de façon triée pour que les
valeurs similaires soient stockées les unes à côté des autres. Cela signifie
que les index sont des regroupements de données de valeurs similaires.
Cette possibilité de regrouper les données est si essentielle que je l’appelle
la deuxième puissance de l’indexation.

Les sections suivantes expliquent comment utiliser les index pour regrouper
les données et améliorer les performances des requêtes.

Prédicats de filtre utilisés


intentionnellement sur des index
Très souvent, des prédicats de filtre d’index indiquent une mauvaise
utilisation d’un index causé par un ordre incorrect des colonnes dans un
index concaténé. Toutefois, les prédicats de filtre d’index peuvent aussi
être utilisés pour une bonne raison : pas pour améliorer les performances
d’un parcours d’intervalle, mais pour regrouper des données accédées
consécutivement.

Les prédicats de la clause where qui ne peuvent pas servir en tant que
prédicat d’accès sont de bons candidats pour cette technique :

SELECT prenom, nom, id_supplementaire, numero_telephone


FROM employes
WHERE id_supplementaire = ?
AND UPPER(nom) LIKE '%INA%';

Rappelez-vous que les expressions LIKE avec des caractères joker en début
ne peuvent pas utiliser d’index. Cela signifie que l’indexation de NOM ne
peut pas réduire l’intervalle parcouru dans l’index, peu importe que vous
indexiez nom ou UPPER(nom). Du coup, cette condition n’est pas un bon
candidat pour l’indexation.

Néanmoins, la condition sur ID_SUPPLEMENTAIRE est tout à fait convenable


pour l’indexation. Nous n’avons même pas besoin d’ajouter un nouvel index
car la colonne ID_SUPPLEMENTAIRE est déjà la première colonne dans l’index
de la clé primaire.

112 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Prédicats de filtre utilisés intentionnellement sur des index

--------------------------------------------------------------
|Id | Operation | Name | Rows | Cost |
--------------------------------------------------------------
| 0 | SELECT STATEMENT | | 17 | 230 |
|*1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES | 17 | 230 |
|*2 | INDEX RANGE SCAN | EMPLOYE_PK | 333 | 2 |
--------------------------------------------------------------

Predicate Information (identified by operation id):


---------------------------------------------------
1 - filter(UPPER("NOM") LIKE '%INA%')
2 - access("ID_SUPPLEMENTAIRE"=TO_NUMBER(:A))

Dans le plan d’exécution ci-dessus, le coût est multiplié par 100 de


l’opération INDEX RANGE SCAN à l’opération TABLE ACCESS BY INDEX ROWID.
Autrement dit, l’accès à la table est la cause du plus gros travail. C’est
un modèle courant et n’est pas un problème en soi. Néanmoins, c’est
le contributeur le plus significatif à la durée d’exécution globale de cette
requête.

L’accès à la table n’est pas nécessairement un goulet d’étranglement si les


lignes accédées sont enregistrées dans le même bloc de table car la base de
données peut récupérer toutes les lignes en une seule opération de lecture.
Si les mêmes lignes sont réparties sur de nombreux blocs, en contraste,
l’accès à la table peut devenir un sérieux problème de performance parce
que la base de données doit récupérer plusieurs blocs pour retrouver
toutes les lignes. Cela signifie que les performances dépendent de la
distribution physique des lignes accédées... autrement dit, elles dépendent
du regroupement des lignes.

Note
La corrélation entre l’ordre de l’index et l’ordre de la table est un test
de performance appelé le facteur de regroupement de l’index.

En fait, il est possible d’améliorer les performances des requêtes en


réordonnant les lignes dans la table pour qu’elles correspondent à l’ordre
des enregistrements dans l’index. Néanmoins, cette méthode est peu
souvent applicable car vous pouvez seulement enregistrer les lignes de
la table en une séquence. Cela signifie que vous ne pouvez optimiser la
table que pour un seul index. Même en choisissant un seul index, l’exercice
reste difficile car la plupart des bases de données n’offrent que des outils
rudimentaires pour réaliser cette tâche. Le séquençage des lignes n’est
qu’une approche peu pratique.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 113


chapitre 5 : Regrouper les données

Le facteur de regroupement de l’index


Le facteur de regroupement de l’index (en anglais, « index clustering
factor ») est une mesure indirecte de la probabilité pour que deux
entrées d’index à la suite fassent référence au même bloc de la table.
L’optimiseur prend cette probabilité en compte lors du calcul du coût
d’une ligne pour une opération TABLE ACCESS BY INDEX ROWID.

C’est exactement là que survient la deuxième puissance de l’indexation, le


regroupement des données. Vous pouvez ajouter de nombreuses colonnes à
un index pour qu’elles soient automatiquement enregistrées dans un ordre
bien défini. Cela fait de l’index un outil puissant bien que simple pour le
regroupement des données.

Pour appliquer ce concept à la requête ci-dessus, nous devons étendre


l’index pour qu’il couvre toutes les colonnes provenant de la clause where,
même s’ils ne réduisent pas l’intervalle parcouru dans l’index :

CREATE INDEX empsousnommaj ON employes


(id_supplementaire, UPPER(nom));

La colonne ID_SUPPLEMENTAIRE est la première colonne de l’index pour


qu’elle puisse être utilisée comme prédicat d’accès. L’expression UPPER(nom)
couvre le filtre LIKE comme un prédicat de filtre de l’index. Indexer la
représentation en majuscule aide à diminuer l’utilisation du processeur à
l’exécution, mais un simple index sur NOM fonctionnerait tout aussi bien.
Vous en apprendrez plus sur cette technique dans la prochaine section.

----------------------------------------------------------------
|Id | Operation | Name | Rows | Cost |
----------------------------------------------------------------
| 0 | SELECT STATEMENT | | 17 | 20 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMPLOYES | 17 | 20 |
|*2 | INDEX RANGE SCAN | EMPSOUSNOMMAJ| 17 | 3 |
----------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("ID_SUPPLEMENTAIRE"=TO_NUMBER(:A))
filter(UPPER("NOM") LIKE '%INA%')

Le nouveau plan d’exécution montre les mêmes opérations qu’auparavant.


Néanmoins, le coût a baissé fortement. Les informations de prédicat

114 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Prédicats de filtre utilisés intentionnellement sur des index

montrent que le filtre LIKE est déjà appliqué lors de l’opération INDEX RANGE
SCAN. Les lignes qui ne satisfont pas le filtre LIKE sont immédiatement
ignorées. L’accès à la table n’a plus de prédicat de filtre. Cela signifie qu’il
n’a plus besoin de charger des lignes qui ne satisfont pas la clause where.

La différence entre les deux plans d’exécutions est clairement visible dans
la colonne « Rows ». Suivant les estimations de l’optimiseur, la requête
récupère à la fin 17 enregistrements. Le parcours d’index dans le premier
plan d’exécution en récupère 333. La base de données doit alors charger
ces 333 lignes de la table pour leur appliquer le filtre LIKE qui en réduit
le nombre à 17. Dans le deuxième plan d’exécution, l’accès à l’index ne
doit pas délivrer ces lignes car la base de données doit exécuter l’opération
TABLE ACCESS BY INDEX ROWID 17 fois seulement.

Notez aussi que le coût de l’opération INDEX RANGE SCAN augmente de deux
à trois à cause de la colonne supplémentaire qui rend l’index plus gros. Au
vue du gain de performances, c’est un compromis acceptable.

Avertissement
Ne pas ajouter un nouvel index uniquement pour les prédicats de
filtre. Améliorez un index existant pour garder l’effort de mainte-
nance bas. Avec certaines bases de données, vous pouvez même
ajouter des colonnes à l’index de la clé primaire, sans qu’elles ne
fassent partie de la clé primaire.

Cet exemple trivial semble confirmer la sagesse commune qui revient à


indexer chaque colonne comprise dans une clause where. Néanmoins, cette
« sagesse » ignore l’importance de l’ordre des colonnes qui détermine les
conditions pouvant être utilisées comme prédicats d’accès et donc qui peut
avoir un impact important sur les performances. La décision sur l’ordre des
colonnes ne devrait jamais être laissée au hasard.

L’index grossit avec le nombre de colonnes, tout spécialement avec


les colonnes de texte. Bien sûr, la performance n’est pas meilleure
avec un index plus gros bien que la scalabilité logarithmique limite
considérablement l’impact. Vous ne devez en aucun cas ajouter dans l’index
toutes les colonnes mentionnées dans la clause where. À la place, utilisez
seulement les prédicats de filtres pour réduire le volume de données lors
de l’étape d’exécution précédente.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 115


chapitre 5 : Regrouper les données

Parcours d’index seul


Le parcours d’index seul est une des optimisations les plus puissantes. Non
seulement cela évite d’accéder à la table pour évaluer la clause where mais
cela évite aussi complètement d’accéder à la table si la base de données
peut trouver les colonnes sélectionnées dans l’index.

Pour couvrir une requête entière, un index doit contenir toutes les colonnes
de la requête SQL, en particulier celles de la clause select comme indiqué
dans l’exemple suivant :

CREATE INDEX ventes_sous_eur


ON ventes
( id_supplementaire, valeur_eur );

SELECT SUM(valeur_eur)
FROM ventes
WHERE id_supplementaire = ?;

Bien sûr, indexer la clause where est prioritaire par rapport aux autres
clauses. La colonne ID_SUPPLEMENTAIRE est du coup en première position
pour que l’index soit utilisable en tant que prédicat d’accès.

Le plan d’exécution montre que l’index est parcouru sans faire appel à un
parcours de table (pas d’opération TABLE ACCESS BY INDEX ROWID).

------------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 104 |
| 1 | SORT AGGREGATE | | 1 | |
|* 2 | INDEX RANGE SCAN| VENTES_SOUS_EUR | 40388 | 104 |
------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("SUBSIDIARY_ID"=TO_NUMBER(:A))

L’index couvre la requête complète, donc il est aussi appelé index couvrant.

116 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Parcours d’index seul

Note
Si un index empêche un accès à la table, il est aussi appelé un index
couvrant.
Néanmoins, le terme est ambigu car il pourrait passer pour une
propriété de l’index. L’expression « parcours d’index seul » suggère
à raison qu’il s’agit d’une opération dans un plan d’exécution.

L’index a une copie de la colonne VALEUR_EUR, donc la base de données peut


utiliser la valeur stockée dans l’index. Accéder à la table n’est pas requis car
l’index a toutes les informations pour satisfaire la requête.

Un parcours d’index seul peut améliorer très fortement les performances.


Regardez l’estimation du nombre de lignes dans le plan d’exécution :
l’optimiseur s’attend à agréger plus de 40 000 lignes. Cela signifie que le
parcours d’index seul a empêché la lecture de 40 000 blocs de table, si
chaque ligne se trouve dans un bloc différent de la table. Si l’index a un
bon facteur de regroupement, autrement dit si les lignes respectives sont
bien regroupées dans un petit nombre de blocs de la table, l’avantage du
parcours d’index seul sera moindre.

En dehors du facteur de regroupement, le nombre de lignes sélectionnées


limite le gain potentiel de performances d’un parcours d’index seul. Si
vous sélectionnez une seule ligne par exemple, vous pouvez seulement
économiser un accès à la table. Comme le parcours d’arbre a besoin de
récupérer quelques blocs, l’accès sauvé à la table peut devenir négligeable.

Important
L’avantage en termes de performance d’un parcours d’index seul
dépend du nombre de lignes accédées et du facteur de regroupement
de l’index.

Le parcours d’index seul est une stratégie d’indexation agressive. Il ne


faut pas concevoir un index pour un parcours d’index seul sur de simples
suppositions car il utilise plus de mémoire et augmente l’effort de
maintenance lors des requêtes update. Voir le Chapitre 8, « Modifier les
données ». En pratique, vous devez tout d’abord indexer sans considérer la
clause select et étendre l’index seulement si cela se révèle nécessaire.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 117


chapitre 5 : Regrouper les données

Les parcours d’index seul peuvent aussi causer de mauvaises surprises, par
exemple si nous limitons la requête aux ventes récentes :

SELECT SUM(valeur_eur)
FROM ventes
WHERE id_supplementaire = ?
AND date_vente > ?;

Sans regarder le plan d’exécution, on pourrait s’attendre à ce que la requête


soit rapide car elle sélectionne peu de lignes. Néanmoins, la clause where
fait référence à une colonne qui ne se trouve pas dans l’index pour que la
base de données accède à la table pour charger cette colonne.

---------------------------------------------------------------
|Id | Operation | Name | Rows |Cost |
---------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 371 |
| 1 | SORT AGGREGATE | | 1 | |
|*2 | TABLE ACCESS BY INDEX ROWID| VENTES | 2019 | 371 |
|*3 | INDEX RANGE SCAN | DATE_VENTE | 10541 | 30 |
---------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - filter("ID_SUPPLEMENTAIRE"=TO_NUMBER(:A))
3 - access("DATE_VENTE">:B)

L’accès à la table augmente le temps de réponse bien que la requête


sélectionne moins de lignes. Le facteur adéquat n’est pas le nombre de
lignes que la requête ramène mais le nombre de lignes que la requête doit
inspecter pour trouver les bonnes lignes.

Avertissement
Étendre la clause where peut causer un comportement illogique au
niveau des performances. Vérifiez le plan d’exécution avant d’étendre
les requêtes.

Si un index ne peut plus être utilisé pour un parcours d’index seul,


l’optimiseur choisira le prochain meilleur plan d’exécution. Autrement dit,
il pourrait choisir un plan d’exécution complètement différent ou, comme
ci-dessus, un plan similaire avec un autre index. Dans notre cas, il utilise
un index sur DATE_VENTE, qui date du chapitre précédent.

Du point de vue de l’optimiseur, cet index a deux avantages sur


VENTES_SOUS_EUR. L’optimiseur pense que le filtre sur DATE_VENTE est plus

118 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Parcours d’index seul

sélectif que celui sur ID_SUPPLEMENTAIRE. Vous pouvez vous en rendre


compte en regardant la colonne « Rows » des deux derniers plans
d’exécutions (environ 10 000 pour l’un et 40 000 pour l’autre). Néanmoins,
ces estimations sont plutôt arbitraires car la requête utilise les paramètres
liés. La condition DATE_VENTE pourrait sélectionner la table entière si un
utilisateur fournit la date de la première vente.

Le deuxième avantage de l’index sur DATE_VENTE est son meilleur facteur de


regroupement. Cette raison est valide car la table VENTES grossit seulement
chronologiquement. Les nouvelles lignes sont toujours ajoutées à la fin
de la table, tant qu’aucune ligne n’a été supprimée. L’ordre de la table
correspond donc à l’ordre de l’index car les deux sont grossièrement triées
chronologiquement. L’index a un bon facteur de regroupement.

Lors de l’utilisation d’un index doté d’un bon facteur de regroupement, les
lignes sélectionnées sont stockées proches les unes des autres pour que la
base de données n’ait besoin de lire que quelques blocs de la table pour
obtenir toutes les lignes. En utilisant cet index, la requête pourrait être
assez rapide sans même utiliser un parcours d’index seul. Dans ce cas, nous
devons supprimer les colonnes inutiles de l’autre index.

Note
Certains index ont automatiquement un bon facteur de regroupe-
ment. L’avantage des parcours d’index seul est minimal pour eux.

Dans cet exemple particulier, c’était une coïncidence heureuse. Le nouveau


filtre sur DATE_VENTE a non seulement empêché un parcours d’index seul
mais a aussi ouvert un nouveau chemin d’accès en même temps. Du coup,
l’optimiseur était capable de limiter l’impact sur les performances de ce
changement. Néanmoins, il est aussi possible d’empêcher un parcours
d’index seul en ajoutant des colonnes sur d’autres clauses. Ajouter une
colonne à la clause select ne peut jamais ouvrir un nouveau chemin
d’accès, ce qui limiterait l’impact de la perte du parcours d’index seul.

Astuce
Maintenez vos parcours d’index seul.
Ajoutez des commentaires pour vous rappeler le parcours d’index
seul et faites références à cette page pour que tout le monde puisse
en prendre connaissance.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 119


chapitre 5 : Regrouper les données

Les index fonctionnels peuvent aussi causer de mauvaises suprises relatives


aux parcours d’index seul. Un index sur UPPER(nom) ne peut pas être utilisé
pour un parcours d’index seul lors de la sélection de la colonne NOM. Dans
la section précédente, nous aurions dû indexer la colonne NOM elle-même
pour supporter le filtre LIKE et lui permettre d’être utilisé pour un parcours
d’index seul lors de la sélection de la colonne NOM.

Astuce
Avoir toujours comme but d’indexer la donnée originale car elle est
souvent l’information la plus utile à placer dans un index.
Évitez les index fonctionnels pour les expressions qui ne peuvent pas
être utilisées comme prédicats d’accès.

Agréger des requêtes comme celle ci-dessus est excellent pour les parcours
d’index seul. Elles récupèrent plusieurs lignes mais peu de colonnes,
rendant un petit index suffisant pour supporter un parcours d’index seul.
Plus vous demandez de colonnes, plus vous devez ajouter de colonnes
à l’index qui servira au parcours d’index seul. En tant que développeur,
vous devez du coup seulement sélectionner les colonnes dont vous avez
réellement besoin.

Astuce
Évitez select * et récupérez seulement les colonnes dont vous avez
besoin.

Sans compter qu’indexer un grand nombre de lignes demande beaucoup


d’espace disque, vous pouvez aussi atteindre les limites de votre base de
données. La plupart des bases de données impose des limites assez rigides
sur le nombre de colonnes par index et sur la taille totale d’une entrée d’un
index. Cela signifie que vous ne pouvez pas indexer un nombre arbitraire
de colonnes, pas plus que des colonnes arbitrairement longues. L’aperçu
suivant liste les limitations les plus importantes. Néanmoins, il existe des
index qui couvrent la table entière comme nous le voyons dans la prochaine
section.

Réflexion
Les requêtes qui ne sélectionnent aucune colonne de table sont
souvent exécutées avec des parcours d’index seul.
Pouvez-vous trouver un exemple intéressant ?

120 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Parcours d’index seul

INCLUDE: Colonnes non clés


SQL Server et PostgreSQL 11+ acceptent des colonnes appelées
non clés dans les index B-tree. Nous avons discuté des colonnes
clés jusqu’à maintenant. Les colonnes non clés sont des colonnes
enregistrées uniquement dans les noeuds feuilles et, de ce fait, ne
peuvent pas être utilisées pour satisfaire les prédicats.
Les colonnes non clés sont précisées dans la clause include :

CREATE INDEX empsubupnam


ON employees
(subsidiary_id, last_name)
INCLUDE(phone_number, first_name)

MySQL
MySQL avec InnoDB limite la longueur totale des clés (toutes les
colonnes) à 3072 octets. De plus, la longueur de chaque colonne est
limitée à 767 octets si l’option innodb_large_prefix n’est pas activée
ou si les formats ligne autre que DYNAMIC ou COMPRESSED sont utilisés.
C’était la valeur par défaut jusqu’à la version 5.6 de MySQL. Les index
MyISAM sont limités à 16 colonnes et une longueur maximale de clés
de 1000 octets.

MySQL dispose d’une fonctionnalité unique appelée « indexation


de préfixe » (quelque fois aussi appelé « indexation partielle »).
Cette fonctionnalité permet de n’indexer que les quelques premiers
caractères d’une colonne, donc cela n’a rien à voir avec les index
partiels décrits dans Chapitre 2. Si vous indexez une colonne qui
dépasse la longueur maximale autorisée (767, 1000 ou 3072 octets
comme décrit ci-dessus), MySQL pourrait, suivant le mode SQL et
le format ligne, tronquer la colonne. Dans ce cas, l’instruction
create index réussit avec le message d’avertissement “Specified key
was too long; max key length is … bytes” (en français, « La clé spécifiée
était trop longue ; la longueur maximale d’une clé est de … octets »).
Ceci signifie que l’index n’a plus une copie complète de la colonne.
Sélectionner la colonne empêche un parcours uniquement de l’index
(similaire aux index fonctionnels).

Vous pouvez utiliser l’indexation du préfixe de MySQL explicitement


pour empêcher le dépassement de la limite de la longueur de la clé si
vous obtenez le message d’erreur « Specified key was too long; max

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 121


chapitre 5 : Regrouper les données

key length is … bytes. ». L’exemple suivant crée un index sur les dix
premiers caractères de la colonne NOM.

CREATE INDEX .. ON employes (nom(10));

Oracle Database
La longueur maximale d’une clé d’index dépend de la taille du bloc et
des paramètres de stockage de l’index (75% de la taille d’un bloc de la
base moins l’en-tête). Un index B-tree est limité à 32 colonnes.

Lors de l’utilisation d’Oracle 11g avec toutes les valeurs par défaut (donc
des blocs de 8 Ko), la longueur maximale de la clé d’un index est de 6398
octets. Dépasser cette limite renvoie le message d’erreur « ORA-01450:
maximum key length (6398) exceeded. »
PostgreSQL
La base de données PostgreSQL supporte les parcours d’index seul
depuis la version 9.2.

La longueur des enregistrements B-tree est limitée à 2713 octets (codé


en dur, approximativement BLCKSZ/3). Le message d’erreur respectif «
index row size ... exceeds btree maximum, 2713 » apparaît seulement lors
de l’exécution d’un insert ou d’un update qui dépasse la limite. Les
index B-tree peuvent contenir jusqu’à 32 colonnes.
SQL Server
Depuis la version 2016, SQL Server supporte jusqu’à 32 colonnes.
La limite de longueur est 1700 octets (900 octets pour les index
1
clusterisés). Les colonnes non clés ne sont pas comptabilisées pour ce
qui concerne cette limite.

Tables organisées en index


Le parcours d’index seul exécute une requête SQL en utilisant seulement
les données redondantes enregistrées dans l’index. Les données originales
disponibles dans la table (heap) ne sont pas nécessaires. Si nous prenons
ce concept au niveau supérieur et plaçons toutes les colonnes dans l’index,
nous pourrions nous demander pourquoi nous avons besoin de la table.

De ce fait, certaines bases de données utilisent un index comme principal


stockage de la table. La base de données Oracle appelle ce concept table

Avant SQL Server 2016 : 16 colonnes et 900 octets.

122 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Tables organisées en index

organisée en index (IOT, pour « index-organized table »), d’autres bases de


données utilisent le terme index regroupé (« clustered index » en anglais).
Dans cette section, les deux termes sont utilisés pour mettre l’accent sur
les caractéristiques de la table ou de l’index, suivant le besoin.

Ainsi, une table organisée en index est un index B-tree sans table (heap).
Cela a deux avantages : on évite de perdre de l’espace disque et chaque
accès à un index regroupé est automatiquement un parcours d’index seul.
Ces deux avantages sont très prometteurs mais difficilement atteignables
en pratique.

Les inconvénients des tables organisées en index deviennent apparents lors


de la création d’un autre index sur la même table. De façon analogue à un
index standard, un index secondaire fait référence aux données de la table
originale, qui sont stockées dans l’index regroupé. Là, les données ne sont
pas enregistrées statiquement comme dans une table mais peuvent être
déplacées à tout moment pour maintenir l’ordre de l’index. Du coup, il n’est
pas possible d’enregistrer l’emplacement physique des lignes dans la table
organisée en index dans l’index secondaire. La base de données doit utiliser
une clé logique à la place.

Le graphe suivant montre une recherche via un index pour trouver toutes
les ventes du 23 mai 2012. En comparaison, nous allons tout d’abord
regarder la Figure 5.1 qui montre le processus lors de l’utilisation d’une
table standard. L’exécution implique deux étapes : l’opération INDEX RANGE
SCAN et l’opération TABLE ACCESS BY INDEX ROWID.

Figure 5.1. Accès basé sur l’index à une table standard


Index B-tree Table
E

UR

TE
VA LOY
_E E

_E

EN
ID ENT

2012-05-20 ROWID
MP

UR

_V
_V

LE

TE

2012-05-20 ROWID
ID

DA

2012-05-20 2012-05-23 ROWID 23 21 9.99 2010-02-23


2012-05-23 87 20 4.99 2012-05-23
2012-05-24
2012-05-25 2012-05-23 ROWID
44 44 2.49 2011-07-04
2012-05-24 ROWID
73 84 5.99 2012-05-23
2012-05-24 ROWID

INDEX RANGE SCAN TABLE ACCESS BY INDEX ROWID

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 123


chapitre 5 : Regrouper les données

Bien que l’accès à la table puisse devenir un goulet d’étranglement, cela


reste limité à une opération de lecture par ligne car l’index dispose du ROWID
comme pointeur direct vers la ligne de la table. La base de données peut
immédiatement charger la ligne à partir de la table standard car l’index
a sa position exacte. Néanmoins, le graphe change lors de l’utilisation de
l’index secondaire sur une table organisée en index. Un index secondaire ne
stocke pas de pointeurs physiques (ROWID) mais seulement les valeurs clés
de l’index regroupé, fréquemment appelées clé de regroupement. Il s’agit
souvent de la clé primaire de la table organisée en index.

Accéder à un index secondaire ne ramène pas un ROWID mais une clé logique
pour rechercher la ligne dans l’index regroupé. Néanmoins, un seul accès
n’est pas suffisant pour rechercher dans l’index regroupé. Cela nécessite un
parcours complet de l’arbre. Autrement dit, un accès à la table via un index
secondaire fait une recherche dans deux index : tout d’abord dans l’index
secondaire (INDEX RANGE SCAN), puis dans l’index regroupé pour chaque ligne
trouvée dans l’index secondaire (INDEX UNIQUE SCAN).

Figure 5.2. Index secondaire sur un IOT


Index secondaire Table organisée comme un index
(index clusterisé)
YE

UR

TE
TE

LO

_E

EN
EN

MP

UR

_V
_V

_E

LE

TE
ID

ID

VA

DA

71
72 54 8.99 2009-09-23
73
2012-05-20 65 73 20 4.99 2012-05-23
75
2012-05-20 46
2012-05-20 2012-05-23 73 75
2012-05-23
82
2012-05-24
2012-05-23 87 90
2012-05-25
2012-05-24 22
86
2012-05-24 50 87 84 5.99 2012-05-23
88
88 14 2.49 2008-03-25
90

INDEX RANGE SCAN INDEX UNIQUE SCAN

La Figure 5.2 rend évident le fait que le B-tree de l’index regroupé se tient
entre l’index secondaire et les données de la table.

124 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Tables organisées en index

Accéder à une table organisée en index via un index secondaire est très
inefficace, et cela peut être empêché de la même façon qu’on empêche un
accès à la table sur une table standard : en utilisant un parcours d’index
seul, qui est mieux décrit dans ce cas comme un « parcours d’index secon-
daire seul ». L’avantage au niveau des performances d’un parcours d’index
seul est encore plus important car il empêche non seulement un accès seul
mais aussi une opération INDEX UNIQUE SCAN entière.

Important
Accéder à une table organisée en index via un index secondaire est
très inefficace.

En utilisant cet exemple, nous pouvons aussi voir que les bases de données
exploitent toutes les redondances qu’elles ont. Gardez en tête qu’un index
secondaire enregistre la clé de regroupement pour chaque entrée d’index.
En conséquence, nous pouvons demander la clé de regroupement à partir
d’un index secondaire sans accéder à la table organisée en index :

SELECT id_vente
FROM ventes_iot
WHERE date_vente = ?;
--------------------------------------------------
| Id | Operation | Name | Cost |
--------------------------------------------------
| 0 | SELECT STATEMENT | | 4 |
|* 1 | INDEX RANGE SCAN| VENTES_IOT_DATE | 4 |
--------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("DATE_VENTE"=:DT)

La table VENTES_IOT est une table organisée en index qui utilise ID_VENTE
comme clé de regroupement. Bien que l’index VENTE_IOT_DATE soit
seulement sur la colonne DATE_VENTE, il a toujours une copie de la clé de
regroupement ID_VENTE pour qu’il puisse répondre à la requête en utilisant
seulement l’index secondaire.

Lors de la sélection des autres colonnes, la base de données doit exécuter


une opération INDEX UNIQUE SCAN sur l’index regroupé pour chaque ligne :

SELECT valeur_eur
FROM ventes_iot
WHERE date_vente = ?;

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 125


chapitre 5 : Regrouper les données

----------------------------------------------------
| Id | Operation | Name | Cost |
----------------------------------------------------
| 0 | SELECT STATEMENT | | 13 |
|* 1 | INDEX UNIQUE SCAN| VENTES_IOT_PK | 13 |
|* 2 | INDEX RANGE SCAN| VENTES_DATE_IOT | 4 |
----------------------------------------------------

Predicate Information (identified by operation id):


---------------------------------------------------
1 - access("DATE_VENTE"=:DT)
2 - access("DATE_VENTE"=:DT)

Les tables organisées en index et les index regroupés ne sont donc pas
si utiles. Les améliorations de performance sur l’index regroupé sont
facilement perdues lors de l’utilisation d’un index secondaire. La clé de
regroupement est généralement plus longue qu’un ROWID, du coup les index
secondaires sont plus gros qu’une table standard, ce qui élimine souvent
le gain réalisé par l’omission de la table standard. La force des tables
organisées en index et des index regroupés est souvent limitée aux tables
qui n’ont pas besoin d’index secondaire. Les tables standard ont l’intérêt
de fournir des données qui ne sont pas déplacées, ce qui facilite leur
référencement.

Important
Les tables avec un seul index sont intéressantes pour la mise en place
d’index regroupés ou de tables organisées en index.
Les tables ayant plus d’index peuvent généralement bénéficier du
format standard. Vous pouvez toujours utiliser les parcours d’index
seul pour éviter l’accès à la table. Cela vous donne les performances
d’un select sur un index regroupé sans ralentir les autres index.

Le support des bases de données pour les tables organisées en index et pour
les index regroupés est très différent. L’aperçu sur la page suivante explique
les spécificités les plus importantes.

126 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Tables organisées en index

MySQL
Le moteur MyISAM utilise seulement les tables standards alors que
le moteur InnoDB utilise toujours les index regroupés. Autrement dit,
vous n’avez pas le choix.

Oracle Database
La base de données Oracle utilise par défaut les tables standards. Les
tables organisées en index peuvent être créées en utilisant la clause
ORGANIZATION INDEX :

CREATE TABLE (
id NUMBER NOT NULL PRIMARY KEY,
[...]
) ORGANIZATION INDEX;

La base de données Oracle utilise toujours la clé primaire comme clé


de regroupement.

PostgreSQL
PostgreSQL utilise seulement les tables standards.

Néanmoins, vous pouvez utiliser la clause CLUSTER pour aligner le


contenu de la table standard avec un index.

SQL Server
Par défaut, SQL Server utilise des index regroupés (tables organisées
en index) en utilisant la clé primaire comme clé de regroupement.
Néanmoins, vous pouvez utiliser des colonnes arbitraires pour la clé de
regroupement, voire même des colonnes non uniques.

Pour créer une table standard, vous devez utiliser la clause


NONCLUSTERED dans la définition de la clé primaire :

CREATE TABLE (
id NUMBER NOT NULL,
[...]
CONSTRAINT pk PRIMARY KEY NONCLUSTERED (id)
);

Supprimer un index regroupé transforme la table en table standard.

Le comportement par défaut de SQL Server cause souvent des


problèmes de performances lors de l’utilisation d’index secondaires.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 127


chapitre 5 : Regrouper les données

Pourquoi les index secondaires


n’ont pas de ROWID
Un pointeur direct à une ligne de la table serait préférable dans
l’index secondaire. Cela est seulement possible si la ligne de la
table reste à la même position de stockage. Malheureusement, ce
n’est pas possible si la ligne est partie intégrante d’une structure
d’index qui est conservée dans l’ordre. Garder l’index dans l’ordre
rend parfois nécessaire de déplacer les lignes. Ceci est aussi vrai pour
les opérations qui n’affectent pas directement la ligne elle-même. Par
exemple, une requête insert pourrait diviser en deux un nœud feuille
pour gagner de la place pour la nouvelle entrée. Cela signifie que
certaines entrées sont déplacées dans un nouveau bloc de données.
D’un autre côté, une table standard ne conserve pas les lignes dans
un certain ordre. La base de données enregistre les nouvelles entrées
là où elle dispose de la place libre. Une fois écrite, la donnée n’est
pas déplacée.

128 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Chapitre 6

Trier et grouper
Trier est une opération très intensive pour les processeurs. Cependant, le
problème principal est que la base de données doit placer temporairement
les résultats dans un tampon. Une opération de tri doit lire toutes les
données avant de pouvoir renvoyer la première ligne. Les opérations de tri
ne peuvent pas être exécutées dans un pipeline, ce qui devient un problème
pour les gros ensembles de données.

Un index fournit une représentation ordonnée des données indexées : ce


principe a déjà été décrit dans le Chapitre 1. En fait, un index enregistre les
données en les pré-triant. L’index est trié comme si nous avions utilisé une
clause order by dans la définition de l’index. Il n’est donc pas surprenant
que nous puissions utiliser les index pour éviter l’opération de tri tout en
satisfaisant la clause order by.

De façon assez ironique, une opération INDEX RANGE SCAN devient elle-aussi
inefficace pour de gros volumes de données, tout spécialement si elle est
suivie d’un accès à la table. Ceci peut annuler le gain obtenu en évitant
l’opération de tri. Un FULL TABLE SCAN avec une opération de tri explicite
pourrait même être plus rapide dans ce cas. Encore une fois, c’est à
l’optimiseur d’évaluer les différents plans d’exécution et de sélectionner le
meilleur.

Une exécution d’un order by via un index ne permet pas seulement de


gagner sur l’effort de tri. Il est aussi capable de renvoyer les premiers
résultats sans traiter les données en entrée. L’order by peut donc être
exécuté dans un pipeline. Le Chapitre 7, Résultats partiels explique comment
exploiter l’exécution en pipeline pour implémenter des requêtes efficaces
de pagination. Une exécution en pipeline d’un order by est si importante
que j’y vois la troisième puissance de l’indexation.

Ce chapitre explique comment utiliser un index pour l’exécution d’un


order by en pipeline. Pour cela, nous allons faire particulièrement attention
aux interactions avec la clause where ainsi qu’aux modificateurs ASC et DESC.
Le chapitre conclut sur l’application de ces techniques à la clause group by.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 129


chapitre 6 : Trier et grouper

Indexer un tri
Les requêtes SQL disposant d’une clause order by n’ont pas besoin de trier
les résultats explicitement si l’index adéquat renvoie déjà les lignes dans
l’ordre requis. Cela signifie que le même index peut être utilisé pour la
clause where et pour la clause order by.

Prenons comme exemple la requête suivante qui sélectionne les ventes


d’hier ordonnées par la date de la vente et l’identifiant du produit :

SELECT date_vente, id_produit, quantite


FROM ventes
WHERE date_vente = TRUNC(sysdate) - INTERVAL '1' DAY
ORDER BY date_vente, id_produit;

Il existe déjà un index sur DATE_VENTE qui peut être utilisé pour la clause
where. Néanmoins, la base de données doit réaliser une opération de tri
explicite pour satisfaire la clause order by :

---------------------------------------------------------------
|Id | Operation | Name | Rows | Cost |
---------------------------------------------------------------
| 0 | SELECT STATEMENT | | 320 | 18 |
| 1 | SORT ORDER BY | | 320 | 18 |
| 2 | TABLE ACCESS BY INDEX ROWID| VENTES | 320 | 17 |
|*3 | INDEX RANGE SCAN | DATE_VENTE | 320 | 3 |
---------------------------------------------------------------

Un INDEX RANGE SCAN renvoie les lignes dans l’ordre de l’index de toute
façon. Pour maximiser cet avantage, nous avons juste besoin d’étendre la
définition de l’index pour qu’il corresponde à la clause de l’order by :

DROP INDEX date_ventes;


CREATE INDEX date_ventes_pr ON ventes (date_vente, id_produit);

------------------------------------------------------------------
|Id | Operation | Name | Rows | Cost |
------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 320 | 300 |
| 1 | TABLE ACCESS BY INDEX ROWID| VENTES | 320 | 300 |
|*2 | INDEX RANGE SCAN | DATE_VENTES_PR | 320 | 4 |
------------------------------------------------------------------

130 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Indexer un tri

L’opération de tri SORT ORDER BY a disparu du plan d’exécution même si la


requête a toujours une clause order by. La base de données exploite l’ordre
de l’index et n’a plus besoin d’une opération de tri explicite.

Important
Si l’ordre de l’index correspond à la clause order by, la base de
données peut omettre l’opération de tri explicite.

Même si le nouveau plan d’exécution contient moins d’opérations, le


coût a augmenté considérablement parce que le facteur de regroupement
du nouvel index est bien pire (voir « Facteur de regroupement
automatiquement optimisé » à la page 133). Il faut simplement noter
que le coût n’est pas toujours un bon indicateur de l’effort à l’exécution.

Pour cette optimisation, il est suffisant que l’intervalle parcouru de l’index


soit trié suivant la clause order by. Du coup, l’optimisation fonctionne aussi
pour cet exemple particulier lors du tri de ID_PRODUIT seul :

SELECT date_vente, id_produit, quantite


FROM ventes
WHERE date_vente = TRUNC(sysdate) - INTERVAL '1' DAY
ORDER BY id_produit;

Dans la Figure 6.1, nous voyons que la colonne ID_PRODUIT est le seul
critère de tri dans l’intervalle parcouru de l’index. Du coup, l’ordre de
l’index correspond à la clause order by dans cet intervalle d’index. La base
de données peut donc omettre l’opération de tri.

Figure 6.1. Ordre de tri dans l’intervalle adéquat de l’index

DATE_VENTE ID_PRODUIT

3 jours
auparavant

2 jours
auparavant

Intervalle
hier parcouru
de l'index

aujourd'hui

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 131


chapitre 6 : Trier et grouper

Cette optimisation peut causer un comportement inattendu lors de


l’agrandissement de l’intervalle parcouru de l’index :

SELECT date_vente, id_produit, quantite


FROM ventes
WHERE date_vente >= TRUNC(sysdate) - INTERVAL '1' DAY
ORDER BY id_produit;

Cette requête ne récupère pas les ventes d’hier mais toutes les ventes depuis
hier. Cela signifie qu’elle couvre plusieurs jours et parcourt un intervalle qui
n’est plus exclusivement trié par la colonne ID_PRODUIT. Si nous regardons
encore la Figure 6.1 et étendons l’intervalle parcouru de l’index jusqu’au
bout, nous voyons qu’il existe encore des valeurs plus petites de ID_PRODUIT.
Du coup, la base de données doit utiliser une opération explicite de tri pour
satisfaire la clause order by.

-----------------------------------------------------------------
|Id |Operation | Name | Rows | Cost |
-----------------------------------------------------------------
| 0 |SELECT STATEMENT | | 320 | 301 |
| 1 | SORT ORDER BY | | 320 | 301 |
| 2 | TABLE ACCESS BY INDEX ROWID| VENTES | 320 | 300 |
|*3 | INDEX RANGE SCAN | DATE_VENTE_PR | 320 | 4 |
-----------------------------------------------------------------

Si la base de données utilise une opération de tri alors que vous attendiez
une exécution en pipeline, cela peut avoir deux raisons : le plan d’exécution
avec une opération de tri explicite a un meilleur coût ; l’ordre de l’index
dans l’intervalle parcouru ne correspond pas à la clause order by.

Une façon simple de séparer les deux cas est d’utiliser la définition complète
de l’index dans la clause order by, autrement dit en ajustant la requête
d’après l’index pour éliminer la deuxième cause. Si la base de données utilise
toujours une opération explicite de tri, l’optimiseur préfère le plan à cause
de son coût : sinon la base de données ne peut pas utiliser l’index pour la
clause order by originale.

Astuce
Utilisez la définition complète de l’index dans la clause order by pour
trouver la raison d’une opération explicite de tri.

132 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Indexer un tri

Dans les deux cas, vous pourriez vous demander si et comment il est
possible d’avoir une exécution en pipeline d’un order by. Pour cela, vous
pouvez exécuter la requête avec la définition complète de l’index dans la
clause order by, puis vous inspectez le résultat. Vous réaliserez souvent que
vous avez une mauvaise perception de l’index et que l’ordre de l’index n’est
en fait pas requis par la clause originale de l’order by. La base de données
ne peut donc pas utiliser l’index pour éviter une opération de tri.

Si l’optimiseur préfère une opération de tri explicite grâce à son coût, c’est
généralement dû au fait que l’optimiseur prend le meilleur plan d’exécution
pour une exécution complète de la requête. En d’autres termes, l’optimiseur
choisit le plan d’exécution qui est le plus rapide pour obtenir la dernière
ligne. Si la base de données détecte que l’application ne récupère que
les quelques premières lignes, elle pourrait préférer exécuter l’order by
avec un index. Le Chapitre 7, Résultats partiels explique les méthodes
d’optimisation correspondantes.

Facteur de regroupement
automatiquement optimisé
La base de données Oracle conserve le facteur de regroupement à un
minimum en se basant sur le ROWID pour l’ordre de l’index. Quand
deux entrées d’index ont les mêmes valeurs de clé, le ROWID décide
de l’ordre final. Du coup, l’index est aussi ordonné suivant l’ordre de
la table et a le facteur de regroupement le plus petit possible car le
ROWID représente l’adresse physique de la ligne de la table.
En ajoutant une autre colonne à un index, vous insérez un nouveau
critère de tri avant le ROWID. La base de données a moins de liberté
pour aligner les entrées de l’index suivant l’ordre de la table, donc le
facteur de regroupement de l’index peut seulement empirer.
Quoiqu’il en soit, il est toujours possible que l’ordre de l’index
corresponde grossièrement à l’ordre de la table. Les ventes d’un jour
sont probablement toujours regroupées dans la table ainsi que dans
l’index, même si leur séquence n’est plus exactement identique. La
base de données doit lire les blocs de la table plusieurs fois lors
de l’utilisation de l’index DATE_VENTE_PR mais ce sont les mêmes
blocs qu’avant. Grâce à l’utilisation d’un cache pour les données
fréquemment accédées, l’impact sur les performances pourrait être
bien moins que celui indiqué par les coûts estimés.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 133


chapitre 6 : Trier et grouper

Indexer ASC, DESC et NULLS FIRST/LAST


Les bases de données peuvent lire les index dans les deux sens. Cela signifie
qu’un order by en pipeline est aussi possible sur l’intervalle parcouru de
l’index dans l’ordre opposé à celui spécifié dans la clause order by. Bien que
les modificateurs ASC et DESC de la clause order by puissent empêcher une
exécution en pipeline, la plupart des bases de données propose un moyen
simple de changer l’ordre de l’index pour qu’un index soit quand même
utilisable.

L’exemple suivant utilise un index dans l’ordre inverse. Il fournit les ventes
depuis hier, ordonnées par date descendante et par ID_PRODUIT descendant.

SELECT date_vente, id_produit, quantite


FROM ventes
WHERE date_vente >= TRUNC(sysdate) - INTERVAL '1' DAY
ORDER BY date_vente DESC, id_produit DESC;

Le plan d’exécution montre que la base de données lit l’index en sens


inverse.

-----------------------------------------------------------------
|Id |Operation | Name | Rows | Cost |
-----------------------------------------------------------------
| 0 |SELECT STATEMENT | | 320 | 300 |
| 1 | TABLE ACCESS BY INDEX ROWID | VENTES | 320 | 300 |
|*2 | INDEX RANGE SCAN DESCENDING| DATE_VENTE_PR | 320 | 4 |
-----------------------------------------------------------------

Dans ce cas, la base de données utilise l’arbre de l’index pour trouver la


dernière entrée correspondante. À partir de là, il suit la chaîne de nœuds
feuilles en remontant comme indiqué dans la Figure 6.2. Cela explique
pourquoi la base de données utilise une liste doublement chaînée pour
construire la chaîne des nœuds feuilles.

Bien sûr, il est crucial que l’intervalle parcouru de l’index soit dans l’ordre
inverse exact, autrement dit dans le même sens que celui indiqué par la
clause order by.

Important
Les bases de données peuvent lire les index dans les deux sens.

134 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Indexer ASC, DESC et NULLS FIRST/LAST

Figure 6.2. Parcours d’index inverse


DATE_VENTE ID_PRODUIT

3 jours
auparavant

2 jours
auparavant

hier Intervalle
parcouru
de l'index
aujourd'hui

L’exemple suivant ne remplit pas cette condition car il mixe les


modificateurs ASC et DESC dans la clause order by :

SELECT date_vente, id_produit, quantite


FROM ventes
WHERE date_vente >= TRUNC(sysdate) - INTERVAL '1' DAY
ORDER BY date_vente ASC, id_produit DESC;

La requête doit tout d’abord récupérer les ventes d’hier ordonnées par la
colonne ID_PRODUIT en sens inverse, puis les ventes du jour ordonnées là-
aussi par la colonne ID_PRODUIT en sens inverse. La Figure 6.3 illustre ce
travail. Pour obtenir les ventes dans l’ordre demandé, la base de données
devrait sauter à un autre emplacement pendant le parcours de l’index.

Figure 6.3. Clause order by inutilisable dans un pipeline


DATE_VENTE ID_PRODUIT

3 jours
auparavant

2 jours
auparavant

Saut
hier impossible
dans l'index
aujourd'hui

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 135


chapitre 6 : Trier et grouper

Néanmoins, l’index n’a pas de lien permettant d’aller des ventes d’hier
ayant le plus petit ID_PRODUIT aux ventes d’aujourd’hui ayant le plus grand
ID_PRODUIT. Du coup, la base de données ne peut pas utiliser cet index pour
éviter une opération explicite de tri.

Pour ce genre de cas, la plupart des bases de données propose une méthode
simple pour ajuster l’ordre de l’index à la clause order by. Concrètement,
cela signifie que vous pouvez utiliser les modificateurs ASC et DESC dans la
déclaration de l’index :

DROP INDEX date_vente_pr;

CREATE INDEX date_vente_pr


ON ventes (date_vente ASC, id_produit DESC);

Avertissement
Avant la version 8.0, la base de données MySQL ignorait les
modificateurs ASC et DESC dans la définition de l’index.

Maintenant, l’ordre de l’index correspond à celui de la clause order by, donc


la base de données peut éviter l’opération de tri :

-----------------------------------------------------------------
|Id | Operation | Name | Rows | Cost |
-----------------------------------------------------------------
| 0 | SELECT STATEMENT | | 320 | 301 |
| 1 | TABLE ACCESS BY INDEX ROWID| VENTES | 320 | 301 |
|*2 | INDEX RANGE SCAN | DATE_VENTE_PR | 320 | 4 |
-----------------------------------------------------------------

La Figure 6.4 montre le nouvel ordre de l’index. Le changement du sens du


tri pour la deuxième condition a d’une certaine façon changé la direction
des flèches du graphique précédent. Ainsi, la première flèche se termine là
où la seconde flèche commence, ce qui fait que l’index dispose des lignes
dans le bon ordre.

Important
Lors de l’utilisation de modificateurs ASC et DESC mixés dans la clause
order by, vous devez définir l’index dans le même ordre pour pouvoir
l’utiliser en pipeline avec un order by.
Ceci n’affecte pas l’intérêt de l’index pour la clause where.

136 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Indexer ASC, DESC et NULLS FIRST/LAST

Figure 6.4. Index en ordre mixé

DATE_VENTE ID_PRODUIT

3 jours
auparavant

2 jours
auparavant

hier Aucun saut


nécessaire
aujourd'hui

L’indexation ASC/DESC est seulement nécessaire pour trier les colonnes


individuelles en sens inverse. Il n’est pas nécessaire d’inverser l’ordre
de toutes les colonnes car la base de données peut toujours lire l’index
dans l’ordre inverse si besoin est, les index secondaires sur les tables
organisées en index étant la seule exception. Les index secondaires ajoutent
implicitement la clé de regroupement à l’index sans fournir la possibilité de
spécifier l’ordre de tri. Si vous avez besoin de trier la clé de regroupement
en ordre inverse, vous n’avez pas d’autres options que de trier toutes les
autres colonnes en sens inverse. Alors la base de données peut lire l’index
en sens inverse pour obtenir l’ordre souhaité.

En dehors des modificateurs ASC et DESC, le standard SQL définit deux modi-
ficateurs moins connus de la clause order by : NULLS FIRST et NULLS LAST. Le
contrôle explicite sur le tri des valeurs NULL a été « récemment » introduit
comme extension optionnelle du standard SQL:2003. En conséquence, le
support de ce modificateur est peu fréquent. C’est assez inquiétant car le
standard ne définit pas exactement l’ordre de tri des valeurs NULL. Il indique
seulement que toutes les valeurs NULL doivent apparaître ensemble après
le tri mais il ne spécifie pas si elles doivent apparaître avant ou après les
autres valeurs. Vous aurez besoin de spécifier le tri des valeurs NULL pour
toutes les colonnes qui peuvent être NULL dans la clause order by pour
obtenir un comportement bien défini.

Cependant, l’extension optionnelle n’est implémentée ni par SQL Server


2017 ni par MySQL 5.7. La base de données Oracle propose le tri des valeurs
NULLS avant qu’il ait été introduit dans le standard mais il ne l’accepte
dans la définition des index que depuis la version 12c. La base de données

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 137


chapitre 6 : Trier et grouper

Oracle peut ne pas faire un tri en pipeline pour une clause order by lors
d’un tri avec NULLS FIRST. Seule la base de données PostgreSQL (depuis la
version 8.3) supporte le modificateur NULLS dans la clause order by et dans
la définition de la clause.

L’aperçu suivant résume les fonctionnalités fournies par les différentes


bases de données.

Figure 6.5. Matrice bases de données/fonctionnalités

QL

er
eS

rv
QL

Se
le
gr
te
ac

Li
st
yS
2

L
DB

SQ
SQ
Po
Or
M

Lecture inverse de l'index

Order by ASC/DESC

Index ASC/DESC

Order by NULLS FIRST/LAST

Default NULLS order Great Small Great Great Small Small

Index NULLS FIRST/LAST

138 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Indexer le « Group By »

Indexer le « Group By »
Les bases de données SQL utilisent deux algorithmes entièrement différents
pour le group by. Le premier, un algorithme de hachage, agrège les enregis-
trements en entrée dans une table de hachage temporaire. Une fois tous
les enregistrements en entrée traitées, la table de hachage est renvoyée
comme résultat. Le deuxième algorithme, l’algorithme de tri/groupe, trie
en premier lieu les données en entrée en suivant la clé de groupement
pour que les lignes de chaque groupe se suivent les unes les autres en une
succession immédiate. Après cela, la base de données a juste besoin de
les regrouper. En général, les deux algorithmes ont besoin de matérialiser
un état intermédiaire, donc elles ne sont pas exécutées via un pipeline.
Néanmoins, l’algorithme de tri/groupe peut utiliser un index pour éviter
l’opération de tri, et du coup activer l’exécution du group by en pipeline.

Note
MySQL 5.7 n’utilise pas l’algorithme de hachage. Néanmoins,
l’optimisation pour l’algorithme tri/groupe fonctionne comme décrit
ci-dessus.

Considérez la requête suivante. Elle renvoie les revenus d’hier groupés par
ID_PRODUIT :

SELECT id_produit, sum(valeur_eur)


FROM ventes
WHERE date_vente = TRUNC(sysdate) - INTERVAL '1' DAY
GROUP BY id_produit;

En connaissant l’index sur DATE_VENTE et ID_PRODUIT de la section précé-


dente, l’algorithme de tri/groupe est plus approprié car un INDEX RANGE SCAN
renvoie automatiquement les lignes dans l’ordre requis. Ceci signifie que
la base de données évite la matérialisation car elle n’a pas besoin d’une
opération explicite de tri. Le group by est exécuté en pipeline.

------------------------------------------------------------------
|Id |Operation | Name | Rows | Cost |
------------------------------------------------------------------
| 0 |SELECT STATEMENT | | 17 | 192 |
| 1 | SORT GROUP BY NOSORT | | 17 | 192 |
| 2 | TABLE ACCESS BY INDEX ROWID| VENTES | 321 | 192 |
|*3 | INDEX RANGE SCAN | DATE_VENTES_PR | 321 | 3 |
------------------------------------------------------------------

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 139


chapitre 6 : Trier et grouper

Le plan d’exécution de la base de données Oracle marque une opération


SORT GROUP BY en pipeline avec le mot-clé NOSORT. Le plan d’exécution des
autres bases de données ne mentionne pas du tout les opérations de tri.

Le group by en pipeline a les mêmes prérequis que le order by en pipeline,


sauf qu’il n’y a aucun modificateur ASC et DESC. Cela signifie que définir un
index avec les modificateurs ASC/DESC ne devrait pas affecter l’exécution en
pipeline d’un group by. C’est aussi vrai pour NULLS FIRST/LAST. Cependant,
certaines bases de données ne peuvent pas utiliser correctement un index
ASC/DESC pour un group by exécuté en pipeline.

Avertissement
Pour PostgreSQL, vous devez ajouter une clause order by pour qu’un
index avec un tri NULLS LAST puisse être utilisé avec un group by dans
un pipeline.
La base de données Oracle ne peut pas lire un index à l’envers pour
exécuter un group by dans un pipeline s’il est suivi par un order by.

Si nous étendons la requête pour qu’elle considère toutes les ventes


depuis hier, comme nous l’avions fait dans l’exemple sur l’order by en
pipeline, cela empêche le group by en pipeline pour la même raison que
précédemment : le INDEX RANGE SCAN ne fournit pas les lignes triées par la
clé de regroupement (comparez à la Figure 6.1 à la page 131).

SELECT id_produit, sum(valeur_eur)


FROM ventes
WHERE date_vente >= TRUNC(sysdate) - INTERVAL '1' DAY
GROUP BY id_produit;
-----------------------------------------------------------------
|Id |Operation | Name | Rows | Cost |
-----------------------------------------------------------------
| 0 |SELECT STATEMENT | | 24 | 356 |
| 1 | HASH GROUP BY | | 24 | 356 |
| 2 | TABLE ACCESS BY INDEX ROWID| VENTES | 596 | 355 |
|*3 | INDEX RANGE SCAN | DATE_VENTE_PR | 596 | 4 |
-----------------------------------------------------------------

140 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Indexer le « Group By »

À la place, la base de données Oracle utilise l’algorithme de hachage.


L’avantage de cet algorithme est qu’il est seulement nécessaire de mettre
en cache le résultat agrégé alors que l’algorithme de tri/groupe matérialise
l’ensemble complet en entrée. En d’autres termes, l’algorithme de hachage
a besoin de moins de mémoire.

Comme avec un order by exécuté en pipeline, une exécution rapide n’est


pas l’aspect le plus important d’une exécution group by en pipeline. Il est
plus important que la base de données l’exécute via un pipeline et renvoie
les premiers résultats avant de lire l’entrée en entier. C’est la condition pour
les méthodes d’optimisation avancées expliquées dans le prochain chapitre.

Réflexion
Pouvez-vous trouver d’autres opérations des bases de données (en
dehors du tri et des groupes) pouvant utiliser un index pour éviter
un tri ?

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 141


142 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>
Chapitre 7

Résultats partiels
Quelquefois, vous n’avez pas besoin de toutes les lignes d’une requête
SQL. Seules les premières lignes vous intéressent, par exemple les dix
premières. Dans ce cas, il est fréquent de permettre aux utilisateurs de
naviguer parmi les anciens messages, soit en utilisant de la pagination
traditionnelle, soit en utilisant une variante plus moderne de parcours
infini. Néanmoins, les requêtes SQL utilisées pour cette fonction peuvent
causer de sérieux problèmes de performances si tous les messages doivent
être triés pour trouver les plus récents. Du coup, un order by en pipeline
est une optimisation très puissante pour ce type de requêtes.

Ce chapitre démontre comment utiliser un order by en pipeline dans le


but de récupérer efficacement des résultats partiels. Bien que la syntaxe de
ces requêtes varie suivant les bases de données, ces dernières exécutent les
requêtes d’une façon très similaire. Cela démontre une fois de plus qu’elle
ne font pas de miracles.

Récupérer les N premières lignes


Les requêtes Top-N sont des requêtes qui limitent le résultat à un nombre
spécifique de lignes. Ce sont souvent des requêtes permettant de récupérer
les enregistrements les plus récents ou les plus intéressants sur un
ensemble de résultats. Pour une exécution efficace, le tri doit se faire avec
un order by en pipeline.

La façon la plus simple de récupérer les premières lignes d’une requête


est de récupérer les lignes intéressantes puis de terminer la requête.
Malheureusement, l’optimiseur ne peut pas le deviner en préparant le plan
d’exécution. Pour trouver le meilleur plan d’exécution, l’optimiseur doit
savoir si l’application récupérera au final toutes les lignes. Dans ce cas, un
parcours complet de table avec une opération explicite de tri pourrait être
plus performante, alors qu’un order by serait meilleur si on ne souhaite
récupérer que les dix premières lignes, même si la base de données doit
récupérer les dix lignes une par une. En d’autres termes, l’optimiseur doit

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 143


chapitre 7 : Résultats partiels

savoir si vous allez annuler la requête avant d’avoir récupéré toutes les
lignes pour sélectionner le meilleur plan d’exécution.

Astuce
Informez la base de données quand vous n’avez pas besoin de toutes
les lignes.

Le standard SQL n’a pas pris en compte ce besoin pendant longtemps.


L’extension correspondante (fetch first) a été introduite à partir de
SQL:2008 et est actuellement seulement disponible avec IBM DB2,
PostgreSQL et SQL Server 2012. Ceci est dû dans un premier temps au fait
que cette fonctionnalité n’est pas une extension principale, mais aussi
parce que chaque base de données propose sa solution propriétaire depuis
de nombreuses années.

Les exemples suivants montrent l’utilisation de telles extensions en


récupérant les dix ventes les plus récentes. La base est toujours identique :
récupérer toutes les ventes, en commençant par la plus récente. La syntaxe
top-N respective annule simplement l’exécution après avoir récupéré dix
lignes.

MySQL
MySQL et PostgreSQL utilisent la clause limit pour restreindre le
nombre de lignes à récupérer.

SELECT *
FROM ventes
ORDER BY date_vente DESC
LIMIT 10;

Oracle Database
La base de données Oracle fournit la pseudo-colonne ROWNUM qui
numérote automatiquement les lignes dans l’ensemble de résultats.
Pour utiliser cette colonne dans un filtre, nous devons englober la
requête :

SELECT *
FROM (
SELECT *
FROM ventes
ORDER BY date_vente DESC
)
WHERE rownum <= 10;

144 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Récupérer les N premières lignes

PostgreSQL
PostgreSQL supporte l’extension fetch first depuis la version 8.4. La
clause limit précédemment utilisée fonctionne aussi comme indiqué
dans l’exemple pour MySQL.

SELECT *
FROM ventes
ORDER BY date_vente DESC
FETCH FIRST 10 ROWS ONLY;

SQL Server
SQL Server fournit la clause top pour restreindre le nombre de lignes
à récupérer.

SELECT TOP 10 *
FROM ventes
ORDER BY date_vente DESC;

À partir de la version 2012, SQL Server supporte aussi l’extension


fetch first.

Toutes les requêtes SQL affichées ci-dessus sont spéciales car les bases de
données les reconnaissent comme des requêtes top-N.

Important
La base de données peut optimiser une requête pour un résultat
partiel uniquement si elle le sait dès le départ.

Si l’optimiseur sait que nous n’avons besoin que des dix premières lignes,
il va préférer l’utilisation d’un order by en pipeline si c’est possible :

----------------------------------------------------------------
| Operation | Name | Rows | Cost |
----------------------------------------------------------------
| SELECT STATEMENT | | 10 | 9 |
| COUNT STOPKEY | | | |
| VIEW | | 10 | 9 |
| TABLE ACCESS BY INDEX ROWID| VENTES | 1004K| 9 |
| INDEX FULL SCAN DESCENDING| VENTES_DATE_PR | 10 | 3 |
----------------------------------------------------------------

Le plan d’exécution Oracle indique la fin planifiée avec l’opération


COUNT STOPKEY. La base de données a bien reconnu la requête top-N.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 145


chapitre 7 : Résultats partiels

Astuce
L’Annexe A, « Plans d’exécution » résume les opérations correspon-
dantes pour MySQL, Oracle, PostgreSQL et SQL Server.

Utiliser la bonne syntaxe est seulement la moitié du travail car terminer


efficacement l’exécution nécessite que les opérations précédentes soient
exécutées en pipeline. Autrement dit, la clause order by doit être exécutée
via un index, dans cet exemple l’index VENTE_DATE_PR sur VENTE_DATE et
ID_PRODUIT. En utilisant cet index, la base de données peut éviter une
opération explicite de tri et peut donc immédiatement envoyer les lignes
lues de l’index à l’application. L’exécution est annulée après avoir récupéré
dix lignes, donc la base de données ne lit pas plus de lignes que celles
sélectionnées.

Important
Une requête top-N en pipeline n’a pas besoin de lire et trier
l’ensemble complet des résultats.

Si aucun index n’est disponible sur VENTE_DATE pour exécuter un order by


en pipeline, la base de données doit lire et trier la table entière. La première
ligne n’est renvoyée qu’après avoir lu la dernière ligne de la table.

---------------------------------------------------
| Operation | Name | Rows | Cost |
---------------------------------------------------
| SELECT STATEMENT | | 10 | 59558 |
| COUNT STOPKEY | | | |
| VIEW | | 1004K| 59558 |
| SORT ORDER BY STOPKEY| | 1004K| 59558 |
| TABLE ACCESS FULL | VENTES | 1004K| 9246 |
---------------------------------------------------
Ce plan d’exécution n’a pas d’order by en pipeline et est aussi lent que
le client récupère toutes les lignes ou seulement les premières. Utiliser
la syntaxe top-N est toujours préférable car la base de données n’a pas
à matérialiser les résultats complets, seulement les dix premières lignes,
ce qui nécessite bien moins de mémoire. Le plan d’exécution Oracle
indique cette optimisation avec le modificateur STOPKEY sur l’opération
SORT ORDER BY.

Les avantages d’une requête top-N en pipeline incluent non seulement des
gains immédiats en performance mais aussi une scalabilité améliorée. Sans
utiliser une exécution en pipeline, le temps de réponse de cette requête top-

146 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Parcourir les résultats

N grossit avec la taille de la table. Le temps de réponse lors de l’utilisation


d’une exécution grossit seulement avec le nombre de lignes sélectionnées.
En d’autres termes, le temps de réponse d’une requête top-N en pipeline
est toujours le même. Il est totalement indépendant de la taille de la table.
Quand la profondeur du B-tree grossit, la requête peut cependant devenir
un peu plus lente.

La Figure 7.1 montre la scalabilité des deux variantes suivant un volume de


données grossissant. Le temps de réponse linéaire des deux variantes pour
une exécution sans order by en pipeline est clairement visible. Le temps de
réponse pour une exécution en pipeline reste constant.

Figure 7.1. Scalabilité des requêtes Top-N


matérialisé en flux
7 7
6 6
Temps de réponse [sec]

Temps de réponse [sec]


5 5
4 4
3 3
2 2
1 1
0 0
0 20 40 60 80 100
Volume de données

Bien que le temps de réponse d’une requête top-N en pipeline ne dépende


pas de la taille de la table, il grossit toujours avec le nombre de lignes
sélectionnées. Du coup, le temps de réponse double si vous sélectionnez
deux fois plus de lignes. Ceci est particulièrement significatif lors de la
pagination de requêtes qui chargent des résultats supplémentaires car ces
requêtes commencent souvent à la première entrée ; elles liront les lignes
déjà montrées sur la page précédente et les ignoreront jusqu’à atteindre les
résultats de la seconde page. Néanmoins, il existe aussi une solution à ce
problème, comme nous le verrons dans la section suivante.

Parcourir les résultats


Après avoir implémenté une requête top-N en pipeline pour récupérer
efficacement la première page, vous aurez aussi souvent besoin d’une autre
requête pour récupérer les pages suivantes. Le challenge qui en résulte est
que la requête doit ignorer les lignes des pages précédentes. Il existe deux
méthodes : tout d’abord la méthode de décalage, qui numérote les lignes
à partir du début et utilise un filtre sur le numéro de lignes pour ignorer

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 147


chapitre 7 : Résultats partiels

celles qui précèdent la page demandée. La seconde méthode, que j’appelle


la méthode de recherche recherche la dernière ligne de la page précédente
puis récupère les lignes suivantes.
Les exemples suivants montrent la méthode de décalage la plus
fréquemment utilisée. Son avantage principal est qu’elle est très simple à
gérer, tout spécialement avec les bases de données qui ont un mot clé dédié
pour ça (offset). Ce mot-clé a même été repris dans le standard SQL comme
faisant parti de l’extension fetch first.

MySQL
MySQL et PostgreSQL proposent la clause offset pour ignorer le
nombre spécifié de lignes en partant du début d’une requête top-N. La
clause limit est ensuite appliquée.

SELECT *
FROM ventes
ORDER BY date_vente DESC
LIMIT 10 OFFSET 10;

Oracle Database
La base de données Oracle fournit la pseudo-colonne ROWNUM qui
numérote automatiquement les lignes dans l’ensemble de résultats.
Néanmoins, il n’est pas possible d’appliquer un filtre supérieur-ou-
égal (>=) sur cette pseudo-colonne. Pour que cela fonctionne, vous
devez tout d’abord matérialiser les numéros de ligne en renommant la
colonne avec un alias.

SELECT *
FROM ( SELECT tmp.*, rownum rn
FROM ( SELECT *
FROM ventes
ORDER BY date_vente DESC
) tmp
WHERE rownum <= 20
)
WHERE rn > 10;

Notez l’utilisation de l’alias RN pour la limite basse et l’utilisation de la


pseudo-colonne ROWNUM directement pour la limite supérieure.

PostgreSQL
L’extension fetch first définit la clause offset ... rows. Néanmoins,
PostgreSQL accepte seulement offset sans le mot clé rows. La syntaxe
limit/offset précédemment utilisée fonctionne toujours comme
indiqué dans l’exemple MySQL.

148 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Parcourir les résultats

SELECT *
FROM ventes
ORDER BY date_vente DESC
OFFSET 10
FETCH NEXT 10 ROWS ONLY;

SQL Server
SQL Server n’a pas d’extension « offset » pour sa clause propriétaire
top mais a introduit l’extension fetch first avec SQL Server 2012. La
clause offset est obligatoire bien que le standard la définisse comme
optionnelle.

SELECT *
FROM ventes
ORDER BY date_vente DESC
OFFSET 10 ROWS
FETCH NEXT 10 ROWS ONLY;

En dehors de la simplicité, un autre avantage de cette méthode est que


vous avez juste besoin du décalage de la ligne pour récupérer une page
arbitraire. Néanmoins, la base de données doit compter toutes les lignes
à partir du début jusqu’à atteindre la page réclamée. La Figure 7.2 montre
que l’intervalle parcouru de l’index devient plus grand quand on récupère
plus de pages.

Figure 7.2. Accès utilisant la méthode du décalage


DATE_VENTE
Page 4

3 jours
auparavant
Page 3

2 jours
auparavant
Page 2

hier
Page 1

aujourd'hui

Résultat Décalage

Cela présente deux inconvénients : (1) les pages se décalent lors de


l’insertion de nouvelles ventes car la numérotation est toujours refaite à
chaque fois ; (2) le temps de réponse croît en allant plus loin.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 149


chapitre 7 : Résultats partiels

La méthode de recherche évite ces deux inconvénients car elle utilise


les valeurs de la page précédente comme délimiteur. Cela signifie qu’elle
recherche les valeurs qui doivent être avant le dernier enregistrement de la
page précédente. Cela se fait simplement avec une clause where. Autrement
dit, la méthode de recherche ne sélectionne pas les valeurs déjà affichées.

L’exemple suivant montre la méthode de recherche. Pour cette démons-


tration, nous commencerons avec une seule vente par jour. Cela fait de
DATE_VENTE une clé unique. Pour sélectionner les ventes qui viennent après
une date particulière, vous devez utiliser une condition plus-petit-que (<)
à cause de l’ordre de tri descendant. Pour un ordre ascendant, vous devez
utiliser la condition plus-grand-que (>). La clause fetch first est seulement
utilisée pour limiter le résultat à dix lignes.

SELECT *
FROM ventes
WHERE date_vente < ?
ORDER BY date_vente DESC
FETCH FIRST 10 ROWS ONLY;

Au lieu d’un numéro de ligne, vous utilisez la dernière valeur de la page


précédente pour indiquer la limite basse. L’énorme bénéfice consiste en
des performances très importantes car la base de données peut exécuter
la condition DATE_VENTE < ? en accédant à l’index. Cela signifie que la base
de données peut vraiment ignorer les lignes des pages précédentes. De
plus, vous obtiendrez aussi des résultats stables si de nouvelles lignes sont
insérées.

Néanmoins, cette méthode ne fonctionne pas s’il y a plus d’une vente


par jour, comme indiquée dans la Figure 7.2 parce qu’utiliser la dernière
date à partir de la dernière page (« hier ») ignore tous les résultats
d’hier, pas seulement ceux déjà montrés sur la première page. Le problème
vient de la clause order by qui n’établit pas une séquence déterminée de
lignes. Néanmoins, c’est un prérequis pour l’utilisation d’une condition
d’intervalle simple pour les changements de pages.

Sans clause order by déterministique, la base de données, par définition,


ne délivrera pas une séquence déterminée de lignes. La seule raison qui
fait que vous obtenez habituellement une séquence cohérente de lignes
est que la base de données exécute habituellement la requête de la même
façon. Néanmoins, la base de données pourrait en fait renvoyer les lignes
différemment en ayant une même DATE_VENTE tout en respectant la clause
order by. Dans les versions récentes, vous pouvez obtenir le résultat dans

150 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Parcourir les résultats

un ordre différent à chaque fois que vous exécutez la requête, non pas parce
que la base de données modifie intentionnellement l’ordre des lignes mais
parce que la base de données pourrait utiliser une exécution parallélisée de
la requête. Cela signifie que le même plan d’exécution peut aboutir à une
séquence de lignes différentes car les différents exécuteurs finissent leur
travail dans un ordre non déterminé.

Important
La pagination nécessite un ordre de tri déterministique.

Même si les spécifications fonctionnelles nécessitent uniquement un tri


« par date, les derniers en premier », en tant que développeurs, nous
voulons nous assurer que la clause order by ramène une séquence de
lignes déterminée. Pour cela, nous pouvons avoir besoin d’étendre la clause
order by avec des colonnes arbitraires. Si l’index utilisé pour l’order by en
pipeline a des colonnes supplémentaires, il est bon de les ajouter à la clause
order by pour continuer d’utiliser cet index pour un order by en pipeline.
Si cela ne permet toujours pas d’avoir un ordre de tri déterminé, il suffit
d’ajouter une (ou plusieurs) colonne unique et d’étendre l’index de la même
façon.

Dans l’exemple suivant, nous étendons la clause order by et l’index avec la


clé primaire, ID_VENTE, pour obtenir une séquence de lignes déterminée. De
plus, nous devons appliquer la logique « vient après » aux deux colonnes
ensemble pour obtenir le résultat désiré :

CREATE INDEX vt_dtid ON ventes (date_vente, id_vente);

SELECT *
FROM ventes
WHERE (date_vente, id_vente) < (?, ?)
ORDER BY date_vente DESC, id_vente DESC
FETCH FIRST 10 ROWS ONLY;

La clause where utilise la syntaxe peu connue appelée « valeurs de


lignes » (voir l’encadré « Valeurs de ligne en SQL »). Elle combine plusieurs
valeurs en une unité logique qui est applicable aux opérateurs standards
de comparaison. Comme valeurs scalaires, la condition plus-petit-que
correspond à « vient après » lors d’un tri en ordre descendant. Cela signifie
que la requête considère seulement les ventes qui viennent après la paire
DATE_VENTE, ID_VENTE donnée.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 151


chapitre 7 : Résultats partiels

Valeurs de ligne en SQL


En dehors des valeurs scalaires habituelles, le standard SQL définit
aussi ce qu’on appelle des constructeurs de valeur de ligne. Ils
« donnent un ensemble ordonné de valeurs à assembler en une
ligne complète ou partielle » [SQL:92, §7.1: <row value constructor>].
Syntaxiquement, les valeurs de ligne sont des listes entre crochets.
Cette syntaxe est plus connue pour son utilisation dans l’instruction
insert.
Néanmoins, utiliser des constructeurs de valeurs de ligne dans la
clause where est bien moins connu et tout aussi valide. En fait, le
standard SQL définit tous les opérateurs de comparaison pour des
constructeurs de valeurs de ligne. Par exemple, voici la définition
pour l’opération plus-petit-que :

"Rx < Ry" est vrai si et seulement si RXi = RYi pour tout i < n
et RXn < RYn pour certains n.
—SQL:92, §8.2.7.2

où i et n reflètent les positions dans les listes. Cela signifie qu’une


valeur de ligne RX est plus petite que RY si chaque valeur RXn est plus
petite que la valeur correspondante RYn et que toutes les paires de
valeurs correspondantes sont égales (RXi = RYi; pour i<n).
Cette définition fait que l’expression RX < RY est synonyme de « RX
est trié avant RY », ce qui est exactement la logique dont nous avons
besoin pour la méthode de recherche.

Même si la syntaxe des valeurs de ligne fait partie du standard SQL, seules
quelques bases de données la proposent. SQL Server 2017 n’accepte pas
les valeurs de lignes. La base de données Oracle accepte les valeurs de
ligne en principe mais ne peut pas leur appliquer les opérateurs d’intervalle
(ORA-01796). MySQL évalue correctement les expressions de valeurs de ligne
mais ne peut pas les utiliser en prédicat d’accès lors de l’accès à un index.
Néanmoins, PostgreSQL accepte la syntaxe de valeur de ligne et les utilise
pour accéder à l’index si un index correspondant existe.

Néanmoins, il est possible d’utiliser une variante approximative de la


méthode de recherche avec des bases de données qui ne supportent pas
correctement les valeurs de ligne, même si l’approximation n’est pas aussi
élégante et efficace que les valeurs de lignes dans PostgreSQL. Pour cette

152 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Parcourir les résultats

approximation, nous devons utiliser des comparaisons « standards » pour


exprimer la logique demandée comme dans cet exemple pour Oracle :

SELECT *
FROM ( SELECT *
FROM ventes
WHERE date_vente <= ?
AND NOT (date_vente = ? AND id_vente >= ?)
ORDER BY date_vente DESC, id_vente DESC
)
WHERE rownum <= 10;

La clause where consiste en deux parties. La première considère seulement


DATE_VENTE et utilise une condition plus-petit-que-ou-égal-à (<=). Elle
sélectionne plus de lignes que nécessaire. Cette partie de la clause
where est suffisamment simple pour que toutes les bases de données
puissent l’utiliser en accédant à l’index. La deuxième partie de la clause
where supprime les lignes en trop qui ont déjà été montrées sur la
page précédente. L’encadré « Indexer une logique équivalente » explique
pourquoi la clause where est exprimée de cette façon.

Le plan d’exécution montre que la base de données utilise la première partie


de la clause where comme prédicat d’accès.

---------------------------------------------------------------
|Id | Operation | Name | Rows | Cost |
---------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 4 |
|*1 | COUNT STOPKEY | | | |
| 2 | VIEW | | 10 | 4 |
| 3 | TABLE ACCESS BY INDEX ROWID | VENTES | 50218 | 4 |
|*4 | INDEX RANGE SCAN DESCENDING| VT_DTIT | 2 | 3 |
---------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter(ROWNUM<=10)
4 - access("DATE_VENTE"<=:DATE_VENTE)
filter("DATE_VENTE"<>:DATE_VENTE
OR "ID_VENTE"<TO_NUMBER(:ID_VENTE))

Les prédicats d’accès sur DATE_VENTE active la base de données pour ignorer
les jours qui étaient affichés sur les pages précédentes. La deuxième partie
de la clause where est seulement un prédicat d’accès. Cela signifie que la
base de données inspecte de nouveau quelques lignes à partir de la page
précédente, mais les supprime immédiatement. La Figure 7.3 montre le
chemin d’accès respectif.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 153


chapitre 7 : Résultats partiels

Figure 7.3. Accès utilisant la méthode de recherche

DATE_VENTE ID_VENTE

Page 4
3 jours
auparavant

Page 3
2 jours
auparavant

Page 2
hier

Page 1
aujourd'hui

Résultat Décalage

La Figure 7.4 compare les caractéristiques des performances des méthodes


de décalage et de recherche. La précision de la mesure n’est pas suffisante
pour voir la différence sur le côté gauche du graphe. Par contre, la
différence est évidente à partir de la page 20.

Figure 7.4. Scalabilité lors de la récupération de la prochaine page


Décalage Recherche
1.2 1.2
Temps de réponse [sec]

Temps de réponse [sec]

1 1

0.8 0.8

0.6 0.6

0.4 0.4

0.2 0.2

0 0
0 20 40 60 80 100
Page

Bien sûr, la méthode de recherche a aussi des inconvénients, sa gestion


difficile étant la principale. Non seulement vous devez être très attentif
lors de l’écriture de la clause where, vous ne pouvez pas en plus récupérer
des pages arbitraires. Mais, en plus, vous avez besoin d’inverser toutes
les opérations de comparaison et de tri pour modifier la direction de la
navigation. Plus précisément, ces deux fonctions, pour passer des pages et
naviguer en sens inverse, ne sont pas nécessaires lors de l’utilisation du
mécanisme de parcours infini pour l’interface utilisateur.

154 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Parcourir les résultats

Indexer une logique équivalente


Une condition logique peut toujours être exprimée de plusieurs
façons. Par exemple, vous pouvez aussi implémenter la logique
montrée ci-dessus ainsi :

WHERE (
(date_vente < ?)
OR
(date_vente = ? AND id_vente < ?)
)

Cette variante utilise seulement les conditions incluantes et est


certainement plus facile à comprendre, tout du moins pour les êtres
humains. Les bases de données ont un point de vue différent. Elles
ne comprennent pas que la clause where sélectionne toutes les lignes
commençant avec la paire DATE_VENTE/ID_VENTE, en supposant que
DATE_VENTE est la même pour les deux branches. À la place, la base
de données utilise la clause where complète comme prédicat de filtre.
Nous pouvons au moins nous attendre à ce que l’optimiseur factorise
la condition DATE_VENTE <= ? des deux branches OU, mais aucune des
bases de données ne le fait.
Néanmoins, nous pouvons ajouter manuellement la condition
redondante, même si cela n’améliore pas la lisibilité :

WHERE date_vente <= ?


AND (
(date_vente < ?)
OR
(date_vente = ? AND id_vente < ?)
)

Heureusement, toutes les bases de données peuvent utiliser cette


partie de la clause where comme prédicat d’accès. Cependant, cette
clause est plus dure à appréhender que la logique approximative
montrée ci-dessus. De plus, la logique originale évite le risque
que toute partie non nécessaire (redondante) soit supprimée
accidentellement de la clause where.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 155


chapitre 7 : Résultats partiels

Utiliser les fonctions de fenêtrage


pour une pagination
Les fonctions de fenêtrage offrent un autre moyen d’implémenter la
pagination en SQL. C’est une méthode flexible et, par-dessus tout, c’est une
méthode proposée par le standard. Néanmoins, seules les bases de données
SQL Server et Oracle peuvent les utiliser pour une requête top-N en pipeline.
1
MySQL , DB2 et PostgreSQL n’annulent pas le parcours d’index après avoir
récupéré les lignes nécessaires, ce qui implique qu’elle ne peut donc pas les
exécuter de façon efficace.

L’exemple suivant utilise la fonction de fenêtrage ROW_NUMBER pour une


requête de pagination :

SELECT *
FROM ( SELECT ventes.*
, ROW_NUMBER() OVER (ORDER BY date_vente DESC
, id_vente DESC) rn
FROM ventes
) tmp
WHERE rn between 11 and 20
ORDER BY date_vente DESC, id_vente DESC;

La fonction ROW_NUMBER énumère les lignes suivant l’ordre de tri défini


dans la clause over. La clause where externe utilise cette énumération pour
limiter les résultats à la seconde page (lignes 11 à 20).

La base de données Oracle reconnaît la condition d’annulation et utilise


l’index sur DATE_VENTE et ID_VENTE pour produire un comportement du type
requête top-N en pipeline :

MySQL accepte les fonctions de fenêtrage depuis la version 8.0.

156 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Utiliser les fonctions de fenêtrage pour une pagination

---------------------------------------------------------------
|Id | Operation | Name | Rows | Cost |
---------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1004K| 36877 |
|*1 | VIEW | | 1004K| 36877 |
|*2 | WINDOW NOSORT STOPKEY | | 1004K| 36877 |
| 3 | TABLE ACCESS BY INDEX ROWID | VENTES | 1004K| 36877 |
| 4 | INDEX FULL SCAN DESCENDING | VT_DTID | 1004K| 2955 |
---------------------------------------------------------------

Predicate Information (identified by operation id):


---------------------------------------------------
1 - filter("RN">=11 AND "RN"<=20)
2 - filter(ROW_NUMBER() OVER (
ORDER BY "DATE_VENTE" DESC, "ID_VENTE" DESC ) <=20)

L’opération WINDOW NOSORT STOPKEY indique qu’il n’y a pas d’opération de


tri (NOSORT) et que la base de données annule l’exécution une fois qu’elle
a atteint la limite supérieure (STOPKEY). En considérant que les opérations
annulées ont été exécutées sous la forme d’un pipeline, cela signifie que
cette requête est aussi efficace que la méthode de décalage expliquée dans
la section précédente.

Néanmoins, la force des fonctions de fenêtrage n’est pas la pagination


mais les calculs analytiques. Si vous n’avez jamais utilisé de fonctions de
fenêtrage auparavant, vous devriez passer quelques heures à étudier la
documentation relative.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 157


158 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>
Chapitre 8

Modifier les données

Pour l’instant, nous avons seulement discuté des performances des


requêtes d’extraction mais le SQL ne porte pas uniquement sur l’extraction
de données. Il supporte aussi la manipulation de données. Les commandes
respectives, insert, delete et update, forment le langage de manipulation
des données (DML), qui est une section du standard SQL. Les performances
de ces commandes sont généralement mauvaises à cause des index.

Un index est de la redondance pure. Il contient seulement des données


qui sont aussi stockées dans la table. Lors des opérations d’écriture, la
base de données doit conserver une cohérence dans cette redondance. Plus
exactement, les commandes insert, delete et update n’affectent pas que la
table mais aussi les index qui détiennent une copie des données concernées.

Insertion
Le nombre d’index sur une table est le facteur le plus important pour les
performances d’un insert. Plus une table a d’index, plus l’exécution sera
lente. L’instruction insert est la seule opération qui ne peut pas bénéficier
directement de l’indexation car elle n’a pas de clause where.

Ajouter une nouvelle ligne dans une table implique de suivre plusieurs
étapes. Tout d’abord, la base de données doit trouver une place pour
enregistrer la ligne. Pour une table standard, sans ordre particulier pour les
lignes, la base de données peut utiliser tout bloc de la table qui dispose de
suffisamment d’espace libre. C’est un traitement très simple et très rapide,
généralement exécuté en mémoire. Tout ce qui reste à faire est d’ajouter
la nouvelle entrée dans le bloc de données respectif.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 159


chapitre 8 : Modifier les données

Si la table dispose d’index, la base de données doit s’assurer que la nouvelle


entrée se trouve dans les index. Pour cela, il doit ajouter la nouvelle entrée
dans chaque index de la table. Le nombre d’index est donc un multiplicateur
du coût pour une requête insert.

De plus, l’ajout d’une entrée dans un index est bien plus coûteux que son
ajout dans une structure de table car la base de données doit conserver
l’ordre de l’index et la propriété balancée de l’arbre. La nouvelle entrée
ne peut donc pas être écrite dans n’importe quel bloc. Elle appartient à
un nœud feuille spécifique. Bien que la base de données utilise l’arbre de
l’index pour trouver le bon nœud feuille, elle doit toujours lire quelques
blocs d’index pour parcourir l’arbre.

Une fois que le nœud feuille valide a été identifié, la base de données vérifie
s’il reste suffisamment de place libre dans ce nœud. Si ce n’est pas le cas, la
base de données doit diviser en deux le nœud feuille et distribuer les entrées
entre l’ancien nœud et le nouveau nœud. Ce traitement affecte aussi la
référence dans le nœud branche correspondant. Il se peut aussi que le nœud
branche n’ait plus d’espace, auquel cas il devra être divisé en deux. Dans le
pire des cas, la base de données doit diviser tous les nœuds jusqu’au nœud
racine. Dans ce seul cas, l’arbre dispose d’une couche supplémentaire,
autrement dit sa profondeur grossit.

Il découle de tout cela que la maintenance de l’index est la partie la plus


coûteuse de l’instruction insert. C’est aussi visible dans la Figure 8.1,
« Performance des insertions suivant le nombre d’index » : le temps
d’exécution est difficilement visible si la table n’a pas d’index mais ajouter
un seul index est suffisant pour multiplier le temps d’exécution par 100. Et
chaque index supplémentaire ralentit encore plus l’exécution.

Figure 8.1. Performance des insertions suivant le nombre d’index

0.10 0.10
Durée d'exécution [sec]

Durée d'exécution [sec]

0.08 0.08

0.06 0.06

0.04 0.04
0.0003s

0.02 0.02

0.00 0.00
0 1 2 3 4 5
Index

160 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Insertion

Note
La plus grosse différence est observée dès le premier index.

Pour optimiser les performances des insert, il est tout particulièrement


important d’avoir un nombre d’index le plus petit possible.

Astuce
Utilisez les index délibérément et uniquement s’ils sont nécessaires.
Évitez les index redondants autant que possible. Ce sera aussi
bénéfique pour les requêtes delete et update.

En ne considérant que les requêtes insert, il serait préférable d’éviter


complètement les index. C’est le seul moyen pour obtenir les meilleures
performances pour un insert. Néanmoins, des tables sans index sont
inimaginables pour les applications. Vous voulez généralement récupérer
les données stockées dans la base, donc vous avez besoin d’index pour
améliorer les performances des requêtes d’extraction. Même les tables en
écriture seule ont souvent une clé primaire, et du coup un index.

Néanmoins, la performance sans les index est si bonne qu’il peut


être intéressant de supprimer temporairement tous les index lors du
chargement d’une grosse quantité de données, à condition que les index ne
soient pas nécessaires pour d’autres requêtes SQL au même moment. Cela
peut donner une accélération très importante sur la vitesse d’insertion,
à tel point que c’est devenu une pratique courante pour les entrepôts de
données.

Réflexion
Quel changement verrait-on sur la Figure 8.1 lors de l’utilisation d’une
table organisée en index ou d’un index regroupé ?
Y a-t-il un moyen indirect pour qu’un insert puisse bénéficier d’un
index ? autrement dit, comment un index supplémentaire pourrait
améliorer les performances d’une requête insert ?

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 161


chapitre 8 : Modifier les données

Suppression
Contrairement à la requête insert, la requête delete a une clause where qui
peut utiliser toutes les méthodes décrites dans le Chapitre 2, « La clause
Where » pour bénéficier directement des index. En fait, la requête delete
fonctionne comme une requête select suivie d’une étape supplémentaire
de suppression des lignes identifiées.

La suppression réelle d’une ligne suit un traitement similaire à l’insertion


d’une ligne, tout spécialement la suppression des références enregistrées
dans les index et la conservation de la propriété balancée des index. Le
graphe de performances montré en Figure 8.2 est de ce fait très similaire
à celui montré pour l’insert.

Figure 8.2. Performance des suppressions suivant le nombre d’index

0.12 0.12
Durée d'exécution [sec]

Durée d'exécution [sec]

0.10 0.10

0.08 0.08

0.06 0.06

0.04 0.04

0.02 0.02

0.00 0.00
1 2 3 4 5
Index

En théorie, nous devons nous attendre à avoir les meilleures performances


pour un delete sur une table sans index, exactement comme pour les
insert. Néanmoins, sans index, la base de données doit lire la table entière
pour trouver les lignes à supprimer. Cela signifie que la suppression d’une
ligne est rapide, mais la trouver est bien plus long. Ce cas n’est pas montré
dans la Figure 8.2.

Il peut être sensé d’exécuter une requête delete sans index tout comme il
peut être sensé d’exécuter une requête select sans index si elle renvoie une
grande partie de la table.

Astuce
Même les requêtes delete et update ont un plan d’exécution.

162 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Mise à jour

Une requête delete sans clause where est un exemple évident pour lequel la
base de données ne peut pas utiliser un index. Cependant, c’est un cas très
particulier qui a même sa propre commande SQL : truncate table. Cette
commande a le même effet que delete sans la clause where, sauf qu’elle
supprime toutes les lignes en un coup. Cette commande est très rapide mais
elle a deux effets de bord importants : (1) elle exécute un commit implicite
(avec une exception pour PostgreSQL) ; (2) elle n’exécute aucun trigger.

Effets de bord de MVCC


Le système MVCC (pour « Multiversion concurrency control ») est
un mécanisme de base de données qui permet un accès aux données
en parallèle non bloquant et une vue cohérente de la base. Les
implémentations diffèrent suivant la base de données et peuvent
avoir des effets considérables sur les performances.
Par exemple, la base de données PostgreSQL conserve les
informations de version (aussi appelées informations de visibilité) au
niveau de la table : supprimer une ligne ne fait que mettre à 1 le
drapeau « supprimée » dans le bloc de la table. Les performances
d’une requête delete avec PostgreSQL ne dépendent donc pas du
nombre d’index sur la table. La suppression physique de la ligne dans
la table et la maintenance relative de l’index sont réalisées lors du
VACUUM .

Mise à jour
Une requête update doit déplacer les entrées d’index modifiées pour
maintenir l’ordre de l’index. Pour cela, la base de données doit supprimer
l’ancienne entrée et ajouter la nouvelle entrée à la nouvelle position. Le
temps de réponse est en gros le même qu’une requête delete suivie d’une
requête insert.

Les performances d’un update, tout comme un insert et un delete,


dépendent aussi du nombre d’index sur la table. La seule différence est
que les requêtes update n’affectent pas forcément toutes les colonnes.
Généralement, elles ne modifient que quelques colonnes sélectionnées.
En conséquence, une requête update n’affecte pas nécessairement tous
les index de la table mais seulement ceux qui contiennent les colonnes
modifiées.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 163


chapitre 8 : Modifier les données

La Figure 8.3 montre le temps de réponse pour deux requêtes update : une
qui modifie toutes les colonnes (et donc a un impact sur chaque index) et
une qui met à jour une seule colonne (et du coup a un impact sur un seul
index).

Figure 8.3. Performances de la mise à jour sur les index et le nombre de


colonnes

toutes les colonnes


0.20 0.20
Durée d'exécution [sec]

Durée d'exécution [sec]


une colonne

0.15 0.15

0.10 0.10

0.05 0.05

0.00 0.00
1 2 3 4 5
Nombre d'index

La requête update sur toutes les colonnes montre le même motif que celui
déjà observé dans les sections précédentes : le temps de réponse grossit
avec chaque nouvel index. Le temps de réponse de la requête update qui
ne touche qu’un seul index n’augmente pas tellement car la majorité des
index n’est pas à modifier.

Pour optimiser les performances d’un update, vous devez faire attention à
ne modifier que les colonnes nécessaires. Cela paraît évident si vous écrivez
vous-même la requête update. Néanmoins, les outils ORM peuvent générer
des requêtes update qui modifient toutes les colonnes à chaque fois. Par
exemple, Hibernate le fait si le mode dynamic-update est désactivé. Depuis
la version 4.0, ce mode est activé par défaut.

Lors de l’utilisation d’outils ORM, une bonne pratique est d’activer de temps
en temps la trace des requêtes dans un environnement de développement
pour vérifier les requêtes SQL produites. L’encadré « Activer la trace des
requêtes SQL » à la page 95 donne un court aperçu sur l’activation des
traces des requêtes sur certains outils ORM très utilisés.

Réflexion
Pouvez-vous trouver un exemple où des requêtes insert ou delete
n’affectent pas tous les index d’une table ?

164 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Annexe A

Plans d’exécution
Avant que la base de données ne puisse exécuter une requête SQL,
l’optimiseur doit créer un plan d’exécution. Puis la base de données exécute
ce plan étape par étape. De ce fait, l’optimiseur a un fonctionnement très
similaire à un compilateur car il traduit le code source (la requête SQL) en
un programme exécutable (le plan d’exécution).

Le plan d’exécution est la première chose à regarder lorsqu’on recherche


la cause de requêtes lentes. Les sections suivantes expliquent comment
récupérer et lire un plan d’exécution pour optimiser les performances, sur
différentes bases de données.

Table des matières


Oracle Database ............................................................................. 166
Obtenir un plan d’exécution ...................................................... 166
Opérations ............................................................................... 167
Distinguer les prédicats d’accès et de filtre ................................ 170
PostgreSQL ..................................................................................... 172
Obtenir un plan d’exécution ...................................................... 172
Opérations ............................................................................... 174
Distinguer les prédicats d’accès et de filtre ................................ 177
SQL Server ..................................................................................... 180
Obtenir un plan d’exécution ...................................................... 180
Opérations ............................................................................... 182
Distinguer les prédicats d’accès et de filtre ................................ 185
MySQL ........................................................................................... 188
Obtenir un plan d’exécution ...................................................... 188
Opérations ............................................................................... 188
Distinguer les prédicats d’accès et de filtre ................................ 190

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 165


annexe A : Plans d’exécution

Oracle Database
La plupart des environnements de développement (IDE) peuvent très
facilement afficher un plan d’exécution mais ils utilisent différentes façons
pour les formater à l’écran. La méthode décrite dans cette section montre le
plan d’exécution comme il est déjà apparu dans le reste du livre. Il nécessite
seulement Oracle en version 9iR2 ou en une version plus récente.

Obtenir un plan d’exécution


Visualiser un plan d’exécution avec Oracle nécessite deux étapes :

1. Sauvegarder le plan d’exécution dans PLAN_TABLE avec explain plan for

2. Formater et afficher le plan d’exécution.

Créer et sauvegarder un plan d’exécution


Pour créer un plan d’exécution, vous devez avoir préfixé la requête SQL
respective avec explain plan for :

EXPLAIN PLAN FOR select * from dual;

Vous pouvez exécuter la commande explain plan for dans n’importe


quel environnement de développement ou dans SQL*Plus. Néanmoins,
cela n’affiche pas le plan. Cela le sauvegarde dans une table nommée
PLAN_TABLE. À partir de la version 10g, cette table est automatiquement
disponible en tant que table temporaire globale. Avec les versions
précédentes, vous devez la créer dans chaque schéma où sa présence est
nécessaire. Demandez à votre administrateur de base de données de la
créer pour vous ou de vous fournir la requête create table provenant de
l’installation d’Oracle :

$ORACLE_HOME/rdbms/admin/utlxplan.sql

Vous pouvez exécuter la requête dans n’importe quel schéma pour créer la
table PLAN_TABLE dans ce schéma.

Avertissement
La commande explain plan for pour la commande ne crée pas
nécessairement le même plan d’exécution que s’il exécutait la
requête.

166 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Oracle : Opérations

Afficher le plan d’exécution


Le paquet DBMS_XPLAN a été introduit avec la version 9iR2. Il permet
de formater et d’afficher les plans d’exécution stockés dans PLAN_TABLE.
L’exemple suivant montre comment afficher le dernier plan d’exécution qui
a été créé dans la session courante de la base de données :

select * from table(dbms_xplan.display);

Encore une fois, si cette requête ne fonctionne pas pour vous, demandez
l’aide de votre administrateur de bases de données.

La requête affichera le plan d’exécution comme montré dans ce livre :

--------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|.
--------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 2 | 2 (0)|.
| 1 | TABLE ACCESS FULL| DUAL | 1 | 2 | 2 (0)|.
--------------------------------------------------------------

Certaines des colonnes affichées dans le plan d’exécution ont été


supprimées pour que l’affichage soit facilement lisible sur la page.

Opérations

Accès aux tables et aux index


INDEX UNIQUE SCAN
L’opération INDEX UNIQUE SCAN réalise un parcours seul du B-tree. La
base de données utilise cette opération si une contrainte unique assure
que le critère de recherche correspondra à pas plus d’une entrée. Voir
aussi le Chapitre 1, « Anatomie d’un index ».

INDEX RANGE SCAN


L’opération INDEX RANGE SCAN réalise un parcours du B-tree et suit la
chaîne de nœuds feuilles pour trouver toutes les entrées correspon-
dantes. Voir aussi le Chapitre 1, « Anatomie d’un index ».

Les prédicats de filtre de l’index causent souvent des problèmes de


performance pour un INDEX RANGE SCAN. La section suivante explique
comment les identifier.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 167


annexe A : Plans d’exécution

INDEX FULL SCAN


Lit l’index entier (toutes les lignes) dans l’ordre de l’index. Suivant
différentes statistiques système, la base de données peut choisir de
réaliser cette opération car elle a besoin de toutes les lignes dans
l’ordre de l’index, par exemple à cause d’une clause order by. À la
place, l’optimiseur pourrait aussi utiliser un INDEX FAST FULL SCAN et
réaliser une opération de tri supplémentaire. Voir le Chapitre 6, « Trier
et grouper ».

INDEX FAST FULL SCAN


Lit l’index en entier (toutes les lignes) comme stocké sur disque. Cette
opération est typiquement exécutée à la place d’un parcours complet
de table si toutes les colonnes requises sont disponibles dans l’index.
Comme le TABLE ACCESS FULL, l’opération INDEX FAST FULL SCAN peut
bénéficier d’opérations de lecture multi-blocs. Voir le Chapitre 5,
« Regrouper les données ».

TABLE ACCESS BY INDEX ROWID


Récupère une ligne à partir de la table en utilisant le ROWID récupéré
lors d’une recherche précédente dans l’index. Voir aussi le Chapitre 1,
« Anatomie d’un index ».

TABLE ACCESS FULL


Cette opération est aussi connue sous le nom de parcours complet de
table. Elle lit la table entière, toutes les lignes et toutes les colonnes,
comme elle est stockée sur le disque. Bien que les opérations multi-
blocs accélèrent fortement la rapidité d’un parcours complet de table,
c’est l’une des opérations les plus coûteuses. En dehors de gros taux
d’entrées/sorties, un parcours complet de table doit inspecter toutes
les lignes de la table, donc il peut aussi consommer beaucoup de temps
processeur. Voir aussi le « Parcours complet de table » à la page 13.

Jointures
Généralement, les opérations de jointure traitent deux tables à la fois. Au
cas où la requête a plus de jointures, elles sont exécutées séquentiellement :
tout d’abord deux tables, puis le résultat intermédiaire et la table suivante.
Dans le contexte des jointures, le terme « table » peut aussi signifier
« résultat intermédiaire ».

NESTED LOOPS JOIN


Joint deux tables en récupérant le résultat d’une table et en recherchant
la condition de jointure dans l’autre table ligne par ligne. Voir aussi
« Boucles imbriquées » à la page 92.

168 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Oracle : Opérations

HASH JOIN
La jointure de hachage charge les enregistrements candidats d’un côté
de la jointure dans une table de hachage qui est ensuite sondée pour
chaque ligne de l’autre côté de la jointure. Voir aussi « Jointure par
hachage » à la page 101.

MERGE JOIN
La jointure d’assemblage combine deux listes triées comme une
fermeture Éclair. Les deux côtés de la jointure doivent être pré-triés.
Voir aussi « Fusion par tri » à la page 109.

Tri et regroupement

SORT ORDER BY
Trie le résultat suivant la clause order by. Cette opération a besoin
de grandes quantités de mémoire pour matérialiser le résultat
intermédiaire (sans pipeline). Voir aussi « Indexer un tri » à la page 130.

SORT ORDER BY STOPKEY


Trie un sous-ensemble du résultat suivant la clause order by. Utilisé
pour les requêtes top-N si une exécution en pipeline n’est pas possible.
Voir aussi « Récupérer les N premières lignes » à la page 143.

SORT GROUP BY
Trie l’ensemble de résultat sur les colonnes de la clause group by et
agrège le résultat trié dans une deuxième étape. Cette opération a
besoin de grandes quantités de mémoire pour matérialiser le résultat
intermédiaire (sans pipeline). Voir aussi « Indexer le Group By » à la
page 139.

SORT GROUP BY NOSORT


Agrège un ensemble pré-trié suivant la clause group by. Cette opération
ne place pas le résultat intermédiaire dans un tampon : il est exécuté
dans un pipeline. Voir aussi « Indexer le Group By » à la page 139.

HASH GROUP BY
Groupe les résultats suivant une table de hachage. Cette opération a
besoin de grandes quantités de mémoire pour matérialiser le résultat
intermédiaire (sans pipeline). La sortie n’est pas ordonnée de quelle
que façon que ce soit. Voir aussi « Indexer le Group By » à la page 139.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 169


annexe A : Plans d’exécution

Requêtes Top-N

L’efficacité des requêtes top-N dépend du mode d’exécution des opérations


sous-jacentes. Elles sont très inefficaces lors de l’arrêt d’opérations sans
pipeline, comme SORT ORDER BY.

COUNT STOPKEY
Arrête les opérations sous-jacentes quand le nombre de lignes souhaité
a été atteint. Voir aussi la section intitulée « Récupérer les N premières
lignes ».

WINDOW NOSORT STOPKEY


Utilise une fonction de fenêtrage (clause over) pour arrêter l’exécution
quand le nombre de lignes a été récupéré. Voir aussi « Utiliser les
fonctions de fenêtrage pour une pagination » à la page 156.

Distinguer les prédicats d’accès et de filtre


La base de données Oracle utilise trois méthodes différentes pour appliquer
les clauses where (prédicats) :

Prédicats d’accès (« access »)


Les prédicats d’accès expriment les conditions de début et de fin des
parcours de nœuds feuilles.

Prédicat de filtre d’index (« filter » pour les opérations sur les index)
Les prédicats de filtres d’index sont appliqués seulement lors du
parcours de nœuds feuilles. Ils ne contribuent pas aux conditions de
début et d’arrêt, et ne diminuent pas l’intervalle parcouru.

Prédicat de filtre au niveau table


(« filter » pour les opérations sur la table)
Les prédicats sur les colonnes qui ne font pas partie de l’index sont
évalués au niveau de la table. Pour cela, la base de données doit tout
d’abord charger la ligne à partir de la table.

170 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Oracle : Distinguer les prédicats d’accès et de filtre

Les plans d’exécution qui ont été créés en utilisant l’outil DBMS_XPLAN (voir
« Obtenir un plan d’exécution » à la page 166) montrent l’utilisation de
l’index dans la section « Predicate Information » sous le tableau du plan
d’exécution :

------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 1445 |
| 1 | SORT AGGREGATE | | 1 | |
|* 2 | INDEX RANGE SCAN| MAUVAISIDX | 4485 | 1445 |
------------------------------------------------------
Predicate Information (identified by operation id):
2 - access("SECTION"=:A AND "ID2"=:B)
filter("ID2"=:B)

La numérotation des informations des prédicats fait référence à la colonne


« Id » du plan d’exécution. La base de données affiche aussi une étoile pour
marquer les opérations qui ont des informations de prédicat.

Cet exemple, pris du chapitre « Performance et scalabilité », montre un


INDEX RANGE SCAN qui a des prédicats d’accès et de filtre. La base de données
Oracle a la spécificité de montrer aussi quelques prédicats de filtre en
tant que prédicats d’accès, par exemple ID2=:B dans le plan d’exécution ci-
dessus.

Important
Si une condition s’affiche comme un prédicat de filtre, c’est un
prédicat de filtre, peu importe s’il est aussi affiché comme un
prédicat d’accès.

Cela signifie que l’opération INDEX RANGE SCAN parcourt l’intervalle complet
pour la condition "SECTION"=:A et applique le filtre "ID2"=:B sur chaque
ligne.

Les prédicats de filtre au niveau table sont affichés pour l’accès de table
respectif, comme TABLE ACCESS BY INDEX ROWID ou TABLE ACCESS FULL.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 171


annexe A : Plans d’exécution

PostgreSQL
Les méthodes décrites dans cette section s’appliquent à PostgreSQL 8.0 et
aux versions suivantes.

Obtenir un plan d’exécution


Un plan d’exécution PostgreSQL est récupéré en plaçant la commande
explain au début d’une requête SQL. Néanmoins, il existe une limitation
importante : il n’est pas possible de récupérer un plan d’exécution de cette
façon avec les requêtes SQL comprenant des paramètres liés (par exemple
$1, $2, etc.). Elles doivent d’abord être préparées :

PREPARE req(int) AS SELECT $1;

Notez que PostgreSQL utilise la notation $n pour les paramètres liés. La


couche d’abstraction de la base de données peut vous le cacher pour que
vous puissiez utiliser des points d’interrogation comme définis dans le
standard SQL.

Il est possible d’obtenir le plan d’exécution de la requête préparée ainsi :

EXPLAIN EXECUTE req(1);

Jusqu’à PostgreSQL 9.1, le plan d’exécution était déjà créé lors de l’appel à
prepare et pouvait donc ne pas prendre en considération les valeurs réelles
pour des paramètres liés lors du execute. À partir de PostgreSQL 9.2, la
création du plan d’exécution est repoussé jusqu’à l’exécution et peut donc
prendre en considération les valeurs réelles des paramètres liés.

Note
Les plans d’exécution des requêtes sans paramètres liés s’obtiennent
directement :

EXPLAIN SELECT 1;

Dans ce cas, l’optimiseur a toujours considéré les valeurs réelles lors


de la planification de la requête. Si vous utilisez PostgreSQL 9.1 ou une
version antérieure et que vous utilisez les paramètres liés dans votre
programme, vous devez aussi utiliser explain avec les paramètres liés
pour récupérer le même plan d’exécution.

172 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


PostgreSQL : Obtenir un plan d’exécution

Voici un plan d’exécution :

QUERY PLAN
------------------------------------------
Result (cost=0.00..0.01 rows=1 width=0)

La sortie comprend des informations similaires aux plans d’exécution


d’Oracle montrés dans tout le livre : le nom de l’opération (« Result »),
le coût relatif, l’estimation du nombre de lignes et la taille attendue de la
ligne.

Notez que PostgreSQL affiche deux valeurs de coût. Le premier est le coût de
démarrage (c’est-à-dire le coût pour récupérer la première ligne), le second
est le coût total pour l’exécution si toutes les lignes sont récupérées. Le
plan d’exécution Oracle affiche seulement la deuxième valeur.

La commande explain de PostgreSQL a deux options. L’option VERBOSE


fournit des informations supplémentaires comme le nom complètement
qualifié de la table. VERBOSE a généralement peu d’intérêt.

La deuxième option est ANALYZE. Bien qu’elle soit très utilisée, je ne


recommande pas d’avoir l’habitude de l’utiliser automatiquement car elle
force l’exécution de la requête. C’est généralement sans risque pour les
requêtes select mais cela provoque la modification des données pour
les requêtes insert, update et delete. Pour éviter le risque de modifier
vos données, vous pouvez l’englober dans une transaction et faire une
annulation après coup.

L’option ANALYZE exécute la requête et enregistre la durée réelle et le


nombre de lignes réel. Ces informations sont importantes pour trouver la
cause de mauvaises estimations de cardinalité (estimation du nombre de
lignes) :

BEGIN;
EXPLAIN ANALYZE EXECUTE req(1);

QUERY PLAN
--------------------------------------------------
Result (cost=0.00..0.01 rows=1 width=0)
(actual time=0.002..0.002 rows=1 loops=1)
Total runtime: 0.020 ms
ROLLBACK;

Notez que le plan est formaté pour mieux tenir sur la page. PostgreSQL
affiche les « vraies » valeurs sur la même ligne que les valeurs estimées.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 173


annexe A : Plans d’exécution

Avertissement
explain analyze exécute la requête même s’il s’agit d’un insert, d’un
update ou d’un delete.

Le nombre de lignes est la seule valeur affichée dans les deux parties (réel
et estimation). Ceci vous permet de trouver rapidement les estimations
erronées de cardinalité.

Enfin, les requêtes préparées doivent être fermées :

DEALLOCATE req;

Opérations

Accès aux index et aux tables


Seq Scan
L’opération Seq Scan parcourt la relation complète telle qu’elle est
stockée sur disque (comme un TABLE ACCESS FULL).

Index Scan
L’opération Index Scan réalise un parcours de B-tree, passe au travers
des nœuds feuilles pour trouver toutes les entrées correspondantes, et
récupère les données correspondantes de la table. C’est identique à un
INDEX RANGE SCAN suivi d’un TABLE ACCESS BY INDEX ROWID. Voir aussi le
Chapitre 1, « Anatomie d’un index ».

Les prédicats de filtre d’index causent souvent des problèmes de


performances pour un Index Scan. La section suivante explique
comment les identifier.

Index Only Scan (à partir de PostgreSQL 9.2)


L’opération Index Only Scan réalise un parcours du B-tree, puis traverse
les nœuds feuilles pour trouver les entrées correspondantes. Il n’est pas
nécessaire d’accéder à la table car l’index dispose de toutes les colonnes
pour satisfaire la requête (exception : les informations de visibilité).
Voir aussi « Parcours d’index seul » à la page 116.

174 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


PostgreSQL : Opérations

Bitmap Index Scan / Bitmap Heap Scan / Recheck Cond


Le message sur la liste de discussion des performances sur PostgreSQL
de Tom Lane est très clair et très précis.

Un Index Scan standard récupère les pointeurs de ligne un par


un dans l’index, et visite immédiatement la ligne pointée dans
la table. Un parcours de bitmap récupère tous les pointeurs
de ligne dans l’index en un coup, les trie en utilisant une
structure bitmap en mémoire, puis visite les lignes dans la
table dans l’ordre de leur emplacement physique.
1
—Tom Lane

Jointures

Généralement, les opérations de jointure traitent seulement deux tables à


la fois. Au cas où une requête aurait plus de jointures, elles sont traitées
séquentiellement : tout d’abord deux tables, puis le résultat intermédiaire
avec la table suivante. Dans le contexte des jointures, le terme « table »
peut aussi signifier « résultat intermédiaire ».

Nested Loops
Joint deux tables en récupérant le résultat d’une table, et en
recherchant chaque ligne de la première table dans la seconde. Voir
aussi « Boucles imbriquées » à la page 92.

Hash Join / Hash


La jointure de hachage charge les enregistrements candidats d’un côté
de la jointure dans une table de hachage (marqué avec le mot Hash dans
le plan) dont chaque enregistrement est ensuite testé avec l’autre côté
de la jointure. Voir aussi « Jointure par hachage » à la page 101.

Merge Join
La jointure d’assemblage (ou de tri) combine deux listes triées, comme
une fermeture Éclair. Les deux côtés de la jointure doivent être pré-
triés. Voir aussi « Fusion par tri » à la page 109.

1
https://www.postgresql.org/message-id/12553.1135634231@sss.pgh.pa.us

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 175


annexe A : Plans d’exécution

Tri et regroupement
Sort / Sort Key
Trie l’ensemble de données sur les colonnes mentionnées dans la partie
Sort Key. L’opération Sort a besoin d’une grande quantité de mémoire
pour matérialiser le résultat intermédiaire (sans pipeline). Voir aussi
« Indexer un tri » à la page 130.

GroupAggregate
Agrège un ensemble pré-trié suivant la clause group by. Cette opération
ne place pas de grandes quantités de données (en pipeline). Voir aussi
« Indexer le Group By » à la page 139.

HashAggregate
Utilise une table de hachage temporaire pour grouper les enregis-
trements. L’opération HashAggregate ne requiert pas de données pré-
triées. Elle utilise une grande quantité de mémoire pour matérialiser
le résultat intermédiaire (sans pipeline). La sortie n’est pas triée. Voir
aussi « Indexer le Group By » à la page 139.

Requêtes Top-N
Limit
Annule les opérations sous-jacentes quand le nombre désiré de lignes
a été récupéré. Voir aussi « Récupérer les N premières lignes » à la
page 143.

L’efficacité d’une requête top-N dépend du mode d’exécution


des opérations sous-jacentes. C’est très inefficace lors de l’arrêt
d’opérations ne fonctionnant pas en pipeline, comme un Sort par
exemple.

WindowAgg
Indique l’utilisation de fonctions de fenêtrage. Voir aussi « Utiliser les
fonctions de fenêtrage pour une pagination » à la page 156.

Attention
PostgreSQL ne peut pas exécuter des requêtes top-N lors de
l’utilisation de fonctions de fenêtrage.

176 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


PostgreSQL : Distinguer les prédicats d’accès et de filtre

Distinguer les prédicats d’accès et de filtre


La base de données PostgreSQL utilise trois méthodes différentes pour
appliquer les clauses where (prédicats) :

Prédicat d’accès (« Index Cond »)


Les prédicats d’accès expriment les conditions de début et d’arrêt du
parcours de nœuds feuilles.

Prédicat de filtre d’index (« Index Cond »)


Les prédicats de filtre d’index sont appliqués seulement lors du
parcours des nœuds feuilles. Ils ne contribuent pas aux conditions de
début et de fin, et ne diminuent pas l’intervalle parcouru.

Prédicat de filtre au niveau table (« Filter »)


Les prédicats sur les colonnes qui ne font pas partie de l’index sont
évalués sur le niveau de la table. Pour cela, la base de données doit
charger la ligne de la table.

Les plans d’exécution de PostgreSQL ne montrent pas les prédicats d’accès


et de filtre d’index séparément. Ils s’affichent tous les deux sous le titre
« Index Cond ». Cela signifie que le plan d’exécution doit être comparé à
la définition de l’index pour différencier les prédicats d’accès des prédicats
de filtre d’index.

Note
Le plan d’exécution PostgreSQL ne fournit pas suffisamment
d’informations pour trouver les prédicats de filtre d’index.

Les prédicats affichés comme « Filter » sont toujours des prédicats de filtre
au niveau table, même lorsqu’ils sont affichés au niveau de l’opération
Index Scan.

Considérez l’exemple suivant, cité la première fois dans le chapitre


« Performances et scalabilité » :

CREATE TABLE donnees (


section NUMERIC NOT NULL,
id1 NUMERIC NOT NULL,
id2 NUMERIC NOT NULL
);
CREATE INDEX donnees_key ON donnees(section, id1);

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 177


annexe A : Plans d’exécution

Le select suivant filtre sur la colonne ID2 qui n’est pas inclus dans l’index :

PREPARE req(int) AS SELECT count(*)


FROM donnees
WHERE section = 1
AND id2 = $1;
EXPLAIN EXECUTE req(1);
QUERY PLAN
-----------------------------------------------------
Aggregate (cost=529346.31..529346.32 rows=1 width=0)
Output: count(*)
-> Index Scan using donnees_key on donnees
(cost=0.00..529338.83 rows=2989 width=0)
Index Cond: (donnees.section = 1::numeric)
Filter: (donnees.id2 = ($1)::numeric)

Le prédicat ID2 est affiché en tant que « Filter » sous l’opération


Index Scan. Ceci est dû au fait que PostgreSQL réalise un accès à la
table lors de l’opération Index Scan. En d’autres termes, l’opération
TABLE ACCESS BY INDEX ROWID de la base de données Oracle est cachée dans
l’opération Index Scan de PostgreSQL. De ce fait, il est possible qu’un
Index Scan filtre sur des colonnes qui ne sont pas incluses dans l’index.

Important
Les prédicats Filter de PostgreSQL sont des prédicats de filtre de
niveau table, même quand ils sont affichés en tant qu’Index Scan.

Quand nous ajoutons l’index à partir du chapitre « Performances et


scalabilité », nous pouvons voir que toutes les colonnes s’affichent en tant
que « Index Cond », que ce soit des prédicats d’accès ou de filtre.

CREATE INDEX scale_lent


ON donnees (section, id1, id2);

Le plan d’exécution avec le nouvel index ne montre aucune condition de


filtre :

QUERY PLAN
------------------------------------------------------
Aggregate (cost=14215.98..14215.99 rows=1 width=0)
Output: count(*)
-> Index Scan using scale_lent on donnees
(cost=0.00..14208.51 rows=2989 width=0)
Index Cond: (section = 1::numeric AND id2 = ($1)::numeric)

178 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


PostgreSQL : Distinguer les prédicats d’accès et de filtre

Veuillez noter que la condition sur ID2 ne peut pas diminuer le parcours
de nœuds feuilles car l’index a la colonne ID1 avant la colonne ID2.
L’Index Scan va parcourir l’intervalle entier pour trouver la condition
SECTION=1::numeric et appliquer le filtre ID2=($1)::numeric sur chaque
ligne qui remplit la clause sur SECTION.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 179


annexe A : Plans d’exécution

SQL Server
La méthode décrite dans cette section s’applique à SQL Server Management
Studio 2005 ainsi qu’aux versions ultérieures.

Obtenir un plan d’exécution


Avec SQL Server, il existe plusieurs façons de récupérer un plan d’exécution.
Les deux méthodes les plus importantes sont :

Graphiquement
La représentation graphique des plans d’exécution de SQL Server est
facilement accessible dans Management Studio mais elle est difficile à
partager car les informations de prédicat sont seulement visibles quand
la souris est au-dessus d’une opération spécifique.

En tableau
Le plan d’exécution en tableau est difficile à lire mais facile à copier car
il montre toutes les informations adéquates d’un seul coup.

Graphiquement

Le plan d’exécution graphique est généré avec l’un des deux boutons
surlignés ci-dessous.

Le bouton gauche récupère le plan d’exécution de la requête sélectionnée.


Le bouton droit capture le plan la prochaine fois qu’une requête est
exécutée.

Dans les deux cas, la représentation graphique du plan d’exécution apparaît


dans l’onglet « Execution plan » du panneau « Results ».

180 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


SQL Server : Obtenir un plan d’exécution

La représentation graphique est facile à lire avec un peu de pratique.


Néanmoins, elle affiche seulement les informations essentielles : les opéra-
tions et les tables ou index sur lesquelles elles agissent.

Management Studio affiche plus d’informations lorsque vous déplacez la


souris au-dessus d’une opération. Cela rend difficile le partage d’un plan
d’exécution dans tous ses détails.

En tableau
La représentation en tableau d’un plan d’exécution de SQL Server est récu-
péré en profilant l’exécution d’une requête. La commande suivante l’active :

SET STATISTICS PROFILE ON

Une fois activé, chaque requête exécutée produit un ensemble


supplémentaire de résultats. Par exemple, les requêtes select produisent
deux ensembles de résultats : le premier pour le résultat de la requête, et
le second pour le plan d’exécution.

Le plan d’exécution en tableau est difficilement utilisable dans SQL Server


Management Studio car la colonne StmtText est trop large pour tenir sur
un écran.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 181


annexe A : Plans d’exécution

L’avantage de cette représentation est qu’elle peut être copiée sans perdre
d’informations adéquates. Cela se révèle très pratique si vous voulez poster
un plan d’exécution SQL Server sur un forum ou sur une plateforme
similaire. Dans ce cas, il est souvent suffisant de copier la colonne StmtText
et de la reformater un peu :

select COUNT(*) from employes;


|--Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(...))
|--Stream Aggregate(DEFINE:([Expr1005]=Count(*)))
|--Index Scan(OBJECT:([employes].[employes_pk]))

Enfin, vous pouvez désactiver le profilage ainsi :

SET STATISTICS PROFILE OFF

Opérations

Accès aux tables et aux index


SQL Server a une terminologie simple : les opérations « Scan » lisent la
totalité de l’index ou de la table, alors que les opérations « Seek » utilisent
le B-tree ou une adresse physique (RID, équivalent de ROWID sur Oracle) pour
accéder à une partie spécifique de l’index ou de la table.

Index Seek, Clustered Index Seek


L’opération Index Seek effectue un parcours B-tree et suit la chaîne de
nœuds feuilles pour trouver toutes les entrées correspondantes. Voir
aussi « Anatomie d’un index » à la page 1.

Index Scan, Clustered Index Scan


Lit entièrement l’index (toutes les lignes) dans l’ordre de l’index. Selon
diverses statistiques systèmes, la base de données peut utiliser cette
opération si elle a besoin de toutes les lignes dans l’ordre de l’index,
par exemple pour une clause order by correspondante.

Key Lookup (Clustered)


Récupère une ligne unique à partir d’un index organisé. Ceci est
équivalent à un INDEX UNIQUE SCAN sur Oracle pour une table organisée
en index (IOT). Voir aussi « Regrouper les données » à la page 111.

182 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


SQL Server : Opérations

RID Lookup (Heap)


Récupère une ligne unique à partir d’une table, équivalent au TABLE
ACCESS BY INDEX ROWID sur Oracle. Voir aussi « Anatomie d’un index »
à la page 1.

Table Scan
Cette opération est également connue comme un parcours complet
de table. Elle lit entièrement la table, lignes et colonnes, comme
stockées sur le disque. Bien que des opérations de lecture multi-
blocs puissent améliorer considérablement la vitesse d’un Table Scan,
il s’agit quand même d’une des opérations les plus coûteuses. En
dehors des taux élevés d’entrées/sorties, un Table Scan doit également
inspecter toutes les lignes de la table, et par conséquent requérir
une quantité considérable de temps processeur. Voir aussi « Parcours
complet de table » à la page 13.

Jointures
En général, les opérations de jointure ne traitent que deux tables en
même temps. Dans le cas où une requête a plus de tables jointes, elles
sont exécutées séquentiellement : d’abord deux tables, puis le résultat
intermédiaire avec la table suivante. Dans le contexte de jointure, le terme
« table » peut aussi signifier « résultat intermédiaire ».

Nested Loops
Joint deux tables en récupérant le résultat d’une table et en requêtant
l’autre table pour chacune des lignes de la première. SQL Server
utilise également des opérations de boucles imbriquées pour récupérer
des données de table après un accès d’index. Voir aussi « Boucles
imbriquées » à la page 92.

Hash Match
La jointure par correspondance de hachage charge les enregistrements
candidats à partir d’un côté de la jointure dans une table de hachage,
qui est alors sondée pour chacune des lignes de l’autre côté de la
jointure. Voir aussi « Jointure par hachage » à la page 101.

Merge Join
La jointure d’assemblage combine deux listes triées comme une
fermeture Éclair. Les deux côtés de la jointure doivent être pré-triés.
Voir aussi « Fusion par tri » à la page 109.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 183


annexe A : Plans d’exécution

Tri et regroupement

Sort
Trie le résultat selon la clause order by. Cette opération nécessite une
grande quantité de mémoire pour matérialiser le résultat intermédiaire
(sans pipeline). Voir aussi « Indexer un tri » à la page 130.

Sort (Top N Sort)


Trie un sous ensemble du résultat selon la clause order by. Elle est
utilisée pour les requêtes top-N si une exécution avec pipeline n’est pas
possible. Voir aussi « Récupérer les N premières lignes » à la page 143.

Stream Aggregate
Agrège un ensemble pré-trié selon la clause group by. Cette opération
ne place pas le résultat intermédiaire dans un tampon : il est exécuté
dans un pipeline. Voir aussi « Indexer le Group By » à la page 139.

Hash Match (Aggregate)


Groupe les résultats en utilisant une table de hachage. Cette opération
nécessite une grande quantité de mémoire pour matérialiser le résultat
intermédiaire (sans pipeline). La sortie n’est pas triée. Voir aussi
« Indexer le Group By » à la page 139.

Requêtes Top-N

Top
Annule les opérations sous-jacentes quand le nombre désiré de lignes
a été récupéré. Voir aussi « Récupérer les N premières lignes » à la
page 143.

L’efficacité d’une requête top-N dépend du mode d’exécution


des opérations sous-jacentes. C’est très inefficace lors de l’arrêt
d’opérations ne fonctionnant pas en pipeline, comme un Sort par
exemple.

184 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


SQL Server : Distinguer les prédicats d’accès et de filtre

Distinguer les prédicats d’accès et de filtre


La base de données SQL Server utilise trois méthodes différentes pour
appliquer les clauses where (prédicats) :

Prédicat d’accès (« Seek Predicates »)


Les prédicats d’accès expriment les conditions de début et de fin des
parcours des nœuds feuilles.

Prédicat de filtre d’index


(« Predicates » ou « where » pour les opérations sur les index)
Les prédicats de filtre d’index sont appliqués seulement lors du
parcours de nœuds feuilles. Ils ne contribuent pas aux conditions de
début et d’arrêt, et ne diminuent pas l’intervalle parcouru.

Prédicat de filtre au niveau table


(« where » pour les opérations sur la table)
Les prédicats sur les colonnes qui ne font pas partie de l’index sont
évalués au niveau de la table. Pour cela, la base de données doit d’abord
charger la ligne à partir de la table.

La section suivante explique comment identifier les prédicats de filtre dans


les plans d’exécution de SQL Server. Elle est basée sur l’échantillon utilisé
pour démontrer l’impact d’un prédicat de filtre d’index dans le Chapitre 3.

CREATE TABLE donnees (


section NUMERIC NOT NULL,
id1 NUMERIC NOT NULL,
id2 NUMERIC NOT NULL
);
CREATE INDEX scale_lent ON donnees(section, id1, id2);

L’ordre en exemple sélectionne les données en fonction de SECTION et ID2 :

SELECT count(*)
FROM donnees
WHERE section = @sec
AND id2 = @id2

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 185


annexe A : Plans d’exécution

Plan d’exécution graphique

Le plan d’exécution graphique cache l’information de prédicat dans une


info-bulle qui n’est affichée que lorsque la souris passe sur l’opération
Index Seek.

Les Seek Predicates de SQL Server correspondent aux accès par prédicat
d’Oracle (ils limitent les parcours de nœuds feuilles). Les prédicats de filtre
sont justes nommés Predicates dans le plan d’exécution graphique de SQL
Server.

186 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


SQL Server : Distinguer les prédicats d’accès et de filtre

Plan d’exécution tabulaire

Les plans d’exécution tabulaires ont les informations de prédicat dans la


colonne où apparaît l’opération. Il est par conséquent très facile de copier
et coller toutes les informations pertinentes d’un seul coup.

DECLARE @sec numeric;


DECLARE @id2 numeric;
SET STATISTICS PROFILE ON

SELECT count(*)
FROM donnees
WHERE section = @sec
AND id2 = @id2

SET STATISTICS PROFILE OFF

Le plan d’exécution est montré comme un second ensemble de résultats


dans le panneau de résultats. L’exemple suivant correspond à la colonne
StmtText, avec un léger reformatage pour une meilleure lecture :

|--Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(...))
|--Stream Aggregate(DEFINE:([Expr1008]=Count(*)))
|--Index Seek(OBJECT:([donnees].[scale_slow]),
SEEK: ([donnees].[section]=[@sec])
ORDERED FORWARD
WHERE:([donnees].[id2]=[@id2]))

L’étiquette SEEK présente les prédicats d’accès, et l’étiquette WHERE marque


les prédicats de filtre.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 187


annexe A : Plans d’exécution

MySQL
La méthode décrite dans cette section s’applique à toutes les versions de
MySQL.

Obtenir un plan d’exécution


Ajoutez explain devant un ordre SQL pour obtenir son plan d’exécution.

EXPLAIN SELECT 1;

Le plan est montré de manière tabulaire (quelques colonnes moins


importantes en moins) :

~+-------+------+---------------+------+~+------+------------~
~| table | type | possible_keys | key |~| rows | Extra
~+-------+------+---------------+------+~+------+------------~
~| NULL | NULL | NULL | NULL |~| NULL | No tables...
~+-------+------+---------------+------+~+------+------------~

L’information la plus importante est la colonne TYPE. Bien que la


documentation MySQL y fasse référence en tant que « type de jointure », je
préfère la décrire comme un « type d’accès » car elle précise en fait la façon
dont les données sont accédées. La signification des différentes valeurs de
types sont décrites dans la section suivante.

Opérations

Accès aux tables et aux index


La sortie de plan d’exécution de MySQL a tendance à donner une fausse
impression de sécurité car elle donne beaucoup d’informations sur les index
utilisés. Bien que cela soit techniquement correct, cela ne signifie pas que
l’index soit efficacement utilisé. L’information la plus importante se situe
dans la colonne TYPE de la sortie de l’explain de MySQL (mais même ici, le
mot-clé INDEX n’indique pas une indexation adéquate).

188 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


MySQL : Opérations

eq_ref, const
Réalise un parcours du B-tree pour trouver une ligne (comme
INDEX UNIQUE SCAN) et récupère les colonnes supplémentaires de la
table si nécessaire (TABLE ACCESS BY INDEX ROWID). La base de données
utilise cette opération si une clé primaire ou une contrainte unique
assure que le critère de recherche correspond à au plus une ligne. Voir
“Utiliser un index” pour vérifier si l’accès à la table survient ou pas.

ref, range
Réalise un parcours du B-tree, suit la chaîne de nœuds feuilles
pour trouver toutes les entrées correspondantes (similaire à un
INDEX RANGE SCAN) et récupère les colonnes supplémentaires à partir
de la table primaire si nécessaire (TABLE ACCESS BY INDEX ROWID). Voir
“Utiliser un index” pour vérifier si l’accès à la table survient ou pas.

index
Lit l’index entier (toutes les lignes) dans l’ordre de l’index (similaire à
un INDEX FULL SCAN).

ALL
Lit la table entière, toutes les lignes et toutes les colonnes, comme elle
est stockée sur disque. En dehors de forts taux d’entrées/sorties, un
parcours de table doit également inspecter toutes les lignes de la table,
et peut donc aussi consommer beaucoup de temps processeur. Voir
aussi « Parcours complet de table » à la page 13.

Using Index (dans la colonne « Extra »)


Quand la colonne « Extra » affiche “Using Index”, cela signifie
que la table n’est pas accédée car l’index dispose de toutes les
données nécessaires. Pensez à “using index ONLY” (utilisation de l’index
uniquement). Néanmoins, si un index trié est utilisé (par exemple
l’index PRIMARY lors de l’utilisation d’InnoDB), “Using Index” n’apparaît
pas dans la colonne Extra bien qu’il s’agisse techniquement d’un Index-
Only Scan. Voir aussi « Regrouper les données » à la page 111.

PRIMARY (dans la colonne « key » ou « possible_keys »)


PRIMARY est le nom de l’index automatiquement créé pour la clé
primaire.

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 189


annexe A : Plans d’exécution

Tri et regroupement
using filesort (dans la colonne « Extra »)
« using filesort » dans la colonne Extra indique une opération de tri
explicite, sans se soucier de l’endroit où se fait le tri (en mémoire ou sur
le disque). « Using filesort » a besoin d’une grande quantité de mémoire
pour matérialiser le résultat intermédiaire (sans pipeline). Voir aussi
« Indexer un tri » à la page 130.

Requêtes Top-N
Implicite (pas de « using filesort » dans la colonne « Extra »)
Un plan d’exécution MySQL ne montre pas une requête top-N de
manière explicite. Si vous utilisez la syntaxe limit et ne voyez pas
« using filesort » dans la colonne extra, c’est qu’elle est exécutée
dans un pipeline. Voir aussi « Récupérer les N premières lignes » à la
page 143.

Distinguer les prédicats d’accès et de filtre


La base de données MySQL utilise trois méthodes différentes pour appliquer
les clauses where (prédicats) :

Prédicat d’accès (via la colonne « key_len »)


Les prédicats d’accès expriment les conditions de début et de fin des
parcours de nœuds feuilles.

Prédicat de filtre d’index


(« Using index condition », depuis MySQL 5.6)
Les prédicats de filtres d’index sont appliqués seulement lors du
parcours de nœuds feuilles. Ils ne contribuent pas aux conditions de
début et d’arrêt, et ne diminuent pas l’intervalle parcouru.

Prédicat de filtre au niveau table


(« Using where » dans la colonne « Extra »)
Les prédicats sur les colonnes qui ne font pas partie de l’index sont
évalués au niveau de la table. Pour cela, la base de données doit tout
d’abord charger la ligne à partir de la table.

190 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


MySQL : Distinguer les prédicats d’accès et de filtre

Les plans d’exécution MySQL ne montrent pas le type de prédicat utilisé


pour chaque condition, ils ne font que lister les types de prédicats utilisés.

Dans l’exemple suivant, la totalité de la clause where est utilisée comme un


prédicat d’accès :

CREATE TABLE demo (


id1 NUMERIC
, id2 NUMERIC
, id3 NUMERIC
, val NUMERIC);

INSERT INTO demo VALUES (1,1,1,1);


INSERT INTO demo VALUES (2,2,2,2);
CREATE INDEX demo_idx
ON demo
(id1, id2, id3);
EXPLAIN
SELECT *
FROM demo
WHERE id1=1
AND id2=1;
+------+----------+---------+------+-------+
| type | key | key_len | rows | Extra |
+------+----------+---------+------+-------+
| ref | demo_idx | 12 | 1 | |
+------+----------+---------+------+-------+

Il n’y a pas de « Using where » ou « Using index condition » indiqué dans la


colonne « Extra ». L’index est cependant utilisé (type=ref, key=demo_idx)
et vous pouvez donc supposer que l’intégralité de la clause where qualifie
un prédicat d’accès.

Notez aussi que la colonne ref indique que les deux colonnes sont utilisées
à partir de l’index (les deux sont des constantes de la requête dans cet
exemple). Vous pouvez aussi utiliser la valeur de la colonne key_len pour
le vérifier. Cela montre que la requête utilise les 12 premiers octets de la
définition de l’index. Pour relier cela à des noms de colonnes, il « suffit »
de connaître la place occupée par chacun des champs (voir « Data Type
Storage Requirements » dans la documentation MySQL). En l’absence
de contrainte NOT NULL, MySQL a besoin d’un octet supplémentaire pour
chaque colonne. Au final, chaque colonne de type NUMERIC nécessite 6 octets
dans cet exemple. Par conséquent, la longueur de clé de 12 confirme que

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 191


annexe A : Plans d’exécution

les deux premières colonnes de l’index sont utilisées en tant que prédicat
d’accès.

Lorsque le filtre se fait sur la colonne ID3 (à la place de la colonne ID2),


MySQL 5.6 (et les versions suivantes) utilise un prédicat de filtre d’index
(« Using index condition ») :

EXPLAIN
SELECT *
FROM demo
WHERE id1=1
AND id3=1;

+------+----------+---------+------+-----------------------+
| type | key | key_len | rows | Extra |
+------+----------+---------+------+-----------------------+
| ref | demo_idx | 6 | 1 | Using index condition |
+------+----------+---------+------+-----------------------+

Dans ce cas, la longueur de clé de six -key_len=6 et seulement une constante


(const) dans la colonne ref signifie qu’une seule colonne est utilisée comme
prédicat d’accès.

Les précédentes versions de MySQL utilisaient un prédicat de filtre au


niveau table pour cette requête, identifié par « Using where » dans la
colonne « Extra » :

+------+----------+---------+------+-------------+
| type | key | key_len | rows | Extra |
+------+----------+---------+------+-------------+
| ref | demo_idx | 6 | 1 | Using where |
+------+----------+---------+------+-------------+

192 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


Index
Symboles D
2PC, 90 data transport object (DTO), 106
?, :var, @var (voir paramètre lié) DATE
travailler efficacement avec, 62
DBMS_XPLAN, 167
A DEALLOCATE, 174
analyse, 18 DETERMINISTIC (Oracle), 30
distinct, 98
distinct()
B dans JPA et Hibernate, 97
B-tree (arbre de recherche équilibré), 4 DML, 159
between, 44 dynamic-update (Hibernate), 164
bind peeking (Oracle), 75
Bitmap Index Scan (PostgreSQL), 175
Boucles imbriquées
E
Nested Loops, 92 écoute de paramètres (SQL Server), 76
envoi par pipeline, 92
estimation de cardinalité, 27
C explain
MySQL, 188
cache de plans d’exécution, 75 Oracle, 166
CBO (voir optimiseur, basé sur le coût) PostgreSQL, 172
chargement à la demande
pour les attributs scalaires
(colonnes), 105 F
chargement à l’avance facteur de regroupement, 114
eager fetching, 96 automatiquement optimisé, 133
clé de regroupement, 124 facteur d’ordonnancement, 21
clé primaire sans index unix, 11 FBI (voir index, basé sur une fonction)
cohérence éventuelle, 90 FETCH ALL PROPERTIES (HQL), 105
collation, 24 fetch first, 144
colonnes calculées (SQL Server), 28 fonctions, 24
colonnes virtuelles pour des de fenêtrage, 156
contraintes NOT NULL dans les index déterministe
fonctionnels, 59 dans des index partiels, 52
commit déterministes, 29
contraintes différables, 11 fonctions de fenêtrage, 156
en deux phases, 90 full table scan
implicite pour truncate table, 163 All (MySQL), 189
compilation, 18
complexité
logarithmique, 7 G
complexité logarithmique, 7 group by, 139
contrainte avec les bases de données
différable, 11 PostrgeSQL et Oracle et un index
NOT NULL, 56 ASC/DESC, non exécuté en pipeline,
contrainte DEFERRABLE, 11 140
count(*)
Oracle requiert la contrainte
NOT NULL, 57 H
souvent en tant que parcours HASH GROUP BY, 169
d’index seul, 120 HASH JOIN (Oracle), 169
COUNT STOPKEY, 145 HASH Join (PostgreSQL), 175

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 193


Hash Match, 183 informations sur les prédicats, 20
Hash Match (Aggregate), 184 injection SQL, 32
heap table, 3 INTERNAL_FUNCTION, 67
Hibernate IOT (table organisée en index)
chargement à l’avance, 96 IOT (index-organized table), 123
ILIKE utilise LOWER, 99
met à jour toutes les colonnes, 164
hint, 19 J
jointure, 91
externe complète, 109
I jointure par hachage, 101
IMMUTABLE (PostgreSQL), 30 Jointure par fusion, 109
INCLUDE, 121
index
basé sur des fonctions, 24 K
basé sur une fonction Key Lookup (Clustered), 182
insensible à la casse, 24
pour indexer les calculs
mathématiques, 77 L
couvrant, 117 lecture multi-blocs
fulltext, 48 pour un INDEX FAST FULL SCAN, 168
jointure, 50 pour un parcours complet de table,
limites 13
MySQL, Oracle, PostgreSQL, 121 LIKE, 45
SQL Server, 122 alternatives, 48
merge, 49 en tant que prédicat filtre d’index,
multi-colonnes, 12 112
mauvais ordre (effets), 81 sur les colonnes DATE, 67
partiel, 51 sur une colonne DATE, 67
préfixe (MySQL), 121 limit (MySQL, PostgreSQL), 144
secondaire, 123 liste doublement chaînee, 2
index bitmap, 50 LOWER, 24
index dans les plans d’exécution de
MySQL, 189
index partiel, 51 M
index regroupé Merge Join
clustered index, 123 PostgreSQL, 175
transformer en table standard sur SQL Server, 183
SQL Server, 127 MERGE JOIN (Oracle), 169
Index Cond (PostgreSQL), 177 MVCC, 163
INDEX FAST FULL SCAN, 168 affectent les parcours d’index seul
INDEX FULL SCAN, 168 de PostgreSQL, 174
Index Only Scan (PostgreSQL), 174 mythes
INDEX RANGE SCAN, 167 la colonne la plus sélective en
Index Scan (PostgreSQL), 174 premier
Index Seek, 182 disproof, 43
INDEX UNIQUE SCAN, 167 origine, 50
lors de l’accès à une IOT, 124 le SQL dynamique est lent, 72, 74
information de prédicat Oracle ne peut pas indexer les NULL,
dans les plans d’exécution 56
Oracle, 170
information prédicat
prédicats accès et filtre, 44 N
informations de prédicat Nested Loops
dans les plans d’exécution PostgreSQL, 175
SQL Server, 185 SQL Server, 183

194 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


NESTED LOOPS (Oracle), 168 partitions et paramètres liés, 35
NOSORT plan d’exécution, 10, 165
SORT GROUP BY, 140 cache, 32, 75
WINDOW, 157 création
NULL MySQL, 188
indexer dans Oracle, 54 SQL Server, 180
nœud feuille, 2 créer
diviser, 160 Oracle, 166
nœud racine, 5 PostgreSQL, 172
diviser, 160 jointures
MySQL, 188
Oracle, 167
O PostgreSQL, 174
objets partiels (ORM), 104 SQL Server, 182
offset (MySQL, PostgreSQL), 148 planificateur de requêtes (voir
optimiseur, 18 optimiseur)
basé sur des règles, 18 PLAN_TABLE, 166
basé sur le coût, 18 Prédicat accès, 44
hint, 19 prédicats de filtre
statistiques, 21 effets (graphe), 81
OPTIMIZE FOR (SQL Server), 76 reconnaissance dans les plans
OPTION (SQL Server), 76 d’exécution
OR Oracle, 170
pour désactiver les filtres, 72 PostgreSQL, 177
order by, 130 SQL Server, 185
ASC, DESC, 134 prédicats d’accès
matrice de support, 138 reconnaissance dans les plans
NULLS FIRST/LAST, 137 d’exécution
OVER(), 156 Oracle, 170
PostgreSQL, 177
SQL Server, 185
P prepare (PostgreSQL), 172
pagination problème N+1, 92
méthode de recherche, 150
approximative, 152
méthode décalage, 148 R
paramètre lié, 32 RBO (voir optimiseur, basé sur des
contre-indications règles)
filtres LIKE, 47 RECOMPILE (astuce SQL Server), 76
histogrammes, 34 Requête top-N, 143
partitions, 35 RID, 3
pour la mise en cache du plan RID Lookup (Heap), 183
d’exécution, 32 ROWID, 3
sûreté du type, 66 ROWNUM (pseudo-colonne Oracle), 144,
paramétrisation automatique (SQL 148
Server), 39 ROW_NUMBER, 156
parcourir
pagination, 147
parcours complet de table, 13 S
Seq Scan (PostgreSQL), 174 Scalabilité, 79, 81
TABLE ACCESS FULL (Oracle), 168 horizontal, 87
Table Scan (SQL Server), 183 Seek Predicates (SQL Server), 185
parcours d’index seul, 116 select *, à éviter pour
partage de curseur (Oracle), 39 activer les parcours d’index seul, 120
partage de curseur adaptatif (Oracle), améliorer les performances de la
75 jointure de hachage, 104

Ex Libris scscscscsc <scscscscscscscsc@rhyta.com> 195


séquençage des lignes, 113
Seq Scan, 174
Sort (SQL Server), 184
SORT GROUP BY, 169
NOSORT, 140
SORT ORDER BY, 130
STOPKEY, 145
SSD (Solid State Disk), 89
STATISTICS PROFILE, 181
statistiques, 21
pour les index fonctionnels avec
Oracle, 28
STOPKEY
COUNT, 145
SORT ORDER BY, 146
WINDOW, 157
Stream Aggregate, 184

T
table (heap), 123
table organisée en index
index-organized table, 122
support des bases de données, 126
table standard
créer avec SQL Server, 127
théorème CAP, 90
top (SQL Server), 145
TO_CHAR(DATE), 66
transformateur d’ensemble de
résultats, 98
TRUNC(DATE), 62
truncate table, 163
triggers non exécutés, 163

U
UPPER, 24

V
VACUUM (PostgreSQL), 163
valeur du coût, 18
valeurs de ligne, 152

W
where, 9
conditionnel, 72
Plan d’exécution SQL Server, 187

196 Ex Libris scscscscsc <scscscscscscscsc@rhyta.com>


SQL : Au Cœur des Performances
aide les développeurs à améliorer les performances des bases de données.
Le thème est le SQL et couvre les bases de données principales, sans se
perdre dans les détails de chaque produit spécifique.

En commençant par les bases de l’indexation et de la clause where, ce


livre guide les développeurs au travers de toutes les parties d’une requête
SQL et explique les dangers d’outils ORM tels qu’Hibernate.

Les thèmes couverts incluent :


» Utiliser des index multi-colonnes
» Utiliser correctement les fonctions SQL
» Utiliser efficacement des requêtes LIKE
» Optimiser les opérations de jointure
» Regrouper les données pour améliorer les performances
» Exécuter en pipeline les clauses order by et group by
» Obtenir les meilleures performances des requêtes de pagination
» Comprendre la scalabilité des bases de données

Sa structure systématique fait de « SQL : Au Cœur des Performances » un


manuel de cours et un manuel de référence à la fois. Il devrait être dans
la bibliothèque de chaque développeur.

Couvre
Oracle® Database SQL Server ® MySQL PostgreSQL

À propos de l’auteur
Markus Winand développe des applications SQL depuis 1998. Il est princi-
palement intéressé par les performances, la scalabilité, la fiabilité et plus
généralement tous les autres aspects techniques de la qualité dans les
logiciels. Markus travaille actuellement en tant qu’indépendant et donne
des formations à Vienne, en Autriche. http://winand.at/

À propos du traducteur
Guillaume Lelarge développe des applications SQL depuis 1999, tout
d’abord sur SQL Server, puis sur PostgreSQL. Il participe à la traduction du
manuel de PostgreSQL, et est un des deux traducteurs du livre « Bases de
données PostgreSQL – Gestion des performances ». Guillaume est directeur
technique de la société Dalibo. http://dalibo.com/

ISBN 978-3-9503078-3-2

EUR 29.95 9 783950 307832

Vous aimerez peut-être aussi