Article suivant
Guide SQL: comment mieux Ă©crire des requĂȘtes (partie 1)De la demande aux plans d'exĂ©cution

Sachant que les antipatterns ne sont pas statiques et Ă©voluent au fur et Ă mesure que vous grandissez en tant que dĂ©veloppeur SQL, et le fait qu'il y a beaucoup de choses Ă considĂ©rer lorsque vous pensez Ă des alternatives signifie Ă©galement qu'il est assez difficile d'Ă©viter les antipatterns et les requĂȘtes de réécriture tĂąche. Toute aide peut ĂȘtre utile, c'est pourquoi une approche plus structurĂ©e de l'optimisation des requĂȘtes Ă l'aide de certains outils peut ĂȘtre la plus efficace.
Il convient également de noter que certains des anti-modÚles mentionnés dans la derniÚre section sont enracinés dans des problÚmes de performances, tels que les opérateurs
AND
,
OR
et
NOT
et leur absence lors de l'utilisation des index. Penser à la performance nécessite non seulement une approche plus structurée, mais aussi plus profonde.
Cependant, cette approche structurĂ©e et approfondie sera principalement basĂ©e sur le plan de requĂȘte, qui, comme vous vous en souvenez, est le rĂ©sultat d'une requĂȘte d'abord analysĂ©e dans un "arbre d'analyse" ou "arbre d'analyse" et dĂ©termine exactement quel algorithme utilisĂ© pour chaque opĂ©ration et comment leur exĂ©cution est coordonnĂ©e.
Optimisation des requĂȘtes
Comme vous le lisez dans l'introduction, vous devrez peut-ĂȘtre vĂ©rifier et configurer des plans qui sont compilĂ©s manuellement par l'optimiseur. Dans de tels cas, vous devrez analyser Ă nouveau votre demande en consultant le plan de demande.
Pour accĂ©der Ă ce plan, vous devez utiliser les outils fournis par le systĂšme de gestion de base de donnĂ©es. Les outils suivants peuvent ĂȘtre Ă votre disposition:
- Certains packages contiennent des outils qui gĂ©nĂšrent une reprĂ©sentation graphique du plan de requĂȘte. Prenons l'exemple suivant:

- D'autres outils fourniront une description textuelle du plan de requĂȘte. Un exemple est l'instruction
EXPLAIN PLAN
dans Oracle, mais le nom de l'instruction dépend du SGBD avec lequel vous travaillez. Ailleurs, vous pouvez trouver EXPLAIN
(MySQL, PostgreSQL) ou EXPLAIN QUERY PLAN
(SQLite).
Veuillez noter que lorsque vous travaillez avec PostgreSQL, vous pouvez faire une distinction entre
EXPLAIN
, oĂč vous obtenez simplement une description qui indique comment le planificateur a l'intention d'exĂ©cuter la requĂȘte sans l'exĂ©cuter, tandis
EXPLAIN ANALYZE
exĂ©cute rĂ©ellement la requĂȘte et vous renvoie l'analyse plans de demande attendus et rĂ©els. De maniĂšre gĂ©nĂ©rale, un vĂ©ritable plan d'exĂ©cution est un plan dans lequel une demande est rĂ©ellement exĂ©cutĂ©e, tandis qu'un plan d'exĂ©cution d'Ă©valuation dĂ©termine ce qu'il fera sans rĂ©pondre Ă la demande. Bien que cela soit logiquement Ă©quivalent, le plan d'exĂ©cution rĂ©el est beaucoup plus utile car il contient des informations et des statistiques supplĂ©mentaires sur ce qui s'est rĂ©ellement passĂ© lors de l'exĂ©cution de la demande.
Dans le reste de cette section, vous en apprendrez plus sur
EXPLAIN
et
ANALYZE
, ainsi que sur la façon de les utiliser pour obtenir plus d'informations sur le plan de requĂȘte et ses performances possibles. Pour ce faire, commencez par quelques exemples dans lesquels vous allez travailler avec deux tables:
one_million
et
half_million
.
Vous pouvez obtenir les informations actuelles de la table
one_million
utilisant
EXPLAIN
; Assurez-vous de le placer directement au-dessus de la demande, et aprĂšs l'avoir exĂ©cutĂ©, il vous renverra le plan de requĂȘte:
EXPLAIN SELECT * FROM one_million; QUERY PLAN ____________________________________________________ Seq Scan on one_million (cost=0.00..18584.82 rows=1025082 width=36) (1 row)
Dans ce cas, vous voyez que le coût de la demande est
0.00..18584.82
et le nombre de lignes est
1025082
. La largeur du nombre de colonnes est de
36
.
De plus, vous pouvez mettre Ă jour les statistiques Ă l'aide d'
ANALYZE
.
ANALYZE one_million; EXPLAIN SELECT * FROM one_million; QUERY PLAN ____________________________________________________ Seq Scan on one_million (cost=0.00..18334.00 rows=1000000 width=37) (1 row)
Outre
EXPLAIN
et
ANALYZE
, vous pouvez également obtenir le temps d'exécution réel avec
EXPLAIN ANALYZE
:
EXPLAIN ANALYZE SELECT * FROM one_million; QUERY PLAN ___________________________________________________________ Seq Scan on one_million (cost=0.00..18334.00 rows=1000000 width=37) (actual time=0.015..1207.019 rows=1000000 loops=1) Total runtime: 2320.146 ms (2 rows)
L'inconvénient d'utiliser
EXPLAIN ANALYZE
est que la requĂȘte est rĂ©ellement exĂ©cutĂ©e, alors faites attention Ă cela!
Jusqu'à présent, tous les algorithmes que vous avez vus sont le
Seq Scan
séquentiel (
Seq Scan
sĂ©quentiel) ou le scan complet de la table: il s'agit d'un scan effectuĂ© dans une base de donnĂ©es oĂč chaque ligne de la table scannĂ©e est lue dans l'ordre sĂ©rie et les colonnes trouvĂ©es sont vĂ©rifiĂ©es pour respect de la condition ou non. En termes de performances, les analyses sĂ©quentielles ne sont certainement pas le meilleur plan d'exĂ©cution car vous effectuez toujours une analyse complĂšte de la table. Cependant, ce n'est pas si mal quand la table ne tient pas en mĂ©moire: les lectures sĂ©quentielles sont assez rapides mĂȘme sur des disques lents.
Vous en apprendrez plus Ă ce sujet plus tard lorsque nous parlerons de l'analyse d'index.
Cependant, il existe d'autres algorithmes. Prenons, par exemple, ce plan de requĂȘte pour une connexion:
EXPLAIN ANALYZE SELECT * FROM one_million JOIN half_million ON (one_million.counter=half_million.counter); QUERY PLAN _________________________________________________________________ Hash Join (cost=15417.00..68831.00 rows=500000 width=42) (actual time=1241.471..5912.553 rows=500000 loops=1) Hash Cond: (one_million.counter = half_million.counter) -> Seq Scan on one_million (cost=0.00..18334.00 rows=1000000 width=37) (actual time=0.007..1254.027 rows=1000000 loops=1) -> Hash (cost=7213.00..7213.00 rows=500000 width=5) (actual time=1241.251..1241.251 rows=500000 loops=1) Buckets: 4096 Batches: 16 Memory Usage: 770kB -> Seq Scan on half_million (cost=0.00..7213.00 rows=500000 width=5) (actual time=0.008..601.128 rows=500000 loops=1) Total runtime: 6468.337 ms
Vous voyez que l'optimiseur de requĂȘtes a choisi
Hash Join
here! N'oubliez pas cette opération, car vous en aurez besoin pour évaluer la complexité temporelle de votre demande. Pour l'instant, notez qu'il n'y a pas d'index dans
half_million.counter
, que nous ajoutons dans l'exemple suivant:
CREATE INDEX ON half_million(counter); EXPLAIN ANALYZE SELECT * FROM one_million JOIN half_million ON (one_million.counter=half_million.counter); QUERY PLAN ________________________________________________________________ Merge Join (cost=4.12..37650.65 rows=500000 width=42) (actual time=0.033..3272.940 rows=500000 loops=1) Merge Cond: (one_million.counter = half_million.counter) -> Index Scan using one_million_counter_idx on one_million (cost=0.00..32129.34 rows=1000000 width=37) (actual time=0.011..694.466 rows=500001 loops=1) -> Index Scan using half_million_counter_idx on half_million (cost=0.00..14120.29 rows=500000 width=5) (actual time=0.010..683.674 rows=500000 loops=1) Total runtime: 3833.310 ms (5 rows)
Vous voyez qu'en crĂ©ant l'index, l'optimiseur de requĂȘte a maintenant dĂ©cidĂ© d'utiliser la
Merge join
lors de l'analyse de l'
Index Scan
index.
Notez la différence entre les analyses d'index et les analyses de table complÚtes ou analyses séquentielles: la premiÚre, également appelée «analyses de table», analyse les données ou les pages d'index pour trouver les enregistrements correspondants, tandis que la seconde analyse chaque ligne de la table.
Vous voyez que le temps d'exĂ©cution global a diminuĂ© et que les performances devraient ĂȘtre meilleures, mais il existe deux analyses d'index, ce qui rend la mĂ©moire plus importante ici, surtout si la table ne tient pas dedans. Dans de tels cas, vous devez d'abord effectuer une analyse complĂšte de l'index, qui est effectuĂ©e Ă l'aide de lectures sĂ©quentielles rapides et n'est pas un problĂšme, mais vous avez ensuite de nombreuses opĂ©rations de lecture alĂ©atoire pour sĂ©lectionner des lignes par valeur d'index. Ce sont des opĂ©rations de lecture alĂ©atoire qui sont gĂ©nĂ©ralement plus lentes de plusieurs ordres de grandeur que les opĂ©rations sĂ©quentielles. Dans ces cas, une analyse complĂšte de la table se produit en effet plus rapidement qu'une analyse complĂšte de l'index.
Astuce: Si vous souhaitez en savoir plus sur EXPLAIN ou examiner des exemples plus en détail, pensez à lire
Understanding Explain de Guillaume Lelarge.
Complexité temporelle et Big O
Maintenant que vous avez briĂšvement examinĂ© le plan de requĂȘte, vous pouvez commencer Ă approfondir et Ă rĂ©flĂ©chir aux performances de maniĂšre plus formelle en utilisant la thĂ©orie de la complexitĂ© de calcul. Il s'agit d'un domaine de l'informatique thĂ©orique, qui, entre autres, se concentre sur la classification des problĂšmes de calcul en fonction de leur complexitĂ©; Ces problĂšmes de calcul peuvent ĂȘtre des algorithmes, mais aussi des requĂȘtes.
Cependant, pour les requĂȘtes, elles ne sont pas nĂ©cessairement classĂ©es en fonction de leur complexitĂ©, mais plutĂŽt en fonction du temps nĂ©cessaire pour les complĂ©ter et obtenir des rĂ©sultats. C'est ce qu'on appelle la complexitĂ© temporelle, et vous pouvez utiliser la grande notation O pour formuler ou mesurer ce type de complexitĂ©.
Avec la dĂ©signation big O, vous exprimez le temps d'exĂ©cution en termes de vitesse de croissance par rapport Ă l'entrĂ©e, car l'entrĂ©e devient arbitrairement grande. La grande notation O exclut les coefficients et les membres d'un ordre infĂ©rieur, vous pouvez donc vous concentrer sur la partie importante du temps d'exĂ©cution de votre requĂȘte: son taux de croissance. Lorsqu'ils sont exprimĂ©s de cette maniĂšre, en rejetant les coefficients et les termes d'un ordre infĂ©rieur, ils disent que la complexitĂ© temporelle est dĂ©crite asymptotiquement. Cela signifie que la taille d'entrĂ©e va Ă l'infini.
Dans un langage de base de donnĂ©es, la complexitĂ© dĂ©termine le temps nĂ©cessaire pour terminer une requĂȘte Ă mesure que la taille des tables de donnĂ©es et donc la base de donnĂ©es grandit.
Veuillez noter que la taille de votre base de données augmente non seulement à cause de l'augmentation de la quantité de données dans les tables, mais le fait qu'il existe des index joue également un rÎle dans la taille.
Estimation de la complexitĂ© temporelle de votre plan de requĂȘte
Comme vous l'avez vu prĂ©cĂ©demment, le plan d'exĂ©cution, entre autres, dĂ©termine quel algorithme est utilisĂ© pour chaque opĂ©ration, ce qui vous permet d'exprimer logiquement chaque temps d'exĂ©cution de requĂȘte en fonction de la taille de la table incluse dans le plan de requĂȘte, appelĂ©e fonction de complexitĂ©. En d'autres termes, vous pouvez utiliser la notation O et le plan d'exĂ©cution pour Ă©valuer la complexitĂ© et les performances des requĂȘtes.
Dans les sections suivantes, vous obtiendrez un aperçu des quatre types de complexitĂ© temporelle et vous verrez quelques exemples de la façon dont la complexitĂ© temporelle des requĂȘtes peut varier en fonction du contexte dans lequel elle est exĂ©cutĂ©e.
Astuce: les index font partie de cette histoire!
Cependant, il convient de noter qu'il existe différents types d'index, différents plans d'exécution et différentes implémentations pour différentes bases de données, de sorte que les difficultés temporaires répertoriées ci-dessous sont trÚs générales et peuvent varier en fonction de paramÚtres spécifiques.
O (1): Temps constant
Ils disent qu'un algorithme fonctionne en temps constant s'il a besoin du mĂȘme temps quelle que soit la taille des donnĂ©es d'entrĂ©e. Quand il s'agit d'une requĂȘte, elle sera exĂ©cutĂ©e en temps constant si le mĂȘme temps est requis quelle que soit la taille de la table.
Ce type de requĂȘte n'est pas vraiment courant, mais en voici un exemple:
SELECT TOP 1 t.* FROM t
La complexité temporelle est constante, car une ligne arbitraire est sélectionnée dans le tableau. Par conséquent, la durée ne doit pas dépendre de la taille de la table.
Temps linéaire: O (n)
Ils disent que l'algorithme fonctionne en temps linĂ©aire, si son temps d'exĂ©cution est directement proportionnel Ă la taille des donnĂ©es d'entrĂ©e, c'est-Ă -dire que le temps augmente linĂ©airement avec la taille des donnĂ©es d'entrĂ©e. Pour les bases de donnĂ©es, cela signifie que le temps d'exĂ©cution sera directement proportionnel Ă la taille de la table: Ă mesure que le nombre de lignes de la table augmente, le temps d'exĂ©cution de la requĂȘte augmente.
Un exemple est une requĂȘte avec une
WHERE
pour une colonne non indexée: une analyse complÚte de la table ou une analyse
Seq Scan
sera nĂ©cessaire, ce qui entraĂźnera une complexitĂ© temporelle O (n). Cela signifie que chaque ligne doit ĂȘtre lue afin de trouver la ligne avec l'identifiant (ID) souhaitĂ©. Vous n'avez aucune restriction, vous devez donc compter chaque ligne, mĂȘme si la premiĂšre ligne correspond Ă la condition.
ConsidĂ©rez Ă©galement l'exemple de requĂȘte suivant, qui aura une complexitĂ© O (n) s'il n'y a pas d'index sur le champ
i_id
:
SELECT i_id FROM item;
- Ce qui prĂ©cĂšde signifie Ă©galement que d'autres requĂȘtes, telles que des requĂȘtes pour calculer le nombre de lignes
COUNT (*) FROM TABLE;
aura une complexité temporelle O (n) , car une analyse complÚte de la table sera nécessaire car le nombre total de lignes n'a pas été enregistré pour la table. Sinon, la complexité temporelle serait similaire à O (1) .
Le runtime linéaire est étroitement lié au runtime des plans qui ont des jointures de table. Voici quelques exemples:
- La jointure de hachage a la complexité attendue de O (M + N). L'algorithme de jointure de hachage classique pour joindre en interne deux tables prépare d'abord la table de hachage de la plus petite table. Les entrées de la table de hachage se composent d'un attribut de connexion et de sa chaßne. La table de hachage est accessible en appliquant la fonction de hachage à l'attribut de connexion. Une fois la table de hachage créée, une grande table est analysée et les lignes correspondantes de la petite table sont trouvées en recherchant la table de hachage.
- Les jointures de fusion ont généralement une complexité O (M + N), mais cela dépendra fortement des index de colonne de jointure et, s'il n'y a pas d'index, du tri des lignes en fonction des clés utilisées dans la jointure:
- Si les deux tables sont triĂ©es en fonction des clĂ©s utilisĂ©es dans la jointure, la requĂȘte aura une complexitĂ© temporelle de O (M + N).
- Si les deux tables ont un index pour les colonnes jointes, alors l'index prend déjà en charge ces colonnes dans l'ordre et le tri n'est pas requis. La difficulté sera O (M + N).
- Si aucune des tables n'a d'index sur les colonnes connectées, vous devez d'abord trier les deux tables, afin que la complexité ressemble à O (M log M + N log N).
- Si une seule des tables a un index sur les colonnes connectĂ©es, seule la table qui n'a pas d'index doit ĂȘtre triĂ©e avant que l'Ă©tape de jointure se produise, de sorte que la complexitĂ© ressemble Ă O (M + N log N).
- Pour les jointures imbriquĂ©es, la complexitĂ© est gĂ©nĂ©ralement O (MN). Cette jointure est efficace lorsqu'une ou les deux tables sont extrĂȘmement petites (par exemple, moins de 10 enregistrements), ce qui est une situation trĂšs courante lors de l'Ă©valuation des requĂȘtes, car certaines sous-requĂȘtes sont Ă©crites pour renvoyer une seule ligne.
N'oubliez pas: une jointure imbriquée est une jointure qui compare chaque enregistrement d'une table avec chaque enregistrement d'une autre.
Temps logarithmique: O (log (n))
On dit qu'un algorithme fonctionne en temps logarithmique si son temps d'exĂ©cution est proportionnel au logarithme de la taille d'entrĂ©e; Pour les requĂȘtes, cela signifie qu'elles seront exĂ©cutĂ©es si le temps d'exĂ©cution est proportionnel au logarithme de la taille de la base de donnĂ©es.
Cette complexitĂ© temporelle logarithmique est valide pour les plans de requĂȘte dans lesquels une
Index Scan
ou un index cluster est analysé. Un index cluster est un index dont le niveau d'index final contient les lignes réelles de la table. Un index clusterisé est similaire à tout autre index: il est défini dans une ou plusieurs colonnes. Ils forment une clé d'index. La clé de clustering est les colonnes clés d'un index cluster. L'analyse d'un index cluster est essentiellement l'opération de lecture de votre SGBD pour une ou plusieurs lignes de haut en bas dans un index cluster.
Prenons l'exemple de requĂȘte suivant, oĂč il existe un index pour
i_id
et qui entraßne généralement une complexité O (log (n)):
SELECT i_stock FROM item WHERE i_id = N;
Notez que sans index, la complexité temporelle serait O (n).
Temps quadratique: O (n ^ 2)
On pense que l'algorithme est exĂ©cutĂ© en temps quadratique, si son temps d'exĂ©cution est proportionnel au carrĂ© de la taille d'entrĂ©e. Encore une fois, pour les bases de donnĂ©es, cela signifie que le temps d'exĂ©cution de la requĂȘte est proportionnel au carrĂ© de la taille de la base de donnĂ©es.
Un exemple possible d'une requĂȘte de complexitĂ© temporelle quadratique est le suivant:
SELECT * FROM item, author WHERE item.i_a_id=author.a_id
La complexitĂ© minimale peut ĂȘtre O (n log (n)), mais la complexitĂ© maximale peut ĂȘtre O (n ^ 2) sur la base des informations d'index des attributs de connexion.
Pour résumer, vous pouvez également consulter
la feuille de triche suivante pour Ă©valuer les performances des requĂȘtes en fonction de leur complexitĂ© temporelle et de leur efficacitĂ©:
Réglage SQL
Compte tenu du plan d'exĂ©cution des requĂȘtes et de la complexitĂ© temporelle, vous pouvez personnaliser davantage votre requĂȘte SQL. Vous pouvez commencer par vous concentrer sur les points suivants:
- Remplacez les analyses de table complĂštes inutiles par des analyses d'index;
- Assurez-vous que l'ordre de jointure optimal est appliqué.
- Assurez-vous que les index sont utilisés de maniÚre optimale. Et
- La mise en cache des analyses de texte intégral des petites tables (cache les analyses de table complÚte des petites tables) est utilisée.
Utilisation ultérieure de SQL
FĂ©licitations! Vous ĂȘtes arrivĂ© Ă la fin de cet article, qui vient de vous donner un petit aperçu des performances des requĂȘtes SQL. J'espĂšre que vous avez plus d'informations sur les antipatterns, l'optimiseur de requĂȘtes et les outils que vous pouvez utiliser pour analyser, Ă©valuer et interprĂ©ter la complexitĂ© de votre plan de requĂȘte. Mais il vous reste encore tant Ă dĂ©couvrir! Si vous voulez en savoir plus, lisez le livre «Database Management Systems» de R. Ramakrishnan et J. Gehrke.
Enfin, je ne veux pas vous refuser StackOverflow dans cette citation:
Mon antipattern préféré ne vérifie pas vos demandes.
Cependant, il est applicable lorsque:
- Votre requĂȘte fournit plusieurs tables.
- Vous pensez avoir la conception optimale pour la demande, mais n'essayez pas de vérifier vos hypothÚses.
- Vous acceptez la premiĂšre demande de travail, sans savoir Ă quel point elle est proche de l'optimum.