PostgreSQL-Abfrage sehr langsam, wenn Unterabfrage hinzugefügt wird

9

Ich habe eine relativ einfache Abfrage für eine Tabelle mit 1,5 Millionen Zeilen:

SELECT mtid FROM publication
WHERE mtid IN (9762715) OR last_modifier=21321
LIMIT 5000;

EXPLAIN ANALYZE Ausgabe:

Limit  (cost=8.84..12.86 rows=1 width=8) (actual time=0.985..0.986 rows=1 loops=1)
  ->  Bitmap Heap Scan on publication  (cost=8.84..12.86 rows=1 width=8) (actual time=0.984..0.985 rows=1 loops=1)
        Recheck Cond: ((mtid = 9762715) OR (last_modifier = 21321))
        ->  BitmapOr  (cost=8.84..8.84 rows=1 width=0) (actual time=0.971..0.971 rows=0 loops=1)
              ->  Bitmap Index Scan on publication_pkey  (cost=0.00..4.42 rows=1 width=0) (actual time=0.295..0.295 rows=1 loops=1)
                    Index Cond: (mtid = 9762715)
              ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..4.42 rows=1 width=0) (actual time=0.674..0.674 rows=0 loops=1)
                    Index Cond: (last_modifier = 21321)
Total runtime: 1.027 ms

So weit so gut, schnell und nutzt die verfügbaren Indizes.
Wenn ich eine Abfrage nur ein wenig ändere, lautet das Ergebnis:

SELECT mtid FROM publication
WHERE mtid IN (SELECT 9762715) OR last_modifier=21321
LIMIT 5000;

Die EXPLAIN ANALYZEAusgabe ist:

Limit  (cost=0.01..2347.74 rows=5000 width=8) (actual time=2735.891..2841.398 rows=1 loops=1)
  ->  Seq Scan on publication  (cost=0.01..349652.84 rows=744661 width=8) (actual time=2735.888..2841.393 rows=1 loops=1)
        Filter: ((hashed SubPlan 1) OR (last_modifier = 21321))
        SubPlan 1
          ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1)
Total runtime: 2841.442 ms

Nicht so schnell und mit seq scan ...

Natürlich ist die ursprüngliche Abfrage, die von der Anwendung ausgeführt wird, etwas komplexer und sogar langsamer, und das im Ruhezustand generierte Original ist es natürlich nicht (SELECT 9762715), aber die Langsamkeit ist auch dafür da (SELECT 9762715)! Die Abfrage wird im Ruhezustand generiert, daher ist es eine ziemliche Herausforderung, sie zu ändern, und einige Funktionen sind nicht verfügbar (z. B. UNIONnicht verfügbar, was schnell wäre).

Die Fragen

  1. Warum kann der Index im zweiten Fall nicht verwendet werden? Wie könnten sie verwendet werden?
  2. Kann ich die Abfrageleistung auf andere Weise verbessern?

Zusätzliche Gedanken

Es scheint, dass wir den ersten Fall verwenden könnten, indem wir manuell SELECT ausführen und dann die resultierende Liste in die Abfrage einfügen. Selbst mit 5000 Zahlen in der IN () -Liste ist es viermal schneller als die zweite Lösung. Es scheint jedoch nur FALSCH (außerdem könnte es 100-mal schneller sein :)). Es ist völlig unverständlich, warum der Abfrageplaner für diese beiden Abfragen eine völlig andere Methode verwendet. Daher möchte ich eine bessere Lösung für dieses Problem finden.

P.Péter
quelle
Können Sie Ihren Code irgendwie so umschreiben, dass der Ruhezustand ein JOINanstelle des generiert IN ()? Außerdem wurde publicationkürzlich analysiert?
Dekso
Ja, ich habe sowohl VACUUM ANALYZE als auch VACUUM FULL durchgeführt. Es gab keine Änderung der Leistung. Beim zweiten AFAIR haben wir das versucht und es hat die Abfrageleistung nicht wesentlich beeinflusst.
P.Péter
1
Wenn Hibernate keine ordnungsgemäße Abfrage generiert, warum verwenden Sie nicht einfach Raw SQL? Das ist so, als würden Sie auf Google Übersetzer bestehen, während Sie bereits besser wissen, wie man es auf Englisch ausdrückt. Zu Ihrer Frage: Es hängt wirklich von der tatsächlichen Abfrage ab, die dahinter verborgen ist (SELECT 9762715).
Erwin Brandstetter
Wie ich unten erwähnt habe, ist es langsam, selbst wenn die innere Abfrage ist (SELECT 9762715) . Zur Frage zum Ruhezustand: Dies könnte durchgeführt werden, erfordert jedoch ein ernsthaftes Umschreiben des Codes, da wir benutzerdefinierte Kriterienabfragen für den Ruhezustand haben, die im laufenden Betrieb übersetzt werden. Im Wesentlichen würden wir also den Ruhezustand ändern, was ein großes Unterfangen mit vielen möglichen Nebenwirkungen ist.
P.Péter

Antworten:

6

Mein Kollege hat einen Weg gefunden, die Abfrage so zu ändern, dass sie einfach umgeschrieben werden muss und das tut, was sie tun muss, dh die Unterauswahl in einem Schritt und dann die weiteren Operationen am Ergebnis ausführen:

SELECT mtid FROM publication 
WHERE 
  mtid = ANY( (SELECT ARRAY(SELECT 9762715))::bigint[] )
  OR last_modifier=21321
LIMIT 5000;

Die EXPLAIN-Analyse lautet nun:

 Limit  (cost=92.58..9442.38 rows=2478 width=8) (actual time=0.071..0.074 rows=1 loops=1)
   InitPlan 2 (returns $1)
     ->  Result  (cost=0.01..0.02 rows=1 width=0) (actual time=0.010..0.011 rows=1 loops=1)
           InitPlan 1 (returns $0)
             ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.002 rows=1 loops=1)
   ->  Bitmap Heap Scan on publication  (cost=92.56..9442.36 rows=2478 width=8) (actual time=0.069..0.070 rows=1 loops=1)
         Recheck Cond: ((mtid = ANY (($1)::bigint[])) OR (last_modifier = 21321))
         Heap Blocks: exact=1
         ->  BitmapOr  (cost=92.56..92.56 rows=2478 width=0) (actual time=0.060..0.060 rows=0 loops=1)
               ->  Bitmap Index Scan on publication_pkey  (cost=0.00..44.38 rows=10 width=0) (actual time=0.046..0.046 rows=1 loops=1)
                     Index Cond: (mtid = ANY (($1)::bigint[]))
               ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..46.94 rows=2468 width=0) (actual time=0.011..0.011 rows=0 loops=1)
                     Index Cond: (last_modifier = 21321)
 Planning time: 0.704 ms
 Execution time: 0.153 ms

Es scheint, dass wir einen einfachen Parser erstellen können, der alle Unterauswahlen auf diese Weise findet und neu schreibt und ihn einem Hook im Ruhezustand hinzufügt, um die native Abfrage zu bearbeiten.

P.Péter
quelle
Das hört sich witzig an. Ist es nicht einfacher, alle SELECTs zu entfernen , wie Sie es in Ihrer ersten Abfrage in der Frage getan haben?
Dekso
Natürlich könnte ich einen zweistufigen Ansatz machen: einen SELECTseparaten und dann die äußere Auswahl mit einer statischen Liste nach dem IN. Dies ist jedoch erheblich langsamer (5-10 Mal, wenn die Unterabfrage mehr als ein paar Ergebnisse enthält), da Sie zusätzliche Netzwerkrundfahrten durchführen und Postgres viele Ergebnisse formatieren und diese Ergebnisse dann von Java analysieren (und dann ausführen) das gleiche nochmal rückwärts). Die obige Lösung macht das gleiche semantisch, während der Prozess innerhalb von Postgres verbleibt. Alles in allem scheint dies derzeit der schnellste Weg mit der kleinsten Änderung in unserem Fall zu sein.
P.Péter
Ah ich sehe. Was ich nicht wusste ist, dass Sie viele IDs gleichzeitig erhalten können.
Dekso
5

Der Kern des Problems wird hier offensichtlich:

Seq Scan bei Veröffentlichung (Kosten = 0,01..349652,84 Zeilen = 744661 Breite = 8) (tatsächliche Zeit = 2735,888..2841,393 Zeilen = 1 Schleifen = 1)

Postgres schätzt , dass 744661 Zeilen zurückgegeben werden, während es sich tatsächlich um eine einzelne Zeile handelt. Wenn Postgres nicht besser weiß, was von der Abfrage zu erwarten ist, kann es nicht besser planen. Wir müssten die eigentliche Abfrage dahinter verbergen (SELECT 9762715)- und wahrscheinlich auch Tabellendefinition, Einschränkungen, Kardinalitäten und Datenverteilung kennen. Offensichtlich kann Postgres nicht vorhersagen, wie wenige Zeilen von Postgres zurückgegeben werden. Je nachdem, um was es sich handelt, gibt es möglicherweise Möglichkeiten, die Abfrage neu zu schreiben .

Wenn Sie wissen, dass die Unterabfrage niemals mehr als nZeilen zurückgeben kann, können Sie Postgres einfach Folgendes mitteilen:

SELECT mtid
FROM   publication
WHERE  mtid IN (SELECT ... LIMIT n) --  OR last_modifier=21321
LIMIT  5000;

Wenn n klein genug ist, wechselt Postgres zu (Bitmap-) Index-Scans. Dies funktioniert jedoch nur für den einfachen Fall. Arbeitet beim Hinzufügen einer ORBedingung nicht mehr: Der Abfrageplaner kann dies derzeit nicht bewältigen.

Ich benutze IN (SELECT ...)es selten, um damit zu beginnen. In der Regel gibt es eine bessere Möglichkeit, dasselbe zu implementieren, häufig mit einem EXISTSSemi-Join. Manchmal mit einem ( LEFT) JOIN( LATERAL) ...

Die offensichtliche Problemumgehung wäre die Verwendung UNION, aber Sie haben dies ausgeschlossen. Mehr kann ich nicht sagen, ohne die eigentliche Unterabfrage und andere relevante Details zu kennen.

Erwin Brandstetter
quelle
2
Dahinter verbirgt sich keine Abfrage(SELECT 9762715) ! Wenn ich genau diese Abfrage ausführe, die Sie oben sehen. Natürlich ist die ursprüngliche Abfrage im Ruhezustand etwas komplizierter, aber ich (glaube ich) habe es geschafft, genau zu bestimmen, wo der Abfrageplaner in die Irre geht, und habe diesen Teil der Abfrage vorgestellt. Die obigen Erklärungen und Abfragen sind jedoch wörtlich Strg-Lebenslauf.
P.Péter
Was den zweiten Teil betrifft, funktioniert die innere Grenze nicht: führt EXPLAIN ANALYZE SELECT mtid FROM publication WHERE mtid IN (SELECT 9762715 LIMIT 1) OR last_modifier=21321 LIMIT 5000;auch einen sequentiellen Scan durch und läuft auch ungefähr 3 Sekunden lang ...
P.Péter
@ P.Péter: Es funktioniert für mich in meinem lokalen Test mit einer tatsächlichen Unterabfrage auf Postgres 9.4. Wenn das, was Sie anzeigen, Ihre eigentliche Abfrage ist, haben Sie bereits Ihre Lösung: Verwenden Sie die erste Abfrage in Ihrer Frage mit einer Konstanten anstelle einer Unterabfrage.
Erwin Brandstetter
Nun, ich habe auch eine Unterabfrage für eine neue Testtabelle versucht : CREATE TABLE test (mtid bigint NOT NULL, last_modifier bigint, CONSTRAINT test_property_pkey PRIMARY KEY (mtid)); CREATE INDEX test_last_modifier_btree ON test USING btree (last_modifier); INSERT INTO test (mtid, last_modifier) SELECT mtid, last_modifier FROM publication;. Und der Effekt war immer noch für die gleichen Abfragen da test: Jede Unterabfrage führte zu einem seq-Scan ... Ich habe sowohl 9.1 als auch 9.4 ausprobiert. Der Effekt ist der gleiche.
P.Péter
1
@ P.Péter: Ich habe den Test erneut durchgeführt und festgestellt, dass ich ohne die ORBedingung getestet habe . Der Trick mit LIMITfunktioniert nur für den einfacheren Fall.
Erwin Brandstetter
1

Antwort auf eine zweite Frage: Ja, Sie können Ihrer Unterabfrage ORDER BY hinzufügen, was sich positiv auswirkt. Die Leistung ähnelt jedoch der Lösung "EXISTS (Unterabfrage)". Selbst bei Unterabfragen, die zu zwei Zeilen führen, gibt es einen signifikanten Unterschied.

SELECT mtid FROM publication
WHERE mtid IN (SELECT #column# ORDER BY #column#) OR last_modifier=21321
LIMIT 5000;
iki
quelle