Académique Documents
Professionnel Documents
Culture Documents
ITI
ON
FR
AN
ÇA
ISE
SQL :
AU CŒUR DES
PERFORMANCES
COUVRE TOUTES LES BASES DE DONNÉES MAJEURES
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.
Maderspergerstasse 1-3/9/11
1160 Wien
AUSTRIA
<office@winand.at>
La présente publication est protégée par les droits d’auteur. Tous droits
réservés.
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
Markus Winand
Vienne, Autriche
Table des matières
Préface ............................................................................................ vi
iv
SQL : Au cœur des performances
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 ?
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.
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.
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.
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
viii
Chapitre 1
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.
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.
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.
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
eu s
e
ud nch
s
ille
ne
a
Nœ aci
Nœ br
40 4A 1B
sf
r
s
ud
ud
Nœ
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
Note
Un B-tree est un arbre équilibré — pas un arbre binaire.
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.
Important
L’index B-tree permet de trouver un nœud feuille rapidement.
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.
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
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 :
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.
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.
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.
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 :
--------------------------------------------------------------
|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)
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.
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.
L’index pour la nouvelle clé primaire est donc défini de la façon suivante :
---------------------------------------------------
| 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.
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.
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
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.
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) :
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.
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.
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.
--------------------------------------------------------------
|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 |
--------------------------------------------------------------
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.
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.
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.
----------------------------------------------------------------
|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 |
---------------------------------------------------------------
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.
----------------------------------------------------
| Id | Operation | Name | Rows | Cost |
----------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 477 |
|* 1 | TABLE ACCESS FULL| EMPLOYES | 1 | 477 |
----------------------------------------------------
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.
Astuce
L’Annexe A, « Plans d’exécution » explique comment trouver la partie
« Predicate Information » pour chaque base de données.
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
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.
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.
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é.
---------------------------------------------------------------
|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 :
--------------------------------------------------------------
| 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 |
--------------------------------------------------------------
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 :
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
----------------------------------------------------
| Id | Operation | Name | Rows | Cost |
----------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 477 |
|* 1 | TABLE ACCESS FULL| EMPLOYES | 10 | 477 |
----------------------------------------------------
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.
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 :
--------------------------------------------------------------
|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')
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.
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.
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 :
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.
2
https://dev.mysql.com/doc/refman/5.7/en/generated-column-index-optimizations.html
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.
Réflexion
Comment pouvez-vous utiliser un index pour optimiser une requête
sur les employés de 42 ans ?
Sur-indexation
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.
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.
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.
3
https://fr.wikipedia.org/wiki/Injection_SQL
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 :
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é.
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.
À 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.
Astuce
Ne pas utiliser les paramètres liés est comme recompiler un
programme chaque fois qu’on veut l’utiliser.
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.
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.
C#
Sans paramètres liés :
int id_supplementaire;
SqlCommand cmd = new SqlCommand(
"select prenom, nom"
+ " from employes"
+ " where id_supplementaire = " + id_supplementaire
, connection);
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);
Java
Sans paramètre lié :
int id_supplementaire;
Statement cmd = connection.createStatement(
"select prenom, nom"
+ " from employes"
+ " where id_supplementaire = " + id_supplementaire
);
int id_supplementaire;
PreparedStatement cmd = connection.prepareStatement(
"select prenom, nom"
+ " from employes"
+ " where id_supplementaire = ?"
);
cmd.setInt(1, id_supplementaire);
Perl
Sans paramètre lié :
my $id_supplementaire;
my $sth = $dbh->prepare(
"select prenom, nom"
. " from employes"
. " where id_supplementaire = $id_supplementaire"
);
$sth->execute();
my $id_supplementaire;
my $sth = $dbh->prepare(
"select prenom, nom"
. " from employes"
. " where id_supplementaire = ?"
);
$sth->execute($id_supplementaire);
PHP
En utilisant MySQL, sans paramètre lié :
$mysqli->query(
"select prenom, nom"
. " from employes"
. " where id_supplementaire = " . $id_supplementaire);
Ruby
Sans paramètre lié :
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:
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).
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.
Bien sûr, un index idéal doit couvrir les deux colonnes mais la question est
de connaître l’ordre idéal.
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.
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
06-JAN-71 4 ROWID
06-JAN-71 11 ROWID
08-JAN-71 6 ROWID
09-JAN-71 17 ROWID
09-JAN-71 30 ROWID
12-JAN-71 3 ROWID
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 09-MAR-88 ROWID
27 08-OCT-91 ROWID
30 30-SEP-53 ROWID
Astuce
Règle d’or : indexer pour l’égalité, puis pour les intervalles.
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.
--------------------------------------------------------------
|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 |
--------------------------------------------------------------
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.
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.
---------------------------------------------------------------
| 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 |
---------------------------------------------------------------
Notez que between inclut toujours les valeurs indiquées, tout comme les
opérateurs inférieur ou égal (<=) et supérieur ou égal (>=) :
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.
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.
Astuce
Évitez les expressions LIKE ayant des caractères joker en début (par
exemple, '%TERM').
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.
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
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.
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.
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.
Important
Les index bitmap sont pratiquement inutilisables pour les traite-
ments en ligne (OLTP).
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.
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.
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.
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';
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
SELECT dummy
, dummy || ''
, dummy || NULL
FROM dual
D D D
- - -
X X X
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 :
----------------------------------------------------
| 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)
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 :
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.
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.
Astuce
Ajouter une colonne qui ne peut pas être NULL pour indexer NULL
comme toutes les autres valeurs.
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.
---------------------------------------------------------------
|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 |
---------------------------------------------------------------
----------------------------------------------------
| 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.
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 |
----------------------------------------------------
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 |
-------------------------------------------------------------
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 |
----------------------------------------------------
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.
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)
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.
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.
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')
SELECT ...
FROM ventes
WHERE date_vente BETWEEN DEB_TRIMESTRE(?)
AND FIN_TRIMESTRE(?)
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.
MySQL
Oracle Database
PostgreSQL
SQL Server
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.
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 :
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'
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 :
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
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@!))
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'
SELECT ...
FROM ...
WHERE chaine_numerique = 42
SELECT ...
FROM ...
WHERE TO_NUMBER(chaine_numerique) = 42
SELECT ...
FROM ...
WHERE chaine_numerique = TO_CHAR(42)
42
042
0042
00042
...
Astuce
Utilisez les types numériques pour stocker des nombres.
SELECT ...
FROM ...
WHERE nombre_numerique = '42'
SELECT ...
FROM ...
WHERE TO_CHAR(nombre_numerique) = '42'
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)
SELECT ...
FROM ...
WHERE colonne_dateheure
> DATE_ADD(now(), INTERVAL -1 DAY)
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.
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')
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 :
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.
----------------------------------------------------
| 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))
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.
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
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.
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.
6
http://www.sommarskog.se/dyn-search-2008.html
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.
SELECT nombre_numerique
FROM nom_table
WHERE nombre_numerique - 1000 > ?
SELECT a, b
FROM nom_table
WHERE 3*a + 5 = b
SELECT nombre_numerique
FROM nom_table
WHERE nombre_numerique + 0 = ?
SELECT a, b
FROM nom_table
WHERE 3*a - b = -5
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 :
Performance et scalabilité
Ce chapitre porte sur les performances et la scalabilité des bases de
données.
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.
1
https://fr.wikipedia.org/wiki/Scalability
SELECT count(*)
FROM grosses_donnees
WHERE section = ?
AND id2 = ?
0.10 0.10
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
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é.
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]
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 ?
------------------------------------------------------
| 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 |
------------------------------------------------------
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.
Astuce
Faites attention aux informations de prédicats.
------------------------------------------------------
| Id | Operation | Name | Rows | Cost |
------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 972 |
| 1 | SORT AGGREGATE | | 1 | |
|* 2 | INDEX RANGE SCAN| SCALE_SLOW | 3000 | 972 |
------------------------------------------------------
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.
Important
Les prédicats de filtre sont comme des explosifs non déclenchés. Ils
peuvent exploser à tout moment.
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 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.
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.
20 20
15 15
10 10
5 5
0 0
0 5 10 15 20 25
Charge [requêtes en parallèle]
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.
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.
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]
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.
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
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.
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.
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.
Java
L’exemple JPA utilise l’interface CriteriaBuilder.
Perl
L’exemple Perl suivant utilise l’outil DBIx::Class :
my @employes =
$schema->resultset('Employes')
->search({'UPPER(nom)' => {-like=>'WIN%'}});
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
}
}
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"/>
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.
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);
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 ?
Criteria c = session.createCriteria(Employes.class);
c.add(Restrictions.ilike("nom", 'Win%'));
c.setFetchMode("ventes", FetchMode.JOIN);
c.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY);
Perl
L’exemple suivant utilise l’outil DBIx::Class de Perl :
my @employes =
$schema->resultset('Employes')
->search({ 'UPPER(nom)' => {-like => 'WIN%'}
, {prefetch => ['ventes']}
});
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();
---------------------------------------------------------------
|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"(+))
Astuce
Connaissez votre ORM et gagnez le contrôle sur les jointures.
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.
--------------------------------------------------------------
| 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|
--------------------------------------------------------------
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.
---------------------------------------------------------------
| 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)
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.
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.
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;
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/);
1
https://en.wikipedia.org/wiki/Aliasing_%28computing%29
my @ventes =
$schema->resultset('ParentVentes')
->search($cond
,{ join => 'employe'
,'+columns' => ['employe.prenom'
,'employe.nom']
}
);
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);
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.
Avertissement
MySQL ne connaît pas les jointures par hachage (il s’agit de la
demande de fonctionnalité 59025).
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é.
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).
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.
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 :
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.
--------------------------------------------------------------
|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 |
--------------------------------------------------------------
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.
----------------------------------------------------------------
|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%')
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.
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 :
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.
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.
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.
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 > ?;
---------------------------------------------------------------
|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)
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.
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.
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.
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.
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 ?
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.
key length is … bytes. ». L’exemple suivant crée un index sur les dix
premiers caractères de la colonne NOM.
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.
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.
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.
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
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).
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
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.
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.
SELECT valeur_eur
FROM ventes_iot
WHERE date_vente = ?;
----------------------------------------------------
| Id | Operation | Name | Cost |
----------------------------------------------------
| 0 | SELECT STATEMENT | | 13 |
|* 1 | INDEX UNIQUE SCAN| VENTES_IOT_PK | 13 |
|* 2 | INDEX RANGE SCAN| VENTES_DATE_IOT | 4 |
----------------------------------------------------
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.
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;
PostgreSQL
PostgreSQL utilise seulement les tables standards.
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.
CREATE TABLE (
id NUMBER NOT NULL,
[...]
CONSTRAINT pk PRIMARY KEY NONCLUSTERED (id)
);
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.
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.
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.
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 :
------------------------------------------------------------------
|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 |
------------------------------------------------------------------
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.
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.
DATE_VENTE ID_PRODUIT
3 jours
auparavant
2 jours
auparavant
Intervalle
hier parcouru
de l'index
aujourd'hui
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.
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.
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.
-----------------------------------------------------------------
|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 |
-----------------------------------------------------------------
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.
3 jours
auparavant
2 jours
auparavant
hier Intervalle
parcouru
de l'index
aujourd'hui
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.
3 jours
auparavant
2 jours
auparavant
Saut
hier impossible
dans l'index
aujourd'hui
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 :
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.
-----------------------------------------------------------------
|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 |
-----------------------------------------------------------------
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.
DATE_VENTE ID_PRODUIT
3 jours
auparavant
2 jours
auparavant
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.
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.
QL
er
eS
rv
QL
Se
le
gr
te
ac
Li
st
yS
2
L
DB
SQ
SQ
Po
Or
M
Order by ASC/DESC
Index ASC/DESC
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 :
------------------------------------------------------------------
|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 |
------------------------------------------------------------------
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.
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 ?
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.
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.
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;
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;
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 |
----------------------------------------------------------------
Astuce
L’Annexe A, « Plans d’exécution » résume les opérations correspon-
dantes pour MySQL, Oracle, PostgreSQL et SQL Server.
Important
Une requête top-N en pipeline n’a pas besoin de lire et trier
l’ensemble complet des résultats.
---------------------------------------------------
| 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-
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;
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.
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;
3 jours
auparavant
Page 3
2 jours
auparavant
Page 2
hier
Page 1
aujourd'hui
Résultat Décalage
SELECT *
FROM ventes
WHERE date_vente < ?
ORDER BY date_vente DESC
FETCH FIRST 10 ROWS ONLY;
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.
SELECT *
FROM ventes
WHERE (date_vente, id_vente) < (?, ?)
ORDER BY date_vente DESC, id_vente DESC
FETCH FIRST 10 ROWS ONLY;
"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
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.
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;
---------------------------------------------------------------
|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.
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
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
WHERE (
(date_vente < ?)
OR
(date_vente = ? AND id_vente < ?)
)
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;
---------------------------------------------------------------
|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 |
---------------------------------------------------------------
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.
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.
0.10 0.10
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
Note
La plus grosse différence est observée dès le premier index.
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.
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 ?
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.
0.12 0.12
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
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.
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.
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.
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).
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 ?
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).
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.
$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.
Encore une fois, si cette requête ne fonctionne pas pour vous, demandez
l’aide de votre administrateur de bases de données.
--------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)|.
--------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 2 | 2 (0)|.
| 1 | TABLE ACCESS FULL| DUAL | 1 | 2 | 2 (0)|.
--------------------------------------------------------------
Opérations
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 ».
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 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.
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.
Requêtes Top-N
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 ».
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.
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)
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.
PostgreSQL
Les méthodes décrites dans cette section s’appliquent à PostgreSQL 8.0 et
aux versions suivantes.
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;
QUERY PLAN
------------------------------------------
Result (cost=0.00..0.01 rows=1 width=0)
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.
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.
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é.
DEALLOCATE req;
Opérations
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 ».
Jointures
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.
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
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.
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.
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.
Le select suivant filtre sur la colonne ID2 qui n’est pas inclus 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.
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)
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.
SQL Server
La méthode décrite dans cette section s’applique à SQL Server Management
Studio 2005 ainsi qu’aux versions ultérieures.
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.
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 :
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 :
Opérations
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.
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.
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.
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.
SELECT count(*)
FROM donnees
WHERE section = @sec
AND id2 = @id2
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.
SELECT count(*)
FROM donnees
WHERE section = @sec
AND id2 = @id2
|--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]))
MySQL
La méthode décrite dans cette section s’applique à toutes les versions de
MySQL.
EXPLAIN SELECT 1;
~+-------+------+---------------+------+~+------+------------~
~| table | type | possible_keys | key |~| rows | Extra
~+-------+------+---------------+------+~+------+------------~
~| NULL | NULL | NULL | NULL |~| NULL | No tables...
~+-------+------+---------------+------+~+------+------------~
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.
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.
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
les deux premières colonnes de l’index sont utilisées en tant que prédicat
d’accès.
EXPLAIN
SELECT *
FROM demo
WHERE id1=1
AND id3=1;
+------+----------+---------+------+-----------------------+
| type | key | key_len | rows | Extra |
+------+----------+---------+------+-----------------------+
| ref | demo_idx | 6 | 1 | Using index condition |
+------+----------+---------+------+-----------------------+
+------+----------+---------+------+-------------+
| type | key | key_len | rows | Extra |
+------+----------+---------+------+-------------+
| ref | demo_idx | 6 | 1 | Using where |
+------+----------+---------+------+-------------+
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
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