Welcher Index soll mit vielen doppelten Werten verwendet werden?

14

Nehmen wir ein paar Annahmen an:

Ich habe einen Tisch, der so aussieht:

 a | b
---+---
 a | -1
 a | 17
  ...
 a | 21
 c | 17
 c | -3
  ...
 c | 22

Fakten zu meinem Set:

  • Die Größe der gesamten Tabelle beträgt ~ 10 10 Zeilen.

  • Ich habe ~ 100k Zeilen mit einem Wert ain der Spalte a, ähnlich wie bei anderen Werten (zB c).

  • Das bedeutet ~ 100k verschiedene Werte in der Spalte 'a'.

  • Die meisten meiner Abfragen lesen alle oder die meisten Werte für einen bestimmten Wert in einem, z select sum(b) from t where a = 'c'.

  • Die Tabelle ist so geschrieben, dass aufeinanderfolgende Werte physisch nahe beieinander liegen (entweder ist sie in der richtigen Reihenfolge geschrieben, oder wir gehen davon aus CLUSTER, dass sie für diese Tabelle und Spalte verwendet wurde a).

  • Die Tabelle wird selten, wenn überhaupt, aktualisiert. Wir sorgen uns nur um die Lesegeschwindigkeit.

  • Die Tabelle ist relativ eng (z. B. ~ 25 Bytes pro Tupel, + 23 Bytes Overhead).

Die Frage ist nun, welche Art von Index soll ich verwenden? Mein Verständnis ist:

  • BTree Mein Problem hierbei ist, dass der BTree-Index sehr groß sein wird, da meines Wissens doppelte Werte gespeichert werden (dies muss geschehen, da nicht angenommen werden kann, dass die Tabelle physisch sortiert ist). Wenn der BTree sehr groß ist, muss ich am Ende sowohl den Index als auch die Teile der Tabelle lesen, auf die der Index verweist. (Wir können benutzenfillfactor = 100 den Index etwas verkleinern.)

  • BRIN Ich verstehe, dass ich hier einen kleinen Index haben kann, auf Kosten des Lesens nutzloser Seiten. Ein kleiner pages_per_rangeIndex bedeutet, dass der Index größer ist (was bei BRIN ein Problem ist, da ich den gesamten Index lesen muss), und ein großer Index pages_per_rangebedeutet, dass ich viele nutzlose Seiten lesen werde. Gibt es eine Zauberformel, um einen guten Wert von zu finden?pages_per_range , der diese Kompromisse berücksichtigt?

  • GIN / GiST Nicht sicher, ob diese hier relevant sind, da sie hauptsächlich für die Volltextsuche verwendet werden, aber ich höre auch, dass sie gut mit doppelten Schlüsseln umgehen können. Wäre hier entweder ein GINoder ein GiSTIndex hilfreich?

Eine andere Frage ist, ob Postgres die Tatsache verwendet, dass eine Tabelle CLUSTERim Abfrageplaner bearbeitet wird (vorausgesetzt, es werden keine Aktualisierungen vorgenommen) (z. B. durch binäre Suche nach den relevanten Start- / End-Seiten). Kann ich in gewisser Hinsicht einfach alle meine Spalten in einem BTree speichern und die Tabelle insgesamt löschen (oder etwas Äquivalentes erreichen, ich glaube, das sind Clustered-Indizes in SQL Server)? Gibt es einen hybriden BTree / BRIN-Index, der hier helfen würde?

Ich würde es lieber vermeiden, Arrays zum Speichern meiner Werte zu verwenden, da meine Abfrage auf diese Weise weniger lesbar wird (ich verstehe, dass dies die Kosten für den Overhead von 23 Bytes pro Tupel verringern würde, indem die Anzahl der Tupel verringert wird).

foo
quelle
"Meistens für die Volltextsuche verwendet" GiST wird von PostGIS recht häufig verwendet.
jpmc26

Antworten:

15

BTree

Mein Problem hierbei ist, dass der BTree-Index sehr groß sein wird, da er doppelte Werte speichert (dies ist auch der Fall, da nicht davon ausgegangen werden kann, dass die Tabelle physisch sortiert ist). Wenn der BTree riesig ist, muss ich am Ende sowohl den Index als auch die Teile der Tabelle lesen, auf die der Index ebenfalls verweist ...

Nicht unbedingt - Mit einem Btree-Index, der „abdeckt“, erzielen Sie die schnellste Lesezeit, und wenn Sie nur das möchten (dh wenn Sie sich den zusätzlichen Speicher leisten können), ist dies die beste Wahl.

BRIN

Ich verstehe, dass ich hier einen kleinen Index haben kann, auf Kosten des Lesens nutzloser Seiten. Ein kleiner pages_per_rangeIndex bedeutet, dass der Index größer ist (was bei BRIN ein Problem ist, da ich den gesamten Index lesen muss) und einen großen Index hatpages_per_range bedeutet, dass ich viele nutzlose Seiten lesen werde.

Wenn Sie sich den Speicheraufwand für einen abdeckenden Btree-Index nicht leisten können, ist BRIN ideal für Sie, da bereits Clustering vorhanden ist (dies ist für BRIN von entscheidender Bedeutung). BRIN-Indizes sind winzig , sodass sich wahrscheinlich alle Seiten im Arbeitsspeicher befinden, wenn Sie einen geeigneten Wert für auswählen pages_per_range.

Gibt es eine Zauberformel, um einen guten Wert für pages_per_range zu finden, der diese Kompromisse berücksichtigt?

Keine Zauberformel, aber beginnen Sie mit pages_per_range etwas weniger als der Durchschnittsgröße (in Seiten), die der Durchschnittswert aeinnimmt. Sie versuchen wahrscheinlich, Folgendes zu minimieren: (Anzahl der gescannten BRIN-Seiten) + (Anzahl der gescannten Heap-Seiten) für eine typische Abfrage. Suchen Sie Heap Blocks: lossy=nim Ausführungsplan nach pages_per_range=1und vergleichen Sie sie mit anderen Werten für pages_per_range- dh sehen Sie, wie viele unnötige Heap-Blöcke gescannt werden.

GIN / GiST

Ich bin mir nicht sicher, ob diese relevant sind, da sie hauptsächlich für die Volltextsuche verwendet werden, aber ich höre auch, dass sie gut mit doppelten Schlüsseln umgehen können. Würde entweder ein GIN/ GiSTindex hier helfen?

GIN ist vielleicht eine Überlegung wert, aber wahrscheinlich nicht GiST. Wenn das natürliche Clustering jedoch wirklich gut ist, ist BRIN wahrscheinlich die bessere Wahl.

Hier ist ein Beispielvergleich zwischen den verschiedenen Indextypen für Dummy-Daten, ähnlich wie bei Ihnen:

Tabelle und Indizes:

create table foo(a,b,c) as
select *, lpad('',20)
from (select chr(g) a from generate_series(97,122) g) a
     cross join (select generate_series(1,100000) b) b
order by a;
create index foo_btree_covering on foo(a,b);
create index foo_btree on foo(a);
create index foo_gin on foo using gin(a);
create index foo_brin_2 on foo using brin(a) with (pages_per_range=2);
create index foo_brin_4 on foo using brin(a) with (pages_per_range=4);
vacuum analyze;

Beziehungsgrößen:

select relname "name", pg_size_pretty(siz) "size", siz/8192 pages, (select count(*) from foo)*8192/siz "rows/page"
from( select relname, pg_relation_size(C.oid) siz
      from pg_class c join pg_namespace n on n.oid = c.relnamespace
      where nspname = current_schema ) z;
name | Größe | seiten | Zeilen / Seite
: ----------------- | : ------ | ----: | --------:
foo | 149 MB | 19118 | 135
foo_btree_covering | 56 MB | 7132 | 364
foo_btree | 56 MB | 7132 | 364
foo_gin | 2928 kB | 366 | 7103
foo_brin_2 | 264 kB | 33 | 78787
foo_brin_4 | 136 kB | 17 | 152941

btree abdecken:

explain analyze select sum(b) from foo where a='a';
| ANFRAGEPLAN |
| : ------------------------------------------------- -------------------------------------------------- ------------------------------------------- |
| Aggregat (Kosten = 3282,57,3282,58 Zeilen = 1 Breite = 8) (tatsächliche Zeit = 45,942,45,942 Zeilen = 1 Schleifen = 1) |
| -> Nur Index Mit foo_btree_covering auf foo scannen (Kosten = 0,43..3017.80 Zeilen = 105907 Breite = 4) (tatsächliche Zeit = 0,038..27.286 Zeilen = 100000 Schleifen = 1) |
| Indexbedingung: (a = 'a' :: text) |
| Heap Fetches: 0 |
| Planungszeit: 0,099 ms |
| Ausführungszeit: 45.968 ms |

einfacher Baum:

drop index foo_btree_covering;
explain analyze select sum(b) from foo where a='a';
| ANFRAGEPLAN |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| Aggregat (Kosten = 4064.57..4064.58 Zeilen = 1 Breite = 8) (tatsächliche Zeit = 54.242..54.242 Zeilen = 1 Schleifen = 1) |
| -> Indexsuche mit foo_btree on foo (Kosten = 0,43..3799.80 Zeilen = 105907 Breite = 4) (tatsächliche Zeit = 0,037..33.084 Zeilen = 100000 Schleifen = 1) |
| Indexbedingung: (a = 'a' :: text) |
| Planungszeit: 0,135 ms |
| Ausführungszeit: 54.280 ms |

BRIN pages_per_range = 4:

drop index foo_btree;
explain analyze select sum(b) from foo where a='a';
| ANFRAGEPLAN |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| Aggregat (Kosten = 21595,38..21595,39 Zeilen = 1 Breite = 8) (tatsächliche Zeit = 52,455..52,455 Zeilen = 1 Schleifen = 1) |
| -> Bitmap-Heap-Scan auf foo (Kosten = 888.78..21330.61 Zeilen = 105907 Breite = 4) (tatsächliche Zeit = 2.738..31.967 Zeilen = 100000 Schleifen = 1) |
| Überprüfen Sie erneut Cond: (a = 'a' :: text) |
| Zeilen vom Index entfernt Erneut prüfen: 96 |
| Heap-Blöcke: verlustbehaftet = 736 |
| -> Bitmap-Index-Scan für foo_brin_4 (Kosten = 0,00..862.30 Zeilen = 105907 Breite = 0) (tatsächliche Zeit = 2.720..2.720 Zeilen = 7360 Schleifen = 1) |
| Indexbedingung: (a = 'a' :: text) |
| Planungszeit: 0,101 ms |
| Ausführungszeit: 52.501 ms |

BRIN pages_per_range = 2:

drop index foo_brin_4;
explain analyze select sum(b) from foo where a='a';
| ANFRAGEPLAN |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| Aggregat (Kosten = 21659,38..21659,39 Zeilen = 1 Breite = 8) (tatsächliche Zeit = 53,971..53,971 Zeilen = 1 Schleifen = 1) |
| -> Bitmap-Heap-Scan auf foo (Kosten = 952.78..21394.61 Zeilen = 105907 Breite = 4) (tatsächliche Zeit = 5.286..33.492 Zeilen = 100000 Schleifen = 1) |
| Überprüfen Sie erneut Cond: (a = 'a' :: text) |
| Zeilen vom Index entfernt Erneut prüfen: 96 |
| Heap-Blöcke: verlustbehaftet = 736 |
| -> Bitmap-Index-Scan für foo_brin_2 (Kosten = 0,00..926.30 Zeilen = 105907 Breite = 0) (tatsächliche Zeit = 5,275..5.275 Zeilen = 7360 Schleifen = 1) |
| Indexbedingung: (a = 'a' :: text) |
| Planungszeit: 0,095 ms |
| Ausführungszeit: 54.016 ms |

GIN:

drop index foo_brin_2;
explain analyze select sum(b) from foo where a='a';
| ANFRAGEPLAN |
| : ------------------------------------------------- -------------------------------------------------- ------------------------------ |
| Aggregat (Kosten = 21687.38..21687.39 Zeilen = 1 Breite = 8) (tatsächliche Zeit = 55.331..55.331 Zeilen = 1 Schleifen = 1) |
| -> Bitmap-Heap-Scan auf foo (Kosten = 980.78..21422.61 Zeilen = 105907 Breite = 4) (tatsächliche Zeit = 12.377..33.956 Zeilen = 100000 Schleifen = 1) |
| Überprüfen Sie erneut Cond: (a = 'a' :: text) |
| Heap-Blöcke: genau = 736 |
| -> Bitmap-Index-Scan für foo_gin (Kosten = 0,00..954.30 Zeilen = 105907 Breite = 0) (tatsächliche Zeit = 12.271..12.271 Zeilen = 100000 Schleifen = 1) |
| Indexbedingung: (a = 'a' :: text) |
| Planungszeit: 0,118 ms |
| Ausführungszeit: 55.366 ms |

dbfiddle hier

Jack sagt, versuchen Sie es mit topanswers.xyz
quelle
Ein Deckungsindex würde also das Lesen der Tabelle insgesamt auf Kosten des Speicherplatzes überspringen? Scheint ein guter Kompromiss zu sein. Ich denke , dass wir die gleiche Sache für die BRIN Index bedeuten von ‚lesen Sie den gesamten Index‘ (korrigiert mich wenn ich falsch liege), ich meine die ganze BRIN Index scannen , die ich denke , ist das, was in geschieht dbfiddle.uk/... , nicht wahr?
foo
@foo über die "(hat es auch, da es nicht davon ausgehen kann, dass die Tabelle physisch sortiert ist)." Die physische Reihenfolge (Cluster oder nicht) der Tabelle ist irrelevant. Der Index hat die Werte in der richtigen Reihenfolge. Postgres-B-Tree-Indizes müssen jedoch alle Werte speichern (und zwar mehrfach). So sind sie gestaltet. Das Speichern jedes einzelnen Wertes nur einmal wäre eine nette Funktion / Verbesserung. Sie könnten es den Postgres-Entwicklern vorschlagen (und sogar bei der Implementierung helfen).
ypercubeᵀᴹ
1
@foo - Sie haben vollkommen recht, ein Scan eines BRIN-Index durchsucht immer den gesamten Index ( pgcon.org/2016/schedule/attachments/… , 2. letzte Folie) - obwohl dies auf dem EXPLAIN- Plan in der Geige nicht angezeigt wird , ist es?
Jack sagt, versuchen Sie es topanswers.xyz
2
@ ypercubeᵀᴹ Sie können COMPRESS in Oracle verwenden, in dem jedes Präfix einmal pro Block gespeichert wird.
Jack sagt, versuchen Sie topanswers.xyz
@JackDouglas Ich habe gelesen Bitmap Index Scan, dass es "den gesamten Index lesen" bedeutet, aber vielleicht ist das die falsche Lesart. Oracle COMPRESSsieht nach etwas aus, das hier nützlich wäre, da es die Größe des B-Baums verringern würde, aber ich bleibe bei pg!
foo
6

Neben btree und brin die scheinen , die am sinnvollsten Möglichkeiten, einige andere, exotische Optionen , die eine Untersuchung wert sein könnte - sie könnten nützlich oder nicht in Ihrem Fall:

  • INCLUDEIndizes . Sie werden - hoffentlich - irgendwann im September 2017 in der nächsten Hauptversion (10) von Postgres erscheinen. Ein Index auf (a) INCLUDE (b)hat dieselbe Struktur wie ein Index auf (a), enthält jedoch in den Blattseiten alle Werte von b(aber ungeordnet). Was bedeutet, dass Sie es nicht zum Beispiel für verwenden können SELECT * FROM t WHERE a = 'a' AND b = 2 ;. Der Index kann verwendet werden, aber während ein (a,b)Index die übereinstimmenden Zeilen bei einer einzelnen Suche findet, muss der Include-Index die (möglicherweise 100 KB wie in Ihrem Fall) Werte durchlaufen, die übereinstimmen, a = 'a'und die bWerte prüfen .
    Auf der anderen Seite ist der Index etwas weniger breit als der (a,b)Index, und Sie müssen die Reihenfolge nicht einhalten, bdamit Ihre Abfrage berechnet werden kannSUM(b) . Sie könnten zum Beispiel auch haben(a) INCLUDE (b,c,d) Dies kann für ähnliche Abfragen wie Ihre verwendet werden, die in allen drei Spalten zusammengefasst sind.

  • Gefilterte (Teil-) Indizes . Ein Hinweis , dass vielleicht ein bisschen verrückt klingt * zuerst:

    CREATE INDEX flt_a  ON t (b) WHERE (a = 'a') ;
    ---
    CREATE INDEX flt_xy ON t (b) WHERE (a = 'xy') ;

    Ein Index für jeden aWert. In Ihrem Fall um 100K Indizes. Bedenken Sie, dass jeder Index sehr klein ist, sowohl in Bezug auf die Größe (Anzahl der Zeilen) als auch in Bezug auf die Breite (da nur bWerte gespeichert werden). In allen anderen Aspekten fungiert es (die 100K-Indizes zusammen) als B-Tree-Index, (a,b)während der Platz eines (b)Index verwendet wird.
    Nachteil ist, dass Sie sie jedes Mal selbst erstellen und pflegen müssen, wenn ein neuer Wert von ain die Tabelle eingefügt wird. Da Ihre Tabelle ziemlich stabil ist, ohne viele (oder keine) Einfügungen / Aktualisierungen, scheint dies kein Problem zu sein.

  • Übersichtstabellen. Da die Tabelle relativ stabil ist, können Sie jederzeit eine Übersichtstabelle mit den am häufigsten benötigten Aggregaten ( sum(b), sum(c), sum(d), avg(b), count(distinct b)usw.) erstellen und auffüllen. Es ist klein (nur 100 KB Zeilen) und muss nur einmal ausgefüllt und aktualisiert werden, wenn Zeilen in der Haupttabelle eingefügt / aktualisiert / gelöscht werden.

*: Idee kopiert von diesem Unternehmen, das 10 Millionen Indizes in seinem Produktionssystem ausführt: The Heap: 10 Millionen Postgresql-Indizes in der Produktion ausführen (und zählen) .

ypercubeᵀᴹ
quelle
1 ist interessant, aber wie Sie bereits betont haben, ist S. 10 noch nicht erschienen. 2 hört sich verrückt an (oder zumindest gegen "allgemeine Weisheit"), ich werde es lesen, da Sie darauf hinweisen, dass es mit meinem Workflow mit fast keinen Schreibvorgängen funktionieren könnte. 3. würde für mich nicht funktionieren, ich habe es SUMals Beispiel genommen, aber in der Praxis können meine Abfragen nicht vorberechnet werden (sie select ... from t where a = '?' and ??ähneln ??eher einer anderen benutzerdefinierten Bedingung.
foo
1
Nun, wir können nicht helfen, wenn wir nicht wissen, was ??ist;)
ypercubeᵀᴹ
Sie erwähnen gefilterte Indizes. Was ist mit der Aufteilung der Tabelle?
jpmc26
@ jpmc26 komisch, ich habe darüber nachgedacht, in der antwort hinzuzufügen, dass der vorschlag gefilterter indizes in gewisser weise eine form der partitionierung ist. Partitionierung könnte auch hier hilfreich sein, aber ich bin nicht sicher. Dies würde zu vielen kleinen Indizes / Tabellen führen.
ypercubeᵀᴹ
2
Ich gehe davon aus, dass Teilabdeckungs-Btree-Indizes hier der König der Leistung sind, da die Daten so gut wie nie aktualisiert werden. Auch wenn das 100k Indizes bedeutet. Die Gesamtindexgröße ist am kleinsten (mit Ausnahme eines BRIN-Index, aber dort muss Postgres zusätzlich Heap-Seiten lesen und filtern). Die Indexgenerierung kann mit dynamischem SQL automatisiert werden. Beispielaussage DOin dieser verwandten Antwort .
Erwin Brandstetter