Titelindizes fĂŒr GiST

Der Deckungsindex ist nicht nur eine weitere nĂŒtzliche Funktion. Dieses Ding ist rein praktisch. Ohne sie kann Index Only Scan möglicherweise keinen Gewinn bringen. Obwohl der Deckungsindex in verschiedenen Situationen auf unterschiedliche Weise wirksam ist.

Hier geht es nicht wirklich darum, Indizes abzudecken: Genau genommen sind in Postgres die sogenannten Inklusivindizes erschienen. Aber in der Reihenfolge: Ein Deckungsindex ist ein Index, der alle fĂŒr die Abfrage erforderlichen Spaltenwerte enthĂ€lt. Der Zugriff auf die Tabelle selbst ist jedoch nicht mehr erforderlich. Fast. In einem Artikel von Yegor Rogov , der in seiner Indexreihe mit 10 (!) Teilen enthalten ist, können Sie ĂŒber „fast“ und andere Nuancen lesen . Der Inklusivindex wurde speziell fĂŒr die Suche in typischen Abfragen erstellt: Die Werte von Feldern, die nicht durchsucht werden können, werden dem Suchindex hinzugefĂŒgt. Sie werden nur benötigt, um nicht erneut auf die Tabelle zu verweisen. Solche Indizes werden mit dem SchlĂŒsselwort INCLUDE gebildet.

Anastasia Lubennikova (Postgres Professional) hat die btree-Methode fertiggestellt, damit zusĂ€tzliche Spalten in den Index aufgenommen werden können. Dieser Patch war in PostgreSQL 11 enthalten. Die Patches fĂŒr die GiST / SP-GiST-Zugriffsmethoden hatten jedoch vor der Veröffentlichung dieser Version keine Zeit zum Reifen. Bis zum 12. GiST reifte.

Der konstruktive Wunsch nach integrativen Indizes fĂŒr GiST entstand vor langer Zeit: Mitte April 2018 wurde der Community ein Test-Patch von Andrey Borodin angeboten . Er hat alle grundlegenden, sehr schwierigen Arbeiten erledigt.

Anfang August 2019 fĂŒgte Alexander Korotkov kosmetische Verbesserungen hinzu und verpflichtete den Patch.

Zur Demonstration und fĂŒr einige Nachforschungen werden wir einen Satz von 3 Millionen Rechtecken generieren. Gleichzeitig ein paar Worte zum Box-Typ, da nicht alle Manipulationen damit intuitiv sind.

Die Art der Box - das heißt das Rechteck - ist seit langem in Postgres vorhanden. Sie wird durch 2 Punkte (den geometrischen Typpunkt) definiert - die entgegengesetzten Eckpunkte des Rechtecks ​​(dh das Rechteck kann nicht schrĂ€g und seitlich verstreut sein). Wir lesen in der Dokumentation : „Werte vom Typ box werden in einer der folgenden Formen geschrieben:

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

In der Praxis muss man beispielsweise so schreiben:

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

Zuerst zeigt uns Postgres den oberen rechten Scheitelpunkt, dann den unteren linken. Wenn wir so schreiben,

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

dann werden wir sicherstellen, dass Postgres nicht die Spitzen gab, die sie ihm gaben. Er berechnete die obere rechte und untere linke von unserer oberen linken und unteren rechten. Dies ist eine praktische Eigenschaft, wenn die Position der Scheitelpunkte nicht im Voraus bekannt ist - beispielsweise bei zufÀlliger Erzeugung. Die Notation '1,2', '3,4' entspricht Punkt (1,2), Punkt (3,4). Diese Form ist manchmal bequemer.


FĂŒr Unternehmen: Suchen Sie in 3 Millionen Rechtecken


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

Wir werden 3 Millionen zufĂ€llige Rechtecke erzeugen. Wir wollen eine Normalverteilung, aber um die tablefunc- Erweiterung nicht zu verwenden, verwenden wir den „schlechten“ Ansatz: Wir verwenden random () - random (), was auch ein schönes Bild ergibt (siehe Abb.). Bei Rechtecken sind sie umso nĂ€her an der Mitte, je grĂ¶ĂŸer sie sind. Ihre Schwerpunkte sind ebenfalls zufĂ€llig. Solche Verteilungen sind charakteristisch fĂŒr einige Arten von realen Stadtdaten. Und wer sich mit den Gesetzen der Statistik befassen oder Erinnerungen auffrischen möchte, kann hier zum Beispiel ĂŒber den Unterschied von Zufallsvariablen lesen.




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


Die GrĂ¶ĂŸe der Tabelle mit \dt+ betrĂ€gt 242 MB. Jetzt können Sie die Suche starten.

Wir suchen ohne 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) 

Wir sehen, dass es einen parallelen Seq-Scan gibt - einen sequentiellen Scan (wenn auch parallelisiert).

Erstellen Sie einen regulÀren, nicht inklusiven Index:

 CREATE INDEX ON boxes USING gist(thebox); 

Die GrĂ¶ĂŸe des Index boxes_thebox_idx , der \di+ anzeigt, 262 MB. Als Antwort auf dieselbe Anfrage erhalten wir:


 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) 

Die Suchzeit wurde um den Faktor drei reduziert, und anstelle des parallelen Seq-Scans erhielten sie einen Bitmap-Index-Scan. Es parallelisiert nicht, arbeitet aber schneller.

Töte nun den alten Index und erstelle einen inklusiven:

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

Index der boxes_thebox_name_idx fetteren: 356MB. Lass uns gehen:


 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) 


Nur Index-Scan wird verwendet, aber das Bild ist traurig: Die Zeit ist fast zweimal lÀnger als ohne. Wir lesen das Handbuch des Erstellers der Indizes in Teil I :

â€čRang PostgreSQL-Indizes enthalten keine Informationen, mit denen Sie die Sichtbarkeit von Zeilen beurteilen können. Daher gibt die Zugriffsmethode alle Versionen von Zeilen zurĂŒck, die unter die Suchbedingung fallen, unabhĂ€ngig davon, ob sie fĂŒr die aktuelle Transaktion sichtbar sind oder nicht. Wenn der Indizierungsmechanismus jedoch jedes Mal in die Tabelle schauen mĂŒsste, um die Sichtbarkeit zu bestimmen, wĂŒrde sich diese Scanmethode nicht von der normalen Indexscanung unterscheiden. Das Problem wird durch die Tatsache gelöst, dass PostgreSQL die sogenannte Sichtbarkeitskarte fĂŒr Tabellen unterstĂŒtzt, in der der Vakuumprozess Seiten markiert, auf denen sich Daten nicht lange genug geĂ€ndert haben, damit alle Transaktionen sie sehen können, unabhĂ€ngig von Startzeit und Isolationsstufe. Wenn sich die Kennung der vom Index zurĂŒckgegebenen Zeile auf eine solche Seite bezieht, kann die Sichtbarkeit nicht ĂŒberprĂŒft werden. â€șâ€ș

Wir machen VAKUUM. Wiederholen:

 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) 

Eine ganz andere Sache! Zweimal so viel Gewinn wie im Non-Inclusive-Index.


SelektivitÀt und Gewinn


Die Leistung inklusiver Indizes hĂ€ngt stark von der SelektivitĂ€t der Bedingungen in Abfragen ab. Um diese AbhĂ€ngigkeit ein wenig zu untersuchen, werden wir das umgekehrte Problem lösen: Wir werden eine Beschriftung mit einem Index vom Typ Punkt generieren und wir werden suchen, wie viele Punkte in das gegebene Feld fallen. Verteilen Sie die Punkte gleichmĂ€ĂŸig im Quadrat.

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

Die GrĂ¶ĂŸe der Tabelle betrĂ€gt 211 MB.

 CREATE INDEX on test_covergist USING gist(tochka); 

GrĂ¶ĂŸe 213 MB.

Wir werden natĂŒrlich alle verfĂŒgbaren Punkte auf ein Quadrat setzen:

 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) 

Wir haben EXPLAIN gebeten, die Puffer anzuzeigen. Es wird nĂŒtzlich sein. Jetzt betrĂ€gt die AusfĂŒhrungszeit der Anforderung mehr als 2 Sekunden. Es ist ersichtlich, dass Puffer: gemeinsam gelesen = 54287. In einer anderen Situation konnte eine Mischung aus gemeinsamem Lesen und gemeinsamem Treffer angezeigt werden. Das heißt, einige Puffer werden von der Festplatte (oder aus dem Betriebssystem-Cache) und andere aus dem Puffer-Cache gelesen. Wir kennen die ungefĂ€hre GrĂ¶ĂŸe der Tabelle und der Indizes und schĂŒtzen uns daher, indem wir gemeinsam genutzte Puffer so einstellen, dass alles passt. Starten Sie Postgres mit der Option neu

 -o "-c shared_buffers=1GB" 

Jetzt:

 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) 

Das heißt, das gemeinsame Lesen wurde zu einem gemeinsamen Treffer, und die Zeit wurde dreimal reduziert.

Ein weiteres wichtiges Detail in EXPLAIN: 3 Millionen Punkte werden zurĂŒckgegeben, und die Prognose fĂŒr die zurĂŒckgegebene Anzahl von DatensĂ€tzen betrĂ€gt 3.000. Spoiler: Diese Anzahl Ă€ndert sich bei keiner SelektivitĂ€t. Der Optimierer weiß nicht, wie die KardinalitĂ€t bei der Arbeit mit Box- oder Punkttypen bewertet werden soll. Und der Plan wird sich nicht Ă€ndern: FĂŒr jede GrĂ¶ĂŸe des Rechtecks ​​wird ein Bitmap-Index-Scan fĂŒr test_covergist_tochka_idx durchgefĂŒhrt.

Hier sind zwei weitere Messungen mit der Anzahl der ausgegebenen DatensĂ€tze, die sich um GrĂ¶ĂŸenordnungen unterscheiden:

 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) 

Es werden 10-mal weniger DatensĂ€tze zurĂŒckgegeben (tatsĂ€chliche ... Zeilen = 269882), die Zeit hat sich um etwa das 5-fache verringert.

 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) 

Der Inhalt eines 30K × 30K-Quadrats (2780) wird in nur 16 ms gezĂ€hlt. Und wenn es Dutzende von DatensĂ€tzen gibt, werden sie bereits in Bruchteilen von ms extrahiert, und solche Messungen sind nicht sehr zuverlĂ€ssig.

Messen Sie schließlich dasselbe mit dem Inklusivindex:

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

GrĂ¶ĂŸe 316 MB.

 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) 

Die Zeit ist fast dieselbe wie bei einem herkömmlichen Index, obwohl nur Index scannen.

Aber:

 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) 

Und es war 151 ms. Und dementsprechend:

 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) 

Dies ist bereits ein Bruchteil von ms fĂŒr dieselben 2780-Punkt-DatensĂ€tze.

Puffer wie Waffen


Eine ErklĂ€rung kann in einer Schrotflinte gesucht und gefunden werden, die noch nicht geschossen hat, aber an der Wand hing: die Anzahl der gelesenen Blöcke. Bei einem inklusiven Index werden nur Blöcke des Index selbst gelesen (Heap Fetches: 0). In drei FĂ€llen waren dies die Nummern 40492, 3735 und 52. Bei Verwendung des regulĂ€ren Index bestehen die gelesenen Blöcke jedoch aus der Summe der im Bitmap-Heap-Scan-Index (54248 mit 3 Millionen DatensĂ€tzen) gelesenen Bits und derjenigen, die aus dem Heap gelesen werden mussten (27223). , da das Namensfeld nicht aus einem regulĂ€ren Index extrahiert werden kann. 54248 + 27223 = 81471. Das Exklusive war 40492. FĂŒr zwei andere FĂ€lle: 29534 + 2510 = 31044 und 2655 + 31 = 2686. Im Fall eines regulĂ€ren Index werden ohnehin mehr Blöcke gelesen, aber mit einer Verbesserung der SelektivitĂ€t beginnt sich die Anzahl der gelesenen Blöcke eher um GrĂ¶ĂŸenordnungen als um das Zweifache zu unterscheiden, da die Anzahl der erforderlichen Blöcke aus einem Heap langsamer abnimmt als das Lesen von Indexblöcken.

Gesamtzahl der zurĂŒckgegebenen DatensĂ€tze (Tausend)30002702.7
Leseblöcke (normal / inklusive)81471/4049231044/37352686/52
Zeit755/710151/6616 / 0,7


Aber vielleicht geht es ĂŒberhaupt nicht um SelektivitĂ€t, sondern einfach um die GrĂ¶ĂŸe der Tabelle? FĂŒr alle FĂ€lle wiederholen wir dieselben Schritte und generieren eine Tabelle mit 300.000 und nicht 3 Millionen DatensĂ€tzen:

 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) 

Wiederholen Sie dies als NĂ€chstes fĂŒr den Inklusivindex. Hier sind die Ergebnisse:

Gesamtzahl der zurĂŒckgegebenen DatensĂ€tze (Tausend)300270,25
Leseblöcke (normal / inklusive)5225/37263026/352270/8
Zeit158/17820/130,4 / 0,2


Bei einer 100% igen Punktabdeckung war die Abfrage sogar etwas langsamer als mit dem ĂŒblichen Index. Weiter, wie im Fall von 3 Millionen, passte alles zusammen. Das heißt, SelektivitĂ€t ist wichtig.

Unser Unternehmen testete inklusive GiST-Indizes an realen Daten - ein Satz mit mehreren Millionen Rechtecken auf einer Karte von Moskau. Die Schlussfolgerung ist dieselbe: In vielen Situationen beschleunigen solche Indizes Abfragen spĂŒrbar. Der Artikel kann jedoch nicht mit Bildern und Testzahlen illustriert werden: Diese Daten sind nicht gemeinfrei.

Anstelle einer Schlussfolgerung


Kehren wir fĂŒr einen Moment zu zufĂ€lligen Rechtecken zurĂŒck. Versuchen wir, dasselbe mit spgist zu tun. Sie können sich daran erinnern oder herausfinden, was es heißt, die Unterschiede zwischen SP-GiST und GiST zu verstehen, indem Sie den Artikel Indizes in PostgreSQL - 6 lesen. Erstellen Sie einen Inklusivindex:

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

Leider sind fĂŒr SP-GiST inklusive Indizes noch nicht implementiert.
Es gibt also Raum fĂŒr Verbesserungen!



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


All Articles