Ich habe einen Tisch wie diesen:
CREATE TABLE products (
id serial PRIMARY KEY,
category_ids integer[],
published boolean NOT NULL,
score integer NOT NULL,
title varchar NOT NULL);
Ein Produkt kann mehreren Kategorien angehören. category_ids
Die Spalte enthält eine Liste der IDs aller Produktkategorien.
Eine typische Abfrage sieht folgendermaßen aus (immer auf der Suche nach einer einzelnen Kategorie):
SELECT * FROM products WHERE published
AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title
LIMIT 20 OFFSET 8000;
Um es zu beschleunigen, benutze ich den folgenden Index:
CREATE INDEX idx_test1 ON products
USING GIN (category_ids gin__int_ops) WHERE published;
Dieser hilft sehr, es sei denn, es gibt zu viele Produkte in einer Kategorie. Es filtert schnell Produkte heraus, die zu dieser Kategorie gehören, aber dann gibt es eine Sortieroperation, die auf die harte Tour (ohne Index) durchgeführt werden muss.
Eine installierte btree_gin
Erweiterung, mit der ich einen mehrspaltigen GIN-Index wie folgt erstellen kann:
CREATE INDEX idx_test2 ON products USING GIN (
category_ids gin__int_ops, score, title) WHERE published;
Aber Postgres will das nicht zum Sortieren verwenden . Auch wenn ich den DESC
Bezeichner in der Abfrage entferne .
Alle alternativen Ansätze zur Optimierung der Aufgabe sind sehr willkommen.
Zusätzliche Information:
- PostgreSQL 9.4 mit Intarray-Erweiterung
- Die Gesamtzahl der Produkte beträgt derzeit 260.000, es wird jedoch ein deutliches Wachstum erwartet (bis zu 10 Millionen, dies ist eine mandantenfähige E-Commerce-Plattform).
- Produkte pro Kategorie 1..10000 (kann bis zu 100.000 wachsen), der Durchschnitt liegt unter 100, aber diese Kategorien mit einer großen Anzahl von Produkten ziehen tendenziell viel mehr Anfragen an
Der folgende Abfrageplan wurde von einem kleineren Testsystem erhalten (4680 Produkte in der ausgewählten Kategorie, insgesamt 200.000 Produkte in der Tabelle):
Limit (cost=948.99..948.99 rows=1 width=72) (actual time=82.330..82.341 rows=20 loops=1)
-> Sort (cost=948.37..948.99 rows=245 width=72) (actual time=80.231..81.337 rows=4020 loops=1)
Sort Key: score, title
Sort Method: quicksort Memory: 928kB
-> Bitmap Heap Scan on products (cost=13.90..938.65 rows=245 width=72) (actual time=1.919..16.044 rows=4680 loops=1)
Recheck Cond: ((category_ids @> '{292844}'::integer[]) AND published)
Heap Blocks: exact=3441
-> Bitmap Index Scan on idx_test2 (cost=0.00..13.84 rows=245 width=0) (actual time=1.185..1.185 rows=4680 loops=1)
Index Cond: (category_ids @> '{292844}'::integer[])
Planning time: 0.202 ms
Execution time: 82.404 ms
Hinweis Nr. 1 : 82 ms sehen möglicherweise nicht so beängstigend aus, aber das liegt daran, dass der Sortierpuffer in den Speicher passt. Sobald ich alle Spalten aus der Produkttabelle ausgewählt habe ( SELECT * FROM ...
und im wirklichen Leben gibt es ungefähr 60 Spalten), wird Sort Method: external merge Disk: 5696kB
die Ausführungszeit verdoppelt. Und das nur für 4680 Produkte.
Aktionspunkt Nr. 1 (stammt aus Anmerkung Nr. 1): Um den Speicherbedarf des Sortiervorgangs zu verringern und ihn daher ein wenig zu beschleunigen, ist es ratsam, zuerst Produkt-IDs abzurufen, zu sortieren und zu begrenzen und dann vollständige Datensätze abzurufen:
SELECT * FROM products WHERE id IN (
SELECT id FROM products WHERE published AND category_ids @> ARRAY[23465]
ORDER BY score DESC, title LIMIT 20 OFFSET 8000
) ORDER BY score DESC, title;
Dies bringt uns zurück zu Sort Method: quicksort Memory: 903kB
und ~ 80 ms für 4680 Produkte. Kann immer noch langsam sein, wenn die Anzahl der Produkte auf 100.000 steigt.
quelle
score
kann NULL sein, aber Sie sortieren trotzdem nachscore DESC
, nichtscore DESC NULLS LAST
. Der eine oder andere scheint nicht richtig zu sein ...score
tatsächlich ist NICHT NULL - ich habe die Tabellendefinition korrigiert.Antworten:
Ich habe viel experimentiert und hier sind meine Ergebnisse.
GIN und Sortieren
Der GIN-Index (ab Version 9.4) kann die Bestellung nicht unterstützen .
work_mem
Vielen Dank an Chris für den Hinweis auf diesen Konfigurationsparameter . Der Standardwert ist 4 MB. Wenn Ihr Recordset größer ist, kann das Erhöhen
work_mem
auf den richtigen Wert (zu finden unterEXPLAIN ANALYSE
) die Sortiervorgänge erheblich beschleunigen.Starten Sie den Server neu, damit die Änderung wirksam wird, und überprüfen Sie dann Folgendes:
Ursprüngliche Abfrage
Ich habe meine Datenbank mit 650.000 Produkten gefüllt, wobei einige Kategorien bis zu 40.000 Produkte enthalten. Ich habe die Abfrage etwas vereinfacht, indem ich die folgende
published
Klausel entfernt habe:Wie wir sehen können,
work_mem
war dies nicht genugSort Method: external merge Disk: 29656kB
(die Anzahl hier ist ungefähr, es werden etwas mehr als 32 MB für In-Memory-QuickSort benötigt).Reduzieren Sie den Speicherbedarf
Wählen Sie keine vollständigen Datensätze zum Sortieren aus, verwenden Sie keine IDs, wenden Sie Sortieren, Versetzen und Begrenzen an und laden Sie dann nur 10 Datensätze, die wir benötigen:
Hinweis
Sort Method: quicksort Memory: 7396kB
. Ergebnis ist viel besser.JOIN und zusätzlicher B-Tree-Index
Wie Chris empfohlen hat, habe ich einen zusätzlichen Index erstellt:
Zuerst habe ich versucht, so beizutreten:
Der Abfrageplan unterscheidet sich geringfügig, das Ergebnis ist jedoch dasselbe:
Durch das Spielen mit verschiedenen Offsets und Produktzählungen konnte PostgreSQL keinen zusätzlichen B-Tree-Index verwenden.
Also bin ich klassisch gegangen und habe einen Kreuzungstisch erstellt :
Da der B-Tree-Index immer noch nicht verwendet wurde, passte die
work_mem
Ergebnismenge nicht , daher schlechte Ergebnisse.Unter bestimmten Umständen entscheidet sich PostgreSQL aufgrund der großen Anzahl von Produkten und des kleinen Offsets für die Verwendung des B-Tree-Index:
Dies ist in der Tat ziemlich logisch, da der B-Baum-Index hier kein direktes Ergebnis liefert, sondern nur als Leitfaden für den sequentiellen Scan verwendet wird.
Vergleichen wir mit der GIN-Abfrage:
Das Ergebnis von GIN ist viel besser. Ich überprüfte mit verschiedenen Kombinationen von Anzahl der Produkte und Offset, unter keinen Umständen war der Ansatz der Kreuzungstabelle besser .
Die Kraft des realen Index
Damit PostgreSQL den Index zum Sortieren vollständig nutzen kann, müssen sich alle Abfrageparameter
WHERE
sowieORDER BY
Parameter in einem einzelnen B-Baum-Index befinden. Dazu habe ich Sortierfelder vom Produkt in die Junction-Tabelle kopiert:Und dies ist das schlimmste Szenario mit einer großen Anzahl von Produkten in der ausgewählten Kategorie und einem großen Offset. Bei Offset = 300 beträgt die Ausführungszeit nur 0,5 ms.
Leider erfordert die Wartung eines solchen Verbindungstisches zusätzlichen Aufwand. Dies könnte über indizierte materialisierte Ansichten erreicht werden. Dies ist jedoch nur dann nützlich, wenn Ihre Daten selten aktualisiert werden, da das Aktualisieren einer solchen materialisierten Ansicht ein ziemlich schwerer Vorgang ist.
Daher bleibe ich bisher beim GIN-Index mit einer erhöhten
work_mem
und reduzierten Speicherabfrage.quelle
work_mem
Einstellung in postgresql.conf. Nachladen ist genug. Und lassen Sie mich davor warnen,work_mem
in einer Mehrbenutzerumgebung global zu hoch einzustellen (auch nicht zu niedrig). Wenn Sie einige Abfragen haben, die mehr benötigenwork_mem
, stellen Sie sie für die Sitzung nur mitSET
- oder nur für die Transaktion mit höher einSET LOCAL
. Siehe: dba.stackexchange.com/a/48633/3684Hier sind einige schnelle Tipps, die Ihnen helfen können, Ihre Leistung zu verbessern. Ich beginne mit dem einfachsten Tipp, der für Sie fast mühelos ist, und gehe nach dem ersten zum schwierigeren Tipp über.
1.
work_mem
Ich sehe also sofort, dass eine in Ihrem Erklärungsplan angegebene Sorte
Sort Method: external merge Disk: 5696kB
weniger als 6 MB verbraucht, aber auf die Festplatte verschüttet wird. Sie müssen Ihrework_mem
Einstellung in Ihrerpostgresql.conf
Datei erhöhen , damit die Sortierung in den Speicher passt.BEARBEITEN: Bei weiterer Prüfung
catgory_ids
stelle ich außerdem fest, dass der Bitmap-Index-Scan nach Verwendung des Index zur Überprüfung, ob er Ihren Kriterien entspricht, "verlustbehaftet" werden muss und die Bedingung beim Lesen der Zeilen auf den entsprechenden Heap-Seiten erneut überprüfen muss . In diesem Beitrag auf postgresql.org finden Sie eine bessere Erklärung als ich angegeben habe. : P Der Hauptpunkt ist, dass Ihrwork_mem
viel zu niedrig sind. Wenn Sie die Standardeinstellungen auf Ihrem Server nicht angepasst haben, funktioniert sie nicht gut.Für dieses Update benötigen Sie im Wesentlichen keine Zeit. Eine Änderung zu
postgresql.conf
, und Sie sind weg! Weitere Informationen finden Sie auf dieser Seite zur LeistungsoptimierungWeitere Tipps finden zur .2. Schemaänderung
Sie haben also in Ihrem Schemadesign die Entscheidung getroffen, das
category_ids
in ein ganzzahliges Array zu denormalisieren , wodurch Sie gezwungen werden, einen GIN- oder GIST-Index zu verwenden, um schnellen Zugriff zu erhalten. Nach meiner Erfahrung ist Ihre Auswahl eines GIN-Index für Lesevorgänge schneller als für einen GIST. In diesem Fall haben Sie also die richtige Wahl getroffen. GIN ist jedoch ein unsortierter Index. denken sie eher wie ein Schlüssel-Wert, wo Gleichheit Prädikate sind leicht zu überprüfen, aber Operationen wieWHERE >
,WHERE <
oderORDER BY
nicht durch den Index erleichtert.Ein vernünftiger Ansatz wäre, Ihr Design mithilfe eines Brückentisches / Junction-Tisches zu normalisieren , mit der viele-zu-viele-Beziehungen in Datenbanken angegeben werden.
In diesem Fall haben Sie viele Kategorien und eine Reihe entsprechender Ganzzahlen
category_id
, und Sie haben viele Produkte und die entsprechenden Ganzzahlenproduct_id
.category_id
Entfernen Sie anstelle einer Spalte in Ihrer Produkttabelle, die ein ganzzahliges Array von s ist, diese Array-Spalte aus Ihrem Schema und erstellen Sie eine Tabelle alsAnschließend können Sie B-Tree-Indizes für die beiden Spalten der Brückentabelle generieren.
Nur meine bescheidene Meinung, aber diese Änderungen können einen großen Unterschied für Sie machen. Probieren Sie das aus
work_mem
Änderung zumindest als erstes aus.Viel Glück!
BEARBEITEN:
Erstellen Sie einen zusätzlichen Index, um das Sortieren zu unterstützen
Wenn sich Ihre Produktlinie im Laufe der Zeit erweitert, können bestimmte Abfragen viele Ergebnisse liefern (Tausende, Zehntausende?), Die jedoch möglicherweise nur einen kleinen Teil Ihrer gesamten Produktlinie ausmachen. In diesen Fällen kann das Sortieren sogar ziemlich teuer sein, wenn es im Speicher durchgeführt wird, aber ein entsprechend gestalteter Index kann verwendet werden, um das Sortieren zu unterstützen.
Weitere Informationen finden Sie in der offiziellen PostgreSQL-Dokumentation, in der Indizes und ORDER BY beschrieben werden .
Wenn Sie einen Index erstellen, der Ihren
ORDER BY
Anforderungen entsprichtDann optimiert Postgres und entscheidet, ob die Verwendung des Index oder die Durchführung einer expliziten Sortierung kostengünstiger ist. Beachten Sie, dass es keine Garantie gibt dass Postgres den Index verwendet. Es wird versucht, die Leistung zu optimieren und zwischen der Verwendung des Index und der expliziten Sortierung zu wählen. Wenn Sie diesen Index erstellen, überwachen Sie ihn, um festzustellen, ob er ausreichend verwendet wird, um seine Erstellung zu rechtfertigen, und löschen Sie ihn, wenn die meisten Ihrer Sortierungen explizit durchgeführt werden.
Zu diesem Zeitpunkt wird Ihr "größter Knall fürs Geld" wahrscheinlich mehr verbrauchen
work_mem
, aber es gibt Fälle, in denen der Index das Sortieren unterstützen könnte.quelle
work_mem
Konfiguration war als Lösung für Ihr Problem beim Sortieren auf der Festplatte sowie für Ihr Problem beim erneuten Überprüfen des Zustands gedacht. Wenn die Anzahl der Produkte zunimmt, benötigen Sie möglicherweise einen zusätzlichen Index zum Sortieren. Bitte beachten Sie meine Änderungen oben zur Verdeutlichung.