Index de couverture pour GiST

L'indice de couverture n'est pas seulement une autre fonctionnalitĂ© qui peut ĂȘtre utile. Cette chose est purement pratique. Sans eux, Index Only Scan ne peut pas gagner. Bien que l'indice de couverture dans diffĂ©rentes situations soit efficace de diffĂ©rentes maniĂšres.

Il ne s'agit pas vraiment de couvrir les index: Ă  proprement parler, les soi-disant index inclusifs sont apparus dans Postgres. Mais, dans l'ordre: un index de couverture est un index qui contient toutes les valeurs de colonne requises par la requĂȘte; cependant, l'accĂšs Ă  la table elle-mĂȘme n'est plus nĂ©cessaire. Presque. Vous pouvez lire sur «presque» et d'autres nuances dans un article de Yegor Rogov , inclus dans sa sĂ©rie d'index de 10 (!) Parties. Et l' index inclus est crĂ©Ă© spĂ©cifiquement pour la recherche sur des requĂȘtes typiques: les valeurs des champs qui ne peuvent pas ĂȘtre recherchĂ©s sont ajoutĂ©es Ă  l'index de recherche, elles sont nĂ©cessaires uniquement pour ne plus se rĂ©fĂ©rer Ă  la table. Ces index sont formĂ©s avec le mot clĂ© INCLUDE.

Anastasia Lubennikova (Postgres Professional) a finalisĂ© la mĂ©thode btree afin que des colonnes supplĂ©mentaires puissent ĂȘtre incluses dans l'index. Ce correctif Ă©tait inclus dans PostgreSQL 11. Mais les correctifs pour les mĂ©thodes d'accĂšs GiST / SP-GiST n'avaient pas eu le temps de mĂ»rir avant la sortie de cette version. Le 12 GiST a mĂ»ri.

Un désir constructif d'avoir des index inclusifs pour GiST est né il y a longtemps: un patch de test d'Andrey Borodin a été offert à la communauté à la mi-avril 2018. Il a fait tout le travail de base, trÚs difficile.

Début août 2019, Alexander Korotkov a ajouté des améliorations cosmétiques et a validé le patch.

À des fins de dĂ©monstration et de recherche, nous allons gĂ©nĂ©rer un ensemble de 3 millions de rectangles. Dans le mĂȘme temps, quelques mots sur le type de boĂźte, car toutes les manipulations avec lui ne sont pas intuitives.

Le type de boĂźte - c'est-Ă -dire le rectangle - a longtemps Ă©tĂ© dans Postgres, il est dĂ©fini par 2 points (le point de type gĂ©omĂ©trique) - les sommets opposĂ©s du rectangle (c'est-Ă -dire que le rectangle ne peut pas ĂȘtre oblique, jonchĂ© sur le cĂŽtĂ©). Nous lisons dans la documentation : «les valeurs de type box sont Ă©crites sous l'une des formes suivantes:

( ( x1 , y1 ) , ( x2 , y2 ) ) ( x1 , y1 ) , ( x2 , y2 ) x1 , y1 , x2 , y2 

En pratique, vous devez Ă©crire, par exemple, comme ceci:

 SELECT box('1,2', '3,4'); box ------------- (3,4),(1,2) (1 row) 

Tout d'abord, Postgres nous montre le sommet supérieur droit, puis le coin inférieur gauche. Si on écrit comme ça,

 SELECT box('5,2', '3,4'); box ------------- (5,4),(3,2) (1 row) 

alors nous nous assurerons que Postgres n'a pas donné les pics qu'ils lui ont donnés. Il a calculé le coin supérieur droit et inférieur gauche à partir de notre coin supérieur gauche et inférieur droit. Il s'agit d'une propriété pratique lorsque l'emplacement des sommets n'est pas connu à l'avance - en cas de génération aléatoire, par exemple. La notation «1,2», «3,4» équivaut au point (1,2), au point (3,4). Ce formulaire est parfois plus pratique.


Pour les entreprises: recherchez dans 3 millions de rectangles


 CREATE TABLE boxes(id serial, thebox box, name text); 

Nous générerons 3 millions de rectangles aléatoires. Nous voulons une distribution normale, mais pour ne pas utiliser l'extension tablefunc , nous utilisons l'approche «pauvre»: nous utilisons random () - random (), qui donne également une belle image (voir la figure) avec des rectangles, plus ils sont grands, plus ils sont proches du centre. Leurs centres de gravité sont également aléatoires. De telles distributions sont caractéristiques de certains types de données réelles sur les villes. Et ceux qui veulent se plonger dans les lois de la statistique ou rafraßchir la mémoire peuvent lire ici la différence des variables aléatoires.




 INSERT INTO boxes(thebox, name) SELECT box( point( random()-random(), random()-random() ), point( random()-random(), random()-random() ) ), 'box no.' || x FROM generate_series(1,3000000) AS g(x); 


La taille de la table qui affiche \dt+ est de 242 Mo. Vous pouvez maintenant lancer la recherche.

Nous recherchons sans index:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------------- Gather (cost=1000.00..47853.00 rows=3000 width=46) (actual time=0.140..246.998 rows=139189 loops=1) Workers Planned: 2 Workers Launched: 2 -> Parallel Seq Scan on boxes (cost=0.00..46553.00 rows=1250 width=46) (actual time=0.011..106.708 rows=46396 loops=3) Filter: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Rows Removed by Filter: 953604 Planning Time: 0.040 ms Execution Time: 259.262 ms (8 rows) 

Nous voyons qu'il y a un Parallel Seq Scan - balayage séquentiel (bien que parallélisé).

Créez un index régulier et non inclusif:

 CREATE INDEX ON boxes USING gist(thebox); 

La taille de l'index boxes_thebox_idx , qui affiche \di+ , 262 Mo. En rĂ©ponse Ă  la mĂȘme demande, nous obtenons:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------- Bitmap Heap Scan on boxes (cost=159.66..9033.30 rows=3000 width=46) (actual time=29.101..80.283 rows=139189 loops=1) Recheck Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Blocks: exact=30629 -> Bitmap Index Scan on boxes_thebox_idx (cost=0.00..158.91 rows=3000 width=0) (actual time=25.029..25.029 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Planning Time: 0.053 ms Execution Time: 86.206 ms (7 rows) 

Le temps de recherche a été réduit d'un facteur trois, et au lieu de Parallel Seq Scan, ils ont reçu un Bitmap Index Scan. Il ne se parallélise pas, mais il fonctionne plus rapidement.

Maintenant, tuez l'ancien index et créez-en un:

 CREATE INDEX ON boxes USING spgist(thebox) INCLUDE(name); 

Index de boxes_thebox_name_idx plus gros: 356MB. C'est parti:


 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN ------------------------------------------------------------------------------ Bitmap Heap Scan on boxes (cost=207.66..9081.30 rows=3000 width=46) (actual time=86.822..152.014 rows=139189 loops=1) Recheck Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Blocks: exact=30629 -> Bitmap Index Scan on boxes_thebox_name_idx (cost=0.00..206.91 rows=3000 width=0) (actual time=83.044..83.044 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Planning Time: 3.807 ms Execution Time: 157.997 ms (7 rows) 


Index Only Scan est utilisé, mais l'image est triste: le temps est presque 2 fois plus long que sans lui. Nous lisons le manuel du créateur d'indices, partie I :

â€čLes index PostgreSQL rang ne contiennent pas d'informations permettant de juger de la visibilitĂ© des lignes. Par consĂ©quent, la mĂ©thode d'accĂšs renvoie toutes les versions des lignes qui relĂšvent de la condition de recherche, qu'elles soient visibles ou non pour la transaction en cours. Cependant, si le mĂ©canisme d'indexation devait examiner la table Ă  chaque fois pour dĂ©terminer la visibilitĂ©, cette mĂ©thode d'analyse ne serait pas diffĂ©rente de l'analyse d'index ordinaire. Le problĂšme est rĂ©solu par le fait que PostgreSQL prend en charge la soi-disant carte de visibilitĂ© des tables, dans laquelle le processus de vide marque les pages dans lesquelles les donnĂ©es n'ont pas changĂ© suffisamment longtemps pour que toutes les transactions puissent les voir, quels que soient l'heure de dĂ©but et le niveau d'isolement. Si l'identifiant de la ligne renvoyĂ©e par l'index fait rĂ©fĂ©rence Ă  une telle page, alors la visibilitĂ© ne peut pas ĂȘtre vĂ©rifiĂ©e. â€șâ€ș

Nous faisons le VIDE. Répétez:

 EXPLAIN ANALYZE SELECT thebox, name FROM boxes WHERE thebox @> box('0.5, 0.4','0.3, 0.2'); QUERY PLAN -------------------------------------------------------------------------------- Index Only Scan using boxes_thebox_name_idx on boxes (cost=0.41..236.91 rows=3000 width=46) (actual time=0.104..38.651 rows=139189 loops=1) Index Cond: (thebox @> '(0.5,0.4),(0.3,0.2)'::box) Heap Fetches: 0 Planning Time: 0.052 ms Execution Time: 44.337 ms (5 rows) 

Une toute autre affaire! Deux fois le gain par rapport Ă  l'indice non inclus.


Sélectivité et gain


Les performances des index inclusifs dĂ©pendent fortement de la sĂ©lectivitĂ© des conditions dans les requĂȘtes. Pour Ă©tudier un peu cette dĂ©pendance, nous allons rĂ©soudre le problĂšme inverse: nous allons gĂ©nĂ©rer une Ă©tiquette avec un index de type point et nous chercherons combien de points tomberont dans la case donnĂ©e. RĂ©partissez les points de maniĂšre uniforme au carrĂ©.

 CREATE TABLE test_covergist(id serial, tochka point, name text); 

 INSERT INTO test_covergist(tochka, name) SELECT point(trunc(1000000*random()), trunc(1000000*random())), 'point no.' || gx FROM generate_series(1,3000000) AS g(x); 

La taille de la table est de 211 Mo.

 CREATE INDEX on test_covergist USING gist(tochka); 

Taille 213 Mo.

Nous allons évidemment mettre tous les points disponibles dans un carré:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=1087.964..1864.059 rows=3000000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=27025 Buffers: shared read=54287 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=1084.949..1084.949 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared read=27262 Planning Time: 0.102 ms Execution Time: 2029.501 ms (9 rows) 

Nous avons demandé à EXPLAIN de montrer les tampons. Cela vous sera utile. Maintenant, le temps d'exécution de la demande est supérieur à 2 secondes, on peut voir que Buffers: shared read = 54287. Dans une autre situation, nous pourrions voir un mélange de lecture partagée et de hit partagé - c'est-à-dire que certains tampons sont lus à partir du disque (ou du cache du systÚme d'exploitation), et certains à partir du cache de tampon. Nous connaissons la taille approximative de la table et des index, nous nous protégerons donc en définissant des tampons partagés pour que tout rentre - redémarrez Postgres avec l'option

 -o "-c shared_buffers=1GB" 

Maintenant:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=231.032..613.326 rows=3000000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=27025 Buffers: shared hit=54248 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=228.068..228.068 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared hit=27223 Planning Time: 0.070 ms Execution Time: 755.915 ms (9 rows) 

Autrement dit, la lecture partagée est devenue un succÚs partagé et le temps a été réduit trois fois.

Un autre détail important dans EXPLAIN: 3 millions de points sont retournés, et la prévision du nombre retourné d'enregistrements est de 3 000. Spoiler: ce nombre ne changera avec aucune sélectivité. L'optimiseur ne sait pas comment évaluer la cardinalité lors de l'utilisation de types de boßtes ou de points. Et le plan ne changera pas: pour toute taille de rectangle, il y aura un scan d'index bitmap sur test_covergist_tochka_idx.

Voici deux autres mesures avec le nombre d'enregistrements émis, différents par ordre de grandeur:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN --------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=27.889..134.054 rows=269882 loops=1) Recheck Cond: ('(300000,300000),(0,0)'::box @> tochka) Heap Blocks: exact=27024 Buffers: shared hit=29534 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=24.847..24.847 rows=269882 loops=1) Index Cond: (tochka <@ '(300000,300000),(0,0)'::box) Buffers: shared hit=2510 Planning Time: 0.074 ms Execution Time: 151.269 ms (9 rows) 

Il renvoie 10 fois moins d'enregistrements (réels ... lignes = 269882), le temps a diminué d'environ 5 fois.

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','30000,30000') @> tochka; QUERY PLAN ---------------------------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist (cost=135.66..8778.83 rows=3000 width=32) (actual time=1.882..16.095 rows=2780 loops=1) Recheck Cond: ('(30000,30000),(0,0)'::box @> tochka) Heap Blocks: exact=2624 Buffers: shared hit=2655 -> Bitmap Index Scan on test_covergist_tochka_idx (cost=0.00..134.91 rows=3000 width=0) (actual time=1.035..1.035 rows=2780 loops=1) Index Cond: (tochka <@ '(30000,30000),(0,0)'::box) Buffers: shared hit=31 Planning Time: 0.154 ms Execution Time: 16.702 ms (9 rows) 

Le contenu d'un carrĂ© de 30K × 30K (2780) est comptĂ© en seulement 16 ms. Et quand il y a des dizaines d'enregistrements, ils sont dĂ©jĂ  extraits en fractions de ms, et de telles mesures ne sont pas trĂšs fiables.

Enfin, mesurez la mĂȘme chose avec l'indice inclusif:

 CREATE INDEX on test_covergist USING gist(tochka) INCLUDE(name); 

Taille 316 Mo.

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN --------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.160..568.707 rows=3000000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=40492 Planning Time: 0.090 ms Execution Time: 709.837 ms (6 rows) 

Le temps est presque le mĂȘme qu'avec un index conventionnel, bien qu'Index Only Scan.

Mais:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN -------------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.083..53.277 rows=269882 loops=1) Index Cond: (tochka <@ '(300000,300000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=3735 Planning Time: 0.077 ms Execution Time: 66.162 ms (6 rows) 

Et c'était 151 ms. Et, en conséquence:

 EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist WHERE box('0,0','300000,300000') @> tochka; QUERY PLAN -------------------------------------------------------------------------------------------- Index Only Scan using test_covergist_tochka_name_idx on test_covergist (cost=0.41..216.91 rows=3000 width=32) (actual time=0.043..0.639 rows=2780 loops=1) Index Cond: (tochka <@ '(30000,30000),(0,0)'::box) Heap Fetches: 0 Buffers: shared hit=52 Planning Time: 0.053 ms Execution Time: 0.791 ms (6 rows) 

C'est dĂ©jĂ  une fraction de ms pour les mĂȘmes enregistrements de 2780 points.

Des tampons comme des fusils


Une explication peut ĂȘtre recherchĂ©e et trouvĂ©e dans un fusil de chasse qui n'a pas encore tirĂ© mais qui Ă©tait accrochĂ© au mur: le nombre de blocs lus. Dans le cas d'un index inclusif, seuls les blocs de l'index lui-mĂȘme sont lus (Heap Fetches: 0). Dans trois cas, il s'agissait des numĂ©ros 40492, 3735 et 52. Mais lors de l'utilisation de l'index normal, les blocs lus consistent en la somme des bits lus dans l'index Bitmap Heap Scan (54248 avec 3 millions d'enregistrements) et ceux qui ont dĂ» ĂȘtre lus Ă  partir du tas (27223). , car le champ de nom ne peut pas ĂȘtre extrait d'un index standard. 54248 + 27223 = 81471. L'exclusivitĂ© Ă©tait 40492. Pour deux autres cas: 29534 + 2510 = 31044 et 2655 + 31 = 2686. Dans le cas d'un index rĂ©gulier, plus de blocs sont lus de toute façon, mais avec une amĂ©lioration de la sĂ©lectivitĂ©, le nombre de blocs lus commence Ă  diffĂ©rer par des ordres de grandeur plutĂŽt que 2 fois en raison du fait que le nombre de blocs nĂ©cessaires d'un tas diminue plus lentement que la lecture de blocs d'index.

Nombre total d'enregistrements retournés (en milliers)30002702.7
Lire les blocs (Normal / Inclus)81471/4049231044/37352686/52
Le temps755/710151/6616 / 0,7


Mais peut-ĂȘtre que le point n'est pas du tout la sĂ©lectivitĂ©, mais simplement la taille de la table? Juste au cas oĂč, nous rĂ©pĂ©tons les mĂȘmes Ă©tapes, gĂ©nĂ©rant une table avec 300 000, et non 3 millions d'enregistrements:

 CREATE TABLE test_covergist_small(id serial, tochka point, name text); INSERT INTO test_covergist_small(tochka, name) SELECT point(trunc(1000000*random()), trunc(1000000*random())), 'point no.' || gx FROM generate_series(1,300000) AS g(x); CREATE INDEX ON test_covergist_small USING gist(tochka); EXPLAIN (ANALYZE, buffers) SELECT tochka, name FROM test_covergist_small WHERE box('0,0','3000000,3000000') @> tochka; QUERY PLAN ---------------------------------------------------------------------------- Bitmap Heap Scan on test_covergist_small (cost=14.61..867.19 rows=300 width=31) (actual time=36.115..130.435 rows=300000 loops=1) Recheck Cond: ('(3000000,3000000),(0,0)'::box @> tochka) Heap Blocks: exact=2500 Buffers: shared hit=5225 -> Bitmap Index Scan on test_covergist_small_tochka_idx (cost=0.00..14.53 rows=300 width=0) (actual time=35.894..35.895 rows=300000 loops=1) Index Cond: (tochka <@ '(3000000,3000000),(0,0)'::box) Buffers: shared hit=2725 Planning Time: 0.060 ms Execution Time: 158.580 (9 rows) 

Ensuite, rĂ©pĂ©tez la mĂȘme chose pour l'index inclusif. Voici les rĂ©sultats:

Nombre total d'enregistrements retournés (en milliers)300270,25
Lire les blocs (Normal / Inclus)5225/37263026/352270/8
Le temps158/17820/130,4 / 0,2


Dans le cas d'une couverture Ă  100% des points, la requĂȘte Ă©tait mĂȘme un peu plus lente qu'avec l'index habituel. De plus, comme dans le cas de 3 millions, tout s'est mis en place. Autrement dit, la sĂ©lectivitĂ© est importante.

Notre sociĂ©tĂ© a testĂ© des indices GiST inclusifs sur des donnĂ©es rĂ©elles - un ensemble de plusieurs millions de rectangles sur une carte de Moscou. La conclusion est la mĂȘme: dans de nombreuses situations, de tels index accĂ©lĂšrent sensiblement les requĂȘtes. Mais l'article ne peut pas ĂȘtre illustrĂ© par des images et des nombres de tests: ces donnĂ©es ne sont pas du domaine public.

Au lieu d'une conclusion


Revenons un instant aux rectangles alĂ©atoires. Essayons de faire de mĂȘme avec spgist. Vous pouvez vous rappeler ou dĂ©couvrir ce que c'est que de comprendre les diffĂ©rences entre SP-GiST et GiST en lisant l'article Indexes dans PostgreSQL - 6 . CrĂ©ez un index inclusif:

 CREATE INDEX ON boxes USING spgist(thebox) INCLUDE(name); ERROR: access method "spgist" does not support included columns 

Hélas, pour SP-GiST, les index inclusifs ne sont pas encore implémentés.
Il y a donc place à amélioration!



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


All Articles