Ich versuche festzustellen, welche Indizes für eine SQL-Abfrage mit einer WHERE
Bedingung verwendet werden sollen und GROUP BY
welche zurzeit sehr langsam ausgeführt wird.
Meine Frage:
SELECT group_id
FROM counter
WHERE ts between timestamp '2014-03-02 00:00:00.0' and timestamp '2014-03-05 12:00:00.0'
GROUP BY group_id
Die Tabelle enthält derzeit 32.000.000 Zeilen. Die Ausführungszeit der Abfrage erhöht sich um ein Vielfaches, wenn ich den Zeitrahmen verlängere.
Die betreffende Tabelle sieht folgendermaßen aus:
CREATE TABLE counter (
id bigserial PRIMARY KEY
, ts timestamp NOT NULL
, group_id bigint NOT NULL
);
Ich habe derzeit die folgenden Indizes, aber die Leistung ist immer noch langsam:
CREATE INDEX ts_index
ON counter
USING btree
(ts);
CREATE INDEX group_id_index
ON counter
USING btree
(group_id);
CREATE INDEX comp_1_index
ON counter
USING btree
(ts, group_id);
CREATE INDEX comp_2_index
ON counter
USING btree
(group_id, ts);
Wenn Sie EXPLAIN für die Abfrage ausführen, erhalten Sie das folgende Ergebnis:
"QUERY PLAN"
"HashAggregate (cost=467958.16..467958.17 rows=1 width=4)"
" -> Index Scan using ts_index on counter (cost=0.56..467470.93 rows=194892 width=4)"
" Index Cond: ((ts >= '2014-02-26 00:00:00'::timestamp without time zone) AND (ts <= '2014-02-27 23:59:00'::timestamp without time zone))"
SQL Fiddle mit Beispieldaten: http://sqlfiddle.com/#!15/7492b/1
Die Frage
Kann die Leistung dieser Abfrage durch Hinzufügen besserer Indizes verbessert werden oder muss ich die Verarbeitungsleistung erhöhen?
Bearbeiten 1
Es wird die PostgreSQL-Version 9.3.2 verwendet.
Bearbeiten 2
Ich habe @Erwins Vorschlag versucht mit EXISTS
:
SELECT group_id
FROM groups g
WHERE EXISTS (
SELECT 1
FROM counter c
WHERE c.group_id = g.group_id
AND ts BETWEEN timestamp '2014-03-02 00:00:00'
AND timestamp '2014-03-05 12:00:00'
);
Leider schien dies die Leistung nicht zu verbessern. Der Abfrageplan:
"QUERY PLAN"
"Nested Loop Semi Join (cost=1607.18..371680.60 rows=113 width=4)"
" -> Seq Scan on groups g (cost=0.00..2.33 rows=133 width=4)"
" -> Bitmap Heap Scan on counter c (cost=1607.18..158895.53 rows=60641 width=4)"
" Recheck Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
" -> Bitmap Index Scan on comp_2_index (cost=0.00..1592.02 rows=60641 width=0)"
" Index Cond: ((group_id = g.id) AND (ts >= '2014-01-01 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
Bearbeiten 3
Der Abfrageplan für die LATERAL-Abfrage von ypercube:
"QUERY PLAN"
"Nested Loop (cost=8.98..1200.42 rows=133 width=20)"
" -> Seq Scan on groups g (cost=0.00..2.33 rows=133 width=4)"
" -> Result (cost=8.98..8.99 rows=1 width=0)"
" One-Time Filter: ($1 IS NOT NULL)"
" InitPlan 1 (returns $1)"
" -> Limit (cost=0.56..4.49 rows=1 width=8)"
" -> Index Only Scan using comp_2_index on counter c (cost=0.56..1098691.21 rows=279808 width=8)"
" Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
" InitPlan 2 (returns $2)"
" -> Limit (cost=0.56..4.49 rows=1 width=8)"
" -> Index Only Scan Backward using comp_2_index on counter c_1 (cost=0.56..1098691.21 rows=279808 width=8)"
" Index Cond: ((group_id = $0) AND (ts IS NOT NULL) AND (ts >= '2010-03-02 00:00:00'::timestamp without time zone) AND (ts <= '2014-03-05 12:00:00'::timestamp without time zone))"
group_id
Werte gibt es auf dem Tisch?group_id
und nicht für eine Zählung?Antworten:
Eine andere Idee, die auch die
groups
Tabelle und eine Konstruktion namensLATERAL
join verwendet (für SQL-Server-Fans ist dies fast identisch mitOUTER APPLY
). Dies hat den Vorteil, dass Aggregate in der Unterabfrage berechnet werden können:Test bei SQL-Fiddle zeigt, dass die Abfrage Index-Scans für die ausführt
(group_id, ts)
Index durchführt.Ähnliche Pläne werden mit 2 seitlichen Verknüpfungen erstellt, eine für min und eine für max sowie mit 2 inline korrelierten Unterabfragen. Sie können auch verwendet werden, wenn Sie
counter
neben den Min- und Max-Daten die gesamten Zeilen anzeigen müssen:quelle
Da Sie kein Aggregat in der Auswahlliste haben,
group by
ist das so ziemlich dasselbe wie das Einfügen einesdistinct
in die Auswahlliste, oder?Wenn Sie dies wünschen, können Sie möglicherweise eine schnelle Indexsuche für comp_2_index durchführen, indem Sie diese umschreiben, um eine rekursive Abfrage zu verwenden, wie im PostgreSQL-Wiki beschrieben .
Erstellen Sie eine Ansicht, um die eindeutigen group_ids effizient zurückzugeben:
Verwenden Sie dann diese Ansicht anstelle der Nachschlagetabelle in Erwins
exists
Semi-Join.quelle
Da es nur gibt
133 different group_id's
, könnten Sieinteger
(oder sogarsmallint
) für die group_id verwenden. Es wird Ihnen jedoch nicht viel kosten, da das Auffüllen auf 8 Bytes den Rest in Ihrer Tabelle und mögliche mehrspaltige Indizes verschlingt. Die Verarbeitung von Plaininteger
sollte jedoch etwas schneller sein. Mehr zuint
vs.int2
.@Leo: Zeitstempel werden in modernen Installationen als 8-Byte-Ganzzahlen gespeichert und können perfekt schnell verarbeitet werden. Einzelheiten.
@ypercube: Der Index auf
(group_id, ts)
kann nicht helfen, da es keine Bedingung auf gibtgroup_id
die Abfrage .Ihr Hauptproblem ist die enorme Datenmenge, die verarbeitet werden muss:
Ich sehe, Sie interessieren sich nur für die Existenz einer
group_id
und keine tatsächliche Zählung. Auch gibt es nur 133 verschiedenegroup_id
s. Daher kann Ihre Anfrage mit dem ersten Treffer progorup_id
im Zeitrahmen zufrieden sein . Daher dieser Vorschlag für eine alternative Abfrage mit einemEXISTS
Semi-Join :Angenommen, eine Nachschlagetabelle für Gruppen:
Ihr Index
comp_2_index
für(group_id, ts)
wird jetzt maßgeblich.SQL-Geige (Aufbauend auf der von @ypercube in den Kommentaren bereitgestellten Geige)
Hier bevorzugt die Abfrage den Index für
(ts, group_id)
, aber ich denke, das liegt am Testaufbau mit "geclusterten" Zeitstempeln. Wenn Sie die Indizes mit führendemts
( mehr dazu ) entfernen , verwendet der Planer den Index auch gerne(group_id, ts)
- insbesondere bei einem Index-Only-Scan .In diesem Fall ist möglicherweise keine weitere mögliche Verbesserung erforderlich: Aggregieren Sie Daten in einer materialisierten Ansicht , um die Anzahl der Zeilen drastisch zu reduzieren. Dies würde Sinn insbesondere machen, wenn Sie auch tatsächlich benötigen zählen zusätzlich. Dann haben Sie die Kosten, um viele Zeilen einmalig zu verarbeiten, wenn Sie die MV aktualisieren. Sie können sogar tägliche und stündliche Aggregate (zwei separate Tabellen) kombinieren und Ihre Abfrage daran anpassen.
Sind Zeitrahmen in Ihren Abfragen beliebig? Oder meistens an vollen Minuten / Stunden / Tagen?
Erstellen Sie die erforderlichen Indizes für
counter_mv
und passen Sie Ihre Abfrage an, um damit zu arbeiten ...quelle
groups
Tabelle den Unterschied?ANALYZE
macht einen Unterschied. Aber Indizes auf werdencounter
auch ohne verwendetANALYZE
, sobald ich diegroups
Tabelle einführe . Punkt ist, dass ohne diese Tabelle ohnehin ein seqscan benötigt wird, um die Menge der möglichen group_id´s zu erstellen. Ich habe meiner Antwort mehr hinzugefügt. Und danke für deine Geige!group_id
auch für eineSELECT DISTINCT group_id FROM t;
Abfrage nicht verwendet?LIMIT 1
kann ein Bitmap-Index-Scan ausgewählt werden, der nicht vom frühen Anhalten profitiert und viel länger dauert. (Wenn die Tabelle jedoch frisch gesaugt ist, wird möglicherweise der Index-Scan dem Bitmap-Scan vorgezogen. Welches Verhalten angezeigt wird, hängt also vom Vakuumstatus der Tabelle ab.)