Indizes für SQL-Abfragen mit WHERE-Bedingung und GROUP BY

15

Ich versuche festzustellen, welche Indizes für eine SQL-Abfrage mit einer WHEREBedingung verwendet werden sollen und GROUP BYwelche 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))"
uldall
quelle
Wie viele verschiedene group_idWerte gibt es auf dem Tisch?
Ypercubeᵀᴹ
Es gibt 133 verschiedene group_ids.
Die Zeitstempel reichen von 2011 bis 2014. Sowohl Sekunden als auch Millisekunden werden verwendet.
Interessieren Sie sich nur für group_idund nicht für eine Zählung?
Erwin Brandstetter
@Erwin Wir interessieren uns auch für max () und (min) in einer vierten Spalte, die im Beispiel nicht gezeigt wird.
uldall

Antworten:

6

Eine andere Idee, die auch die groupsTabelle und eine Konstruktion namens LATERALjoin verwendet (für SQL-Server-Fans ist dies fast identisch mit OUTER APPLY). Dies hat den Vorteil, dass Aggregate in der Unterabfrage berechnet werden können:

SELECT group_id, min_ts, max_ts
FROM   groups g,                    -- notice the comma here, is required
  LATERAL 
       ( SELECT MIN(ts) AS min_ts,
                MAX(ts) AS max_ts
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2011-03-02 00:00:00'
                        AND timestamp '2013-03-05 12:00:00'
       ) x 
WHERE min_ts IS NOT NULL ;

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 counterneben den Min- und Max-Daten die gesamten Zeilen anzeigen müssen:

SELECT group_id, 
       min_ts, min_ts_id, 
       max_ts, max_ts_id 
FROM   groups g
  , LATERAL 
       ( SELECT ts AS min_ts, c.id AS min_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts ASC
         LIMIT 1
       ) xmin
  , LATERAL 
       ( SELECT ts AS max_ts, c.id AS max_ts_id
         FROM counter c
         WHERE c.group_id = g.group_id
           AND c.ts BETWEEN timestamp '2012-03-02 00:00:00'
                        AND timestamp '2014-03-05 12:00:00'
         ORDER BY ts DESC 
         LIMIT 1
       ) xmax
WHERE min_ts IS NOT NULL ;
ypercubeᵀᴹ
quelle
@ypercube Ich habe den Abfrageplan für Ihre Abfrage zur ursprünglichen Frage hinzugefügt. Die Abfrage läuft auch bei großen Zeitspannen unter 50 ms.
uldall
5

Da Sie kein Aggregat in der Auswahlliste haben, group byist das so ziemlich dasselbe wie das Einfügen eines distinctin 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:

create or replace view groups as
WITH RECURSIVE t AS (
             SELECT min(counter.group_id) AS group_id
               FROM counter
    UNION ALL
             SELECT ( SELECT min(counter.group_id) AS min
                       FROM counter
                      WHERE counter.group_id > t.group_id) AS min
               FROM t
              WHERE t.group_id IS NOT NULL
    )
     SELECT t.group_id
       FROM t
      WHERE t.group_id IS NOT NULL
UNION ALL
     SELECT NULL::bigint AS col
      WHERE (EXISTS ( SELECT counter.id,
                counter.ts,
                counter.group_id
               FROM counter
              WHERE counter.group_id IS NULL));

Verwenden Sie dann diese Ansicht anstelle der Nachschlagetabelle in Erwins existsSemi-Join.

jjanes
quelle
4

Da es nur gibt 133 different group_id's, könnten Sie integer(oder sogar smallint) 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 Plain integersollte jedoch etwas schneller sein. Mehr zu intvs.int2 .

CREATE TABLE counter (
    id bigserial PRIMARY KEY
  , ts timestamp NOT NULL
  , group_id int NOT NULL
);

@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:

Indexsuche mit ts_index am Zähler (Kosten = 0,56..467470,93 Zeilen = 194892 Breite = 4)

Ich sehe, Sie interessieren sich nur für die Existenz einer group_idund keine tatsächliche Zählung. Auch gibt es nur 133 verschiedene group_ids. Daher kann Ihre Anfrage mit dem ersten Treffer pro gorup_idim Zeitrahmen zufrieden sein . Daher dieser Vorschlag für eine alternative Abfrage mit einem EXISTSSemi-Join :

Angenommen, eine Nachschlagetabelle für Gruppen:

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'
   );

Ihr Index comp_2_indexfü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ührendem ts( 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?

CREATE MATERIALIZED VIEW counter_mv AS
SELECT date_trunc('hour', ts) AS hour
     , group_id
     , count(*) AS ct
GROUP BY 1,2
ORDER BY 1,2;

Erstellen Sie die erforderlichen Indizes für counter_mvund passen Sie Ihre Abfrage an, um damit zu arbeiten ...

Erwin Brandstetter
quelle
1
Ich habe in SQL-Fiddle mehrere ähnliche Dinge mit 10.000 Zeilen ausprobiert , aber alle zeigten einen sequentiellen Scan. Macht die Verwendung der groupsTabelle den Unterschied?
Ypercubeᵀᴹ
@ypercube: Ich denke schon. Auch ANALYZEmacht einen Unterschied. Aber Indizes auf werden counterauch ohne verwendet ANALYZE, sobald ich die groupsTabelle 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!
Erwin Brandstetter
Das ist seltsam. Sie sagen, dass der Postgres-Optimierer den Index group_idauch für eine SELECT DISTINCT group_id FROM t;Abfrage nicht verwendet?
Ypercubeᵀᴹ
1
@ErwinBrandstetter Das dachte ich auch und war sehr überrascht, etwas anderes herauszufinden. Ohne a LIMIT 1kann 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.)
Jjanes
1
@uldall: Tägliche Aggregate reduzieren die Anzahl der Zeilen drastisch. Das sollte den Trick machen. Probieren Sie aber unbedingt die EXISTS-Abfrage aus. Es könnte überraschend schnell sein. Funktioniert nicht zusätzlich für min / max. Ich würde mich jedoch für die resultierende Leistung interessieren, wenn Sie so freundlich wären, hier eine Zeile zu schreiben.
Erwin Brandstetter