Langsame Abfrage einer großen Tabelle mit GROUP BY und ORDER BY

14

Ich habe eine Tabelle mit 7,2 Millionen Tupeln, die so aussieht:

                               table public.methods
 column |          type         |                      attributes
--------+-----------------------+----------------------------------------------------
 id     | integer               | not null DEFAULT nextval('methodkey'::regclass)
 hash   | character varying(32) | not null
 string | character varying     | not null
 method | character varying     | not null
 file   | character varying     | not null
 type   | character varying     | not null
Indexes:
    "methods_pkey" PRIMARY KEY, btree (id)
    "methodhash" btree (hash)

Jetzt möchte ich einige Werte auswählen, aber die Abfrage ist unglaublich langsam:

db=# explain 
    select hash, string, count(method) 
    from methods 
    where hash not in 
          (select hash from nostring) 
    group by hash, string 
    order by count(method) desc;
                                            QUERY PLAN
----------------------------------------------------------------------------------------
 Sort  (cost=160245190041.10..160245190962.07 rows=368391 width=182)
   Sort Key: (count(methods.method))
   ->  GroupAggregate  (cost=160245017241.77..160245057764.73 rows=368391 width=182)
       ->  Sort  (cost=160245017241.77..160245026451.53 rows=3683905 width=182)
             Sort Key: methods.hash, methods.string
             ->  Seq Scan on methods  (cost=0.00..160243305942.27 rows=3683905 width=182)
                   Filter: (NOT (SubPlan 1))
                   SubPlan 1
                   ->  Materialize  (cost=0.00..41071.54 rows=970636 width=33)
                     ->  Seq Scan on nostring  (cost=0.00..28634.36 rows=970636 width=33)

Die hashSpalte ist der MD5-Hash von stringund hat einen Index. Mein Problem ist also, dass die gesamte Tabelle nach ID und nicht nach Hash sortiert ist. Es dauert also eine Weile, sie zuerst zu sortieren und dann zu gruppieren.

Die Tabelle nostringenthält nur eine Liste von Hashes, die ich nicht haben möchte. Aber ich brauche beide Tabellen, um alle Werte zu haben. Es ist also keine Option, diese zu löschen.

Zusätzliche Informationen: Keine der Spalten darf null sein (wie in der Tabellendefinition festgelegt) und ich verwende postgresql 9.2.

reox
quelle
1
Geben Sie immer die Version von PostgreSQL an, die Sie verwenden. Wie hoch ist der Prozentsatz der NULLWerte in der Spalte method? Gibt es Duplikate auf string?
Erwin Brandstetter

Antworten:

18

Die Antwort vonLEFT JOIN in @ dezso sollte gut sein. Ein Index ist jedoch (per se) kaum sinnvoll, da die Abfrage ohnehin die gesamte Tabelle lesen muss - mit Ausnahme von Index-Scans in Postgres 9.2+ und günstigen Bedingungen, siehe unten.

SELECT m.hash, m.string, count(m.method) AS method_ct
FROM   methods m
LEFT   JOIN nostring n USING (hash)
WHERE  n.hash IS NULL
GROUP  BY m.hash, m.string 
ORDER  BY count(m.method) DESC;

Führen Sie EXPLAIN ANALYZEdie Abfrage aus. Mehrmals, um Kasseneffekte und Lärm auszuschließen. Vergleichen Sie die besten Ergebnisse.

Erstellen Sie einen mehrspaltigen Index, der Ihrer Suchanfrage entspricht:

CREATE INDEX methods_cluster_idx ON methods (hash, string, method);

Warten? Nachdem ich sagte, ein Index würde nicht helfen? Nun, wir brauchen es zum CLUSTERTisch:

CLUSTER methods USING methods_cluster_idx;
ANALYZE methods;

Erneut ausführen EXPLAIN ANALYZE. Noch schneller? Es sollte sein.

CLUSTERist eine einmalige Operation zum Umschreiben der gesamten Tabelle in der Reihenfolge des verwendeten Index. Es ist auch effektiv ein VACUUM FULL. Wenn Sie sicher sein möchten, führen Sie einen Vortest mit VACUUM FULLallein durch, um zu sehen, was darauf zurückzuführen ist.

Wenn in Ihrer Tabelle viele Schreibvorgänge ausgeführt werden, nimmt der Effekt mit der Zeit ab. Planen Sie CLUSTERaußerhalb der Geschäftszeiten, um den Effekt wiederherzustellen. Die Feinabstimmung hängt von Ihrem genauen Anwendungsfall ab. Das Handbuch zu CLUSTER.

CLUSTERist ein eher grobes Werkzeug, braucht eine exklusive Sperre auf dem Tisch. Wenn Sie sich das nicht leisten können, überlegen Sie, pg_repackwas Sie auch ohne exklusive Sperre tun können. Mehr in dieser späteren Antwort:


Wenn der Prozentsatz der NULLWerte in der Spalte methodhoch ist (mehr als ~ 20 Prozent, abhängig von der tatsächlichen Zeilengröße), sollte ein Teilindex helfen:

CREATE INDEX methods_foo_idx ON methods (hash, string)
WHERE method IS NOT NULL;

(In Ihrem späteren Update werden Ihre Spalten als NOT NULLnicht zutreffend angezeigt.)

Wenn Sie PostgreSQL 9.2 oder höher ausführen (wie von @deszo kommentiert ), können die angezeigten Indizes nützlich sein, ohne CLUSTERdass der Planer nur Index-Scans verwenden kann . Nur unter günstigen Bedingungen anwendbar: Keine Schreibvorgänge, die sich auf die Sichtbarkeitskarte auswirken würden, da die letzte VACUUMund alle Spalten in der Abfrage vom Index abgedeckt werden müssen. Grundsätzlich können schreibgeschützte Tabellen dies jederzeit verwenden, während stark geschriebene Tabellen begrenzt sind. Weitere Details im Postgres Wiki.

Der oben erwähnte Teilindex könnte in diesem Fall sogar noch nützlicher sein.

Wenn die Spalte hingegen keine NULL Werte enthält method, sollten Sie
1.) diese definieren NOT NULLund
2.) count(*)stattdessen verwenden count(method), das ist etwas schneller und funktioniert auch ohne NULLWerte.

Wenn Sie diese Abfrage häufig aufrufen müssen und die Tabelle schreibgeschützt ist, erstellen Sie eine MATERIALIZED VIEW.


Exotischer Feinschliff: Ihre Tabelle hat einen Namen nostring, scheint jedoch Hashes zu enthalten. Wenn Sie Hashes anstelle von Zeichenfolgen ausschließen, besteht die Möglichkeit, dass Sie mehr Zeichenfolgen als beabsichtigt ausschließen. Sehr unwahrscheinlich, aber möglich.

Erwin Brandstetter
quelle
Mit dem Cluster ist es viel schneller. Ich brauche noch ungefähr 5 Minuten für die Abfrage, aber das ist viel besser als es die ganze Nacht laufen zu lassen: D
reox
@reox: Seit Sie v9.2 ausführen: Haben Sie vor dem Clustering nur mit dem Index getestet? Wäre interessant, wenn du einen Unterschied siehst. (Sie können den Unterschied nach dem Clustering nicht reproduzieren.) Zeigt EXPLAIN jetzt auch (und das wäre billig) einen Index- oder einen vollständigen Tabellenscan an?
Erwin Brandstetter
5

Willkommen bei DBA.SE!

Sie können versuchen, Ihre Anfrage wie folgt neu zu formulieren:

SELECT m.hash, string, count(method) 
FROM 
    methods m
    LEFT JOIN nostring n ON m.hash = n.hash
WHERE n.hash IS NULL
GROUP BY hash, string 
ORDER BY count(method) DESC;

oder eine andere Möglichkeit:

SELECT m.hash, string, count(method) 
FROM 
    methods m
WHERE NOT EXISTS (SELECT hash FROM nostring WHERE hash = m.hash)
GROUP BY hash, string 
ORDER BY count(method) DESC;

NOT IN ist eine typische Leistungssenke, da es schwierig ist, einen Index damit zu verwenden.

Dies kann durch Indizes weiter verbessert werden. Ein Index über nostring.hashsieht nützlich aus. Aber zuerst: Was bekommen Sie jetzt? (Es wäre besser, die Ausgabe von zu sehenEXPLAIN ANALYZE da die Kosten selbst nicht die Zeit angeben, die die Operationen in genommen haben.)

dezso
quelle
Auf nostring.hash wird bereits ein Index erstellt, aber ich glaube, Postgres verwenden ihn nicht, weil es zu viele Tupel gibt. Wenn ich den linken Join benutze, bekomme ich 32 Millionen, also ist es viel besser ... aber ich versuche, es weiter zu optimieren ...
Reox
3
Die Kosten sind nur für den Planer in der Lage, einen ausreichend guten Plan zu wählen. Die tatsächlichen Zeiten korrelieren normalerweise damit, aber nicht unbedingt. Wenn Sie also sicher sein möchten, verwenden Sie EXPLAIN ANALYZE.
29.
1

Da Hash ein md5 ist, können Sie wahrscheinlich versuchen, es in eine Zahl umzuwandeln: Sie können es als Zahl speichern oder einfach einen Funktionsindex erstellen, der diese Zahl in einer unveränderlichen Funktion berechnet.

Andere haben bereits eine Funktion pl / pgsql erstellt, die einen md5-Wert (teilweise) von Text in Zeichenfolge konvertiert. Ein Beispiel finden Sie unter /programming/9809381/hashing-a-string-to-a-numeric-value-in-postgressql

Ich glaube, dass Sie beim Durchsuchen des Index wirklich viel Zeit mit dem Vergleichen von Zeichenfolgen verbringen. Wenn Sie es schaffen, diesen Wert als Zahl zu speichern, sollte er wirklich schneller sein.

eppesuig
quelle
1
Ich bezweifle, dass diese Umstellung die Dinge beschleunigen würde. Alle hier aufgeführten Abfragen verwenden zum Vergleich die Gleichheit. Die Berechnung numerischer Darstellungen und die anschließende Überprüfung auf Gleichheit verspricht mir keine großen Gewinne.
Dezso
2
Ich glaube, ich würde md5 aus Platzgründen eher als bytea speichern als als eine Zahl: sqlfiddle.com/#!12/d41d8/252
Jack sagt, versuchen Sie es mit topanswers.xyz
Willkommen auch bei dba.se!
Jack sagt, versuchen Sie topanswers.xyz
@JackDouglas: Interessanter Kommentar! 16 Byte pro md5 anstelle von 32 sind für große Tabellen einiges.
Erwin Brandstetter
0

Ich bin häufig auf dieses Problem gestoßen und habe einen einfachen zweiteiligen Trick entdeckt.

  1. Erstellen Sie einen Teilstring-Index für den Hash-Wert: (7 ist normalerweise eine gute Länge.)

    create index methods_idx_hash_substring ON methods(substring(hash,1,7))

  2. Lassen Sie Ihre Suchen / Verknüpfungen eine Teilzeichenfolgenübereinstimmung enthalten, sodass der Abfrageplaner darauf hingewiesen wird, den Index zu verwenden:

    alt: WHERE hash = :kwarg

    Neu: WHERE (hash = :kwarg) AND (substring(hash,1,7) = substring(:kwarg,1,7))

Sie sollten auch einen Index für die Rohdaten haben hash .

Das Ergebnis ist (normalerweise), dass der Planer zuerst den Teilzeichenfolgenindex konsultiert und die meisten Zeilen aussortiert. Dann wird der gesamte 32-Zeichen-Hash mit dem entsprechenden Index (oder der entsprechenden Tabelle) abgeglichen. Dieser Ansatz hat 800-ms-Abfragen für mich auf 4 reduziert.

Jonathan Vanasco
quelle