Beschleunigen Sie die Erstellung des Postgres-Teilindex

8

Ich versuche, Teilindizes für eine große statische Tabelle (1,2 TB) in Postgres 9.4 zu erstellen.

Meine Daten sind vollständig statisch, sodass ich alle Daten einfügen und dann alle Indizes erstellen kann.

In dieser 1,2-TB-Tabelle habe ich eine Spalte mit dem Namen run_id, die die Daten sauber aufteilt . Wir haben eine großartige Leistung erzielt, indem wir Indizes erstellt haben, die einen Bereich von run_ids abdecken . Hier ist ein Beispiel:

CREATE INDEX perception_run_frame_idx_run_266_thru_270
ON run.perception
(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

Diese Teilindizes geben uns die gewünschte Abfragegeschwindigkeit. Leider dauert die Erstellung jedes Teilindex etwa 70 Minuten.

Es sieht so aus, als wären wir CPU-begrenzt ( topzeigt 100% für den Prozess an).
Kann ich irgendetwas tun, um die Erstellung unserer Teilindizes zu beschleunigen?

Systemspezifikationen:

  • 18 Kern Xeon
  • 192 GB RAM
  • 12 SSDs in RAID
  • Autovakuums sind ausgeschaltet
  • tenance_work_mem: 64 GB (zu hoch?)

Tabellenspezifikationen:

  • Größe: 1,26 TB
  • Anzahl der Zeilen: 10.537 Milliarden
  • Typische Indexgröße: 3,2 GB (es gibt eine Varianz von ~ 0,5 GB)

Tabellendefinition:

CREATE TABLE run.perception(
id bigint NOT NULL,
run_id bigint NOT NULL,
frame bigint NOT NULL,
by character varying(45) NOT NULL,
by_anyone bigint NOT NULL,
by_me bigint NOT NULL,
by_s_id integer,
owning_p_id bigint NOT NULL,
obj_type_set bigint,
seq integer,
subj_id bigint NOT NULL,
subj_state_frame bigint NOT NULL,
CONSTRAINT perception_pkey PRIMARY KEY (id))

(Lesen Sie nicht zu viel in die Spaltennamen - ich habe sie etwas verschleiert.)

Hintergrundinformation:

  • Wir haben ein separates Team vor Ort, das diese Daten verwendet, aber tatsächlich gibt es nur einen oder zwei Benutzer. (Diese Daten werden alle über eine Simulation generiert.) Benutzer beginnen erst mit der Analyse der Daten, wenn die Einfügungen abgeschlossen und die Indizes vollständig erstellt sind. Unser Hauptanliegen ist es, den Zeitaufwand für die Generierung verwendbarer Daten zu reduzieren. Derzeit besteht der Engpass in der Indexerstellungszeit.
  • Die Abfragegeschwindigkeit war bei Verwendung von Partials völlig ausreichend. Tatsächlich denke ich, wir könnten die Anzahl der Läufe, die jeder Index abdeckt, erhöhen und dennoch eine ausreichend gute Abfrageleistung aufrechterhalten.
  • Ich vermute, dass wir die Tabelle partitionieren müssen. Wir versuchen, alle anderen Optionen auszuschöpfen, bevor wir diesen Weg einschlagen.
Burnsy
quelle
Diese zusätzlichen Informationen wären von entscheidender Bedeutung: Datentypen der beteiligten Spalten, typische Abfrage, Kardinalität (Anzahl der Zeilen), wie viele verschiedene run_id? Gleichmäßig verteilt? Größe des resultierenden Index auf der Festplatte? Daten sind statisch, ok. Aber bist du der einzige Benutzer?
Erwin Brandstetter
Mit weiteren Informationen aktualisiert.
Burnsy
1
" Autovacuums sind ausgeschaltet " - warum? Das ist eine wirklich schlechte Idee. Dies verhindert das Sammeln von Statistiken und führt somit zu schlechten Abfrageplänen
a_horse_with_no_name
@a_horse_with_no_name Wir starten manuell eine Analyse, nachdem alle Daten eingefügt wurden
Burnsy
Ihre Situation ist mir noch unklar. Wie sehen Ihre Anfragen aus? Wenn Ihr Tisch ist completely static, was meinen Sie dann damit We have a separate team onsite that consumes this data? Indizieren Sie nur den Bereich run_id >= 266 AND run_id <= 270oder die gesamte Tabelle? Wie hoch ist die Lebenserwartung jedes Index / wie viele Abfragen werden ihn verwenden? Für wie viele verschiedene Werte run_id? Klingt nach ~ 15 Mio. Zeilen pro run_id, was würde es rund 800 verschiedene Werte für machen run_id? Warum obj_type_set, by_s_id, seqnicht NOT NULL definiert? Welcher grobe Prozentsatz der NULL-Werte für jeden?
Erwin Brandstetter

Antworten:

8

BRIN-Index

Verfügbar seit Postgres 9.5 und wahrscheinlich genau das, wonach Sie suchen. Viel schnellere Indexerstellung, viel kleinerer Index. Abfragen sind jedoch normalerweise nicht so schnell. Das Handbuch:

BRIN steht für Block Range Index. BRIN wurde für die Verarbeitung sehr großer Tabellen entwickelt, bei denen bestimmte Spalten eine natürliche Korrelation mit ihrer physischen Position innerhalb der Tabelle aufweisen. Ein Blockbereich ist eine Gruppe von Seiten, die in der Tabelle physisch benachbart sind. Für jeden Blockbereich werden einige zusammenfassende Informationen vom Index gespeichert.

Lesen Sie weiter, es gibt noch mehr.
Depesz führte einen Vorversuch durch.

Das Optimum für Ihren Fall: Wenn Sie Zeilen schreiben können geclustert auf run_id, wird Ihr Index sehr klein und Schöpfung viel billiger.

CREATE INDEX foo ON run.perception USING brin (run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

Sie können sogar einfach die gesamte Tabelle indizieren .

Tabellenlayout

Was auch immer Sie tun, Sie können 8 Bytes sparen, die aufgrund von Ausrichtungsanforderungen pro Zeile durch Auffüllen verloren gehen, indem Sie Spalten wie folgt anordnen:

CREATE TABLE run.perception(
  id               bigint NOT NULL PRIMARY KEY
, run_id           bigint NOT NULL
, frame            bigint NOT NULL
, by_anyone        bigint NOT NULL
, by_me            bigint NOT NULL
, owning_p_id      bigint NOT NULL
, subj_id          bigint NOT NULL
, subj_state_frame bigint NOT NULL
, obj_type_set     bigint
, by_s_id          integer
, seq              integer
, by               varchar(45) NOT NULL -- or just use type text
);

Verkleinert Ihre Tabelle um 79 GB, wenn keine der Spalten NULL-Werte enthält. Einzelheiten:

Außerdem haben Sie nur drei Spalten, die NULL sein können. Die NULL-Bitmap belegt 8 Bytes für 9 bis 72 Spalten. Wenn nur eine Ganzzahlspalte NULL ist, gibt es einen Eckfall für ein Speicherparadoxon: Es wäre billiger, stattdessen einen Dummy-Wert zu verwenden: 4 Bytes verschwendet, aber 8 Bytes gespeichert, da keine NULL-Bitmap für die Zeile benötigt wird. Weitere Details hier:

Teilindizes

Abhängig von Ihren tatsächlichen Abfragen ist es möglicherweise effizienter, diese fünf Teilindizes anstelle der oben genannten zu verwenden:

CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 266;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 267;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 268;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 269;
CREATE INDEX perception_run_id266_idx ON run.perception(frame) WHERE run_id = 270;

Führen Sie jeweils eine Transaktion aus.

Durch Entfernen auf run_iddiese Weise als Indexspalte werden 8 Byte pro Indexeintrag eingespart - 32 statt 40 Byte pro Zeile. Jeder Index ist auch billiger zu erstellen, aber das Erstellen von fünf statt nur einem Index dauert wesentlich länger, wenn eine Tabelle zu groß ist, um im Cache zu bleiben (wie @ Jürgen und @Chris kommentiert). Das kann also für Sie nützlich sein oder auch nicht.

Partitionierung

Basierend auf Vererbung - die einzige Option bis Postgres 9.5.
(Die neue deklarative Partitionierung in Postgres 11 oder vorzugsweise 12 ist intelligenter.)

Das Handbuch:

Alle Einschränkungen für alle untergeordneten Elemente der übergeordneten Tabelle werden beim Ausschluss von Einschränkungen untersucht, sodass eine große Anzahl von Partitionen die Planungszeit für Abfragen wahrscheinlich erheblich verlängert. Die auf Legacy-Vererbung basierende Partitionierung funktioniert also gut mit bis zu hundert Partitionen . Versuchen Sie nicht, viele tausend Partitionen zu verwenden.

Meine kühne Betonung. Wenn Sie also 1000 verschiedene Werte für schätzen run_id, würden Sie Partitionen erstellen, die jeweils etwa 10 Werte umfassen.


maintenance_work_mem

Ich habe vermisst, dass Sie sich bereits maintenance_work_memin meiner ersten Lektüre darauf eingestellt haben. Ich werde Zitat und Rat in meiner Antwort als Referenz hinterlassen. Pro Dokumentation:

maintenance_work_mem (ganze Zahl)

Gibt die maximale Menge an Speicher , der von Wartungsoperationen verwendet werden, wie zum Beispiel VACUUM, CREATE INDEX, und ALTER TABLE ADD FOREIGN KEY. Der Standardwert ist 64 Megabyte ( 64MB). Da jeweils nur einer dieser Vorgänge von einer Datenbanksitzung ausgeführt werden kann und bei einer Installation normalerweise nicht viele von ihnen gleichzeitig ausgeführt werden, ist es sicher, diesen Wert deutlich größer als festzulegen work_mem. Größere Einstellungen können die Leistung beim Staubsaugen und beim Wiederherstellen von Datenbank-Dumps verbessern.

Beachten Sie, dass dieser Speicher bei der Ausführung autovacuumbis zu einem bestimmten autovacuum_max_workersZeitpunkt zugewiesen werden kann. Achten Sie daher darauf, den Standardwert nicht zu hoch einzustellen. Es kann nützlich sein, dies separat zu steuern setting autovacuum_work_mem.

Ich würde es nur so hoch wie nötig einstellen - was von der unbekannten (für uns) Indexgröße abhängt. Und nur lokal für die ausführende Sitzung. Wie das Zitat erklärt, kann eine zu hohe allgemeine Einstellung den Server ansonsten aushungern lassen, da das Autovakuum möglicherweise auch mehr RAM beansprucht. Stellen Sie es auch nicht viel höher als nötig ein, selbst in der ausgeführten Sitzung kann der freie Arbeitsspeicher beim Zwischenspeichern von Daten gut genutzt werden.

Es könnte so aussehen:

BEGIN;

SET LOCAL maintenance_work_mem = 10GB;  -- depends on resulting index size

CREATE INDEX perception_run_frame_idx_run_266_thru_270 ON run.perception(run_id, frame)
WHERE run_id >= 266 AND run_id <= 270;

COMMIT;

Über SET LOCAL:

Die Auswirkungen SET LOCALdauern nur bis zum Ende der aktuellen Transaktion, ob festgeschrieben oder nicht.

So messen Sie Objektgrößen:

Der Server sollte im Allgemeinen natürlich vernünftigerweise anders konfiguriert werden.

Erwin Brandstetter
quelle
Ich wette, seine Arbeit ist IO-gebunden, da die Tabelle viel größer als RAM ist. Wenn Sie die Tabelle noch häufiger lesen, wird das Problem noch schlimmer, unabhängig davon, ob genügend Speicher vorhanden ist, um jeden erstellten Index im Speicher zu sortieren oder nicht.
Jürgen Strobel
Ich bin mit Jürgen in diesem Fall. Ich glaube, dass Sie aufgrund der Größe der Tabelle im Wesentlichen einen vollständigen sequentiellen Scan der Tabelle pro erstelltem Index durchführen müssen. Außerdem bin ich mir nicht sicher, ob Sie durch das Erstellen der separaten Teilindizes so viel Leistungssteigerung sehen werden (ich bin zu 90% sicher, dass Sie keine Steigerung sehen werden, aber diesbezüglich könnte ich aus sein.) Ich glaube, eine bessere Die Lösung für die Indexerstellung besteht darin, einen Index über den gesamten Bereich zu erstellen, den Sie als "einzelnen Teilindex" abfragen möchten, um die gesamte Erstellungszeit niedrig zu halten.
Chris
@Chris: Ich bin damit einverstanden, dass die Erstellung von 5 Indizes länger dauert als nur eines (selbst wenn alle zusammen kleiner sind, ist die Erstellung jedes Index billiger und Abfragen können schneller sein). Wenn Sie noch etwas darüber nachdenken, sollte dies ein perfekter Anwendungsfall für einen BRIN-Index in Postgres 9.5 sein.
Erwin Brandstetter
3

Vielleicht ist das nur überentwickelt. Haben Sie tatsächlich versucht, einen einzelnen vollständigen Index zu verwenden? Teilindizes, die die gesamte Tabelle zusammen abdecken, bieten, wenn überhaupt, keinen großen Gewinn für Indexsuchen, und aus Ihrem Text schließe ich, dass Sie Indizes für alle run_ids haben? Index-Scans mit Teilindizes können einige Vorteile haben, dennoch würde ich zuerst die einfache Ein-Index-Lösung vergleichen.

Für jede Indexerstellung benötigen Sie einen vollständigen E / A-gebundenen Scan der Tabelle. Das Erstellen mehrerer Teilindizes erfordert daher weitaus mehr E / A-Lesen der Tabelle als für einen einzelnen Index, obwohl die Sortierung für den einzelnen großen Index auf die Festplatte übertragen wird. Wenn Sie auf Teilindizes bestehen, können Sie versuchen, alle (oder mehrere) Indizes gleichzeitig zu erstellen (sofern der Speicher dies zulässt).

Für eine grobe Schätzung von wartung_work_mem, die erforderlich ist, um alle run_ids zu sortieren, die 8-Byte-Bigints sind, benötigen Sie 10,5 * 8 GB + etwas Overhead im Speicher.

Jürgen Strobel
quelle
0

Sie können die Indizes auch für andere als die Standardtabellenbereiche erstellen. Diese Tablespaces können auf Festplatten verweisen, die nicht redundant sind (erstellen Sie die Indizes einfach neu, wenn sie fehlschlagen) oder sich auf schnelleren Arrays befinden.

Sie können die Tabelle auch nach denselben Kriterien wie Ihre Teilindizes partitionieren. Dies würde die gleiche Geschwindigkeit wie der Index bei der Abfrage ermöglichen, ohne überhaupt einen Index zu erstellen.

Kirk Roybal
quelle