Guide SQL: comment mieux Ă©crire des requĂȘtes (partie 2)

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.

Source: https://habr.com/ru/post/fr465975/


All Articles