Wie kann DISTINCT ON in PostgreSQL schneller gemacht werden?

12

Ich habe eine Tabelle station_logsin 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 , für jeden den letzten level_sensorWert zu ermitteln . Es gibt ungefähr 400 eindeutige Werte und ungefähr 20.000 Zeilen pro Tag und Tag .submitted_atstation_idstation_idstation_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;
 Einzigartig (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 Scan on 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 ms

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:

 Einzigartig (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 in 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.

Kokizzu
quelle
2
Wie viele unterschiedliche Stations-IDs gibt es, dh wie viele Zeilen gibt die Abfrage zurück? Und welche Version von Postgres?
Ypercubeᵀᴹ
Postgre 9.6, ungefähr 400 eindeutige station_id und ungefähr 20.000 Datensätze pro Tag pro station_id
Kokizzu
Diese Abfrage gibt für jede station_id einen "letzten level_sensor-Wert basierend auf submit_at" zurück. DISTINCT ON beinhaltet eine zufällige Auswahl, außer in Fällen, in denen Sie sie nicht benötigen.
Philipxy

Antworten:

17

Bei nur 400 Stationen ist diese Abfrage erheblich 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
(Vergleich der Pläne für diese Abfrage, Abelistos Alternative und Ihr Original)

Ergebnis EXPLAIN ANALYZEwie 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 in 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 UNIQUEEinschränkung uniq_sid_saterledigt im Grunde auch die Arbeit. Beides beizubehalten scheint eine Verschwendung von Speicherplatz und Schreibleistung zu sein.

Ich NULLS LASThabe ORDER BYin der Abfrage hinzugefügt , weil submitted_atnicht definiert ist NOT NULL. Fügen Sie NOT NULLder Spalte gegebenenfalls eine Einschränkung hinzu submitted_at, löschen Sie den zusätzlichen Index und entfernen Sie ihn NULLS LASTaus der Abfrage.

Wenn submitted_atmöglich NULL, erstellen Sie diesen UNIQUEIndex, um sowohl Ihren aktuellen Index als auch die eindeutige Einschränkung zu ersetzen :

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

Erwägen:

Dies setzt eine separate Tabellestation mit einer Zeile pro relevanter station_id(normalerweise der PK) voraus - 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 ohne stationTabelle zu lösen - 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 kann sinnvoll sein, level_sensorals letzte Spalte zum Index hinzuzufügen , um nur Index-Scans zu ermöglichen , wie von joanolo kommentiert .
Con: Dadurch wird der Index größer - was für alle Abfragen, die ihn verwenden, ein wenig Kosten verursacht.
Pro: Wenn Sie tatsächlich nur Index-Scans erhalten, muss die vorliegende Abfrage überhaupt keine Heap-Seiten besuchen, was sie etwa doppelt so schnell macht. Aber das kann jetzt ein unwesentlicher Gewinn für die sehr schnelle Abfrage sein.

Allerdings erwarte ich nicht , dass für Ihren Fall an der Arbeit. Du erwähntest:

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

In der Regel bedeutet dies eine unaufhörliche Schreiblast (1 pro station_id5 Sekunden). Und Sie interessieren sich für die neueste Reihe. 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 VACUUMEinstellungen für die Tabelle vornehmen, um mit der Schreiblast Schritt zu halten, und es würde die meiste Zeit immer noch nicht funktionieren. Wenn meine Annahmen richtig sind, sind nur Index-Scans nicht verfügbar level_sensor. Fügen Sie sie nicht zum Index hinzu.

OTOH, wenn meine Annahmen zutreffen und Ihre Tabelle sehr groß wird , könnte ein BRIN-Index 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 WHEREallen Abfragen eine übereinstimmende 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 mehr Details:

Erwin Brandstetter
quelle
Immer wenn ich weiß, dass ich (oft) eine verschachtelte Schleife möchte, ist die Verwendung von LATERAL eine Leistungssteigerung für eine Reihe von Situationen.
Paul Draper
6

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
Abelisto
quelle