it-swarm.com.de

Wie kann DISTINCT ON in PostgreSQL schneller gemacht werden?

Ich habe eine Tabelle station_logs In einer PostgreSQL 9.6-Datenbank:

    Column     |            Type             |    
---------------+-----------------------------+
 id            | bigint                      | bigserial
 station_id    | integer                     | not null
 submitted_at  | timestamp without time zone | 
 level_sensor  | double precision            | 
Indexes:
    "station_logs_pkey" PRIMARY KEY, btree (id)
    "uniq_sid_sat" UNIQUE CONSTRAINT, btree (station_id, submitted_at)

Ich versuche, den letzten level_sensor Wert basierend auf submitted_at Für jeden station_id Zu erhalten. Es gibt ungefähr 400 eindeutige station_id - Werte und ungefähr 20.000 Zeilen pro Tag pro station_id.

Vor dem Erstellen eines Index:

EXPLAIN ANALYZE
SELECT DISTINCT ON(station_id) station_id, submitted_at, level_sensor
FROM station_logs ORDER BY station_id, submitted_at DESC;
 Eindeutig (Kosten = 4347852.14..4450301.72 Zeilen = 89 Breite = 20) (tatsächliche Zeit = 22202.080..27619.167 Zeilen = 98 Schleifen = 1) 
 -> Sortieren (Kosten = 4347852.14..4399076.93 Zeilen = 20489916 Breite = 20) (tatsächliche Zeit = 22202.077..26540.827 Zeilen = 20489812 Schleifen = 1) 
 Sortierschlüssel: station_id, submit_at DESC 
 Sortiermethode: externe Zusammenführung Datenträger: 681040kB 
 -> Seq Scannen in station_logs (Kosten = 0,00..598895.16 Zeilen = 20489916 Breite = 20) (tatsächliche Zeit = 0.023..3443.587 Zeilen = 20489812 Schleifen = $ 
 Planungszeit: 0.072 ms 
 Ausführungszeit: 27690.644 Frau

Index erstellen:

CREATE INDEX station_id__submitted_at ON station_logs(station_id, submitted_at DESC);

Nach dem Erstellen des Index für dieselbe Abfrage:

 Eindeutig (Kosten = 0,56..2156367.51 Zeilen = 89 Breite = 20) (tatsächliche Zeit = 0.184..16263.413 Zeilen = 98 Schleifen = 1) 
 -> Index-Scan mit station_id__submitted_at auf station_logs (Kosten = 0.56..2105142.98 Zeilen = 20489812 Breite = 20) (tatsächliche Zeit = 0,181..1 $ 
 Planungszeit: 0,206 ms 
 Ausführungszeit: 16263,490 ms

Gibt es eine Möglichkeit, diese Abfrage zu beschleunigen? Wie zum Beispiel 1 Sekunde sind 16 Sekunden immer noch zu viel.

13
Kokizzu

Für nur 400 Stationen ist diese Abfrage massiv schneller:

SELECT s.station_id, l.submitted_at, l.level_sensor
FROM   station s
CROSS  JOIN LATERAL (
   SELECT submitted_at, level_sensor
   FROM   station_logs
   WHERE  station_id = s.station_id
   ORDER  BY submitted_at DESC NULLS LAST
   LIMIT  1
   ) l;

dbfiddle hier
(Vergleichen Sie die Pläne für diese Abfrage, Abelistos Alternative und Ihr Original.)

Ergebnis EXPLAIN ANALYZE wie vom OP bereitgestellt:

 Verschachtelte Schleife (Kosten = 0,56..356,65 Zeilen = 102 Breite = 20) (tatsächliche Zeit = 0,034..0,979 Zeilen = 98 Schleifen = 1) 
 -> Seq Scan auf Stationen s (Kosten = 0,00..3,02 Zeilen = 102 Breite = 4) (tatsächliche Zeit = 0,009..0,016 Zeilen = 102 Schleifen = 1) 
 -> Limit (Kosten = 0,56..3,45 Zeilen = 1 Breite = 16) (tatsächliche Zeit = 0,009. .0.009 Zeilen = 1 Schleifen = 102) 
 -> Index-Scan mit station_id__submitted_at auf station_logs (Kosten = 0.56..664062.38 Zeilen = 230223 Breite = 16) (tatsächliche Zeit = 0.009 $ 
 Index Cond: (station_id = s.id) 
 Planungszeit: 0,542 ms 
 Ausführungszeit: 1,013 ms  - !!

Der einzige Index , den Sie benötigen, ist der von Ihnen erstellte: station_id__submitted_at. Die Einschränkung UNIQUEuniq_sid_sat macht im Grunde auch den Job. Beides beizubehalten scheint eine Verschwendung von Speicherplatz und Schreibleistung zu sein.

Ich fügte hinzu NULLS LAST bis ORDER BY in der Abfrage, weil submitted_at ist nicht definiert NOT NULL. Im Idealfall, falls zutreffend! Fügen Sie ein NOT NULL Einschränkung auf die Spalte submitted_at, löschen Sie den zusätzlichen Index und entfernen Sie NULLS LAST aus der Abfrage.

Wenn submitted_at kann NULL sein, erstellen Sie diesen UNIQUE Index, um sowohl Ihren aktuellen Index zu ersetzen und eindeutige Einschränkung:

CREATE UNIQUE INDEX station_logs_uni ON station_logs(station_id, submitted_at DESC NULLS LAST);

Erwägen:

Dies setzt eine separate Tabelle station mit einer Zeile pro relevantem station_id (normalerweise die PK) - die Sie so oder so haben sollten. Wenn Sie es nicht haben, erstellen Sie es. Wieder sehr schnell mit dieser rCTE-Technik:

CREATE TABLE station AS
WITH RECURSIVE cte AS (
   (
   SELECT station_id
   FROM   station_logs
   ORDER  BY station_id
   LIMIT  1
   )
   UNION ALL
   SELECT l.station_id
   FROM   cte c
   ,      LATERAL (   
      SELECT station_id
      FROM   station_logs
      WHERE  station_id > c.station_id
      ORDER  BY station_id
      LIMIT  1
      ) l
   )
TABLE cte;

Ich benutze das auch in der Geige. Sie können eine ähnliche Abfrage verwenden, um Ihre Aufgabe direkt zu lösen, ohne die Tabelle station - wenn Sie nicht überzeugt sind, sie zu erstellen.

Detaillierte Anweisungen, Erklärungen und Alternativen:

Index optimieren

Ihre Anfrage sollte jetzt sehr schnell sein. Nur wenn Sie die Leseleistung noch optimieren müssen ...

Es könnte sinnvoll sein, level_sensor als letzte Spalte des Index, um Nur-Index-Scans zu ermöglichen, wie Joanolo kommentiert .
Con : Dadurch wird der Index größer - was allen Abfragen, die ihn verwenden, ein wenig mehr Kosten verursacht.
Pro : Wenn Sie tatsächlich nur Index-Scans erhalten, muss die vorliegende Abfrage überhaupt keine Heap-Seiten besuchen, was sie ungefähr doppelt so schnell macht. Aber das kann jetzt ein unwesentlicher Gewinn für die sehr schnelle Abfrage sein.

Allerdings erwarte ich nicht, dass das für Ihren Fall funktioniert. Du erwähntest:

... ungefähr 20.000 Zeilen pro Tag pro station_id.

In der Regel deutet dies auf eine unaufhörliche Schreiblast hin (1 pro station_id alle 5 Sekunden). Und Sie interessieren sich für die Zeile spätestens. Nur-Index-Scans funktionieren nur für Heap-Seiten, die für alle Transaktionen sichtbar sind (das Bit in der Sichtbarkeitskarte ist gesetzt). Sie müssten extrem aggressive VACUUM -Einstellungen für die Tabelle ausführen, um mit der Schreiblast Schritt zu halten, und es würde die meiste Zeit immer noch nicht funktionieren. Wenn meine Annahmen korrekt sind, werden nur Index-Scans ausgegeben. nicht add level_sensor zum Index.

OTOH, wenn meine Annahmen zutreffen und Ihre Tabelle wächst sehr groß , ein BRIN-Index könnte helfen. Verbunden:

Oder noch spezialisierter und effizienter: Ein Teilindex nur für die neuesten Ergänzungen, um den Großteil der irrelevanten Zeilen abzuschneiden:

CREATE INDEX station_id__submitted_at_recent_idx ON station_logs(station_id, submitted_at DESC NULLS LAST)
WHERE submitted_at > '2017-06-24 00:00';

Wählen Sie einen Zeitstempel, für den Sie wissen, dass jüngere Zeilen vorhanden sein müssen. Sie müssen allen Abfragen eine übereinstimmende WHERE -Bedingung hinzufügen, z.

...
WHERE  station_id = s.station_id
AND    submitted_at > '2017-06-24 00:00'
...

Sie müssen Index und Abfrage von Zeit zu Zeit anpassen.
Verwandte Antworten mit weiteren Details:

18

Probieren Sie den klassischen Weg:

create index idx_station_logs__station_id on station_logs(station_id);
create index idx_station_logs__submitted_at on station_logs(submitted_at);

analyse station_logs;

with t as (
  select station_id, max(submitted_at) submitted_at 
  from station_logs 
  group by station_id)
select * 
from t join station_logs l on (
  l.station_id = t.station_id and l.submitted_at = t.submitted_at);

dbfiddle

EXPLAIN ANALYZE von ThreadStarter

 Nested Loop  (cost=701344.63..702110.58 rows=4 width=155) (actual time=6253.062..6253.544 rows=98 loops=1)
   CTE t
     ->  HashAggregate  (cost=701343.18..701344.07 rows=89 width=12) (actual time=6253.042..6253.069 rows=98 loops=1)
           Group Key: station_logs.station_id
           ->  Seq Scan on station_logs  (cost=0.00..598894.12 rows=20489812 width=12) (actual time=0.034..1841.848 rows=20489812 loop$
   ->  CTE Scan on t  (cost=0.00..1.78 rows=89 width=12) (actual time=6253.047..6253.085 rows=98 loops=1)
   ->  Index Scan using station_id__submitted_at on station_logs l  (cost=0.56..8.58 rows=1 width=143) (actual time=0.004..0.004 rows=$
         Index Cond: ((station_id = t.station_id) AND (submitted_at = t.submitted_at))
 Planning time: 0.542 ms
 Execution time: 6253.701 ms
6
Abelisto