Come rendere DISTINCT ON più veloce in PostgreSQL?


13

Ho una tabella station_logsin un database PostgreSQL 9.6:

    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)

Sto cercando di ottenere l'ultimo level_sensorvalore basato su submitted_at, per ciascuno station_id. Esistono circa 400 station_idvalori univoci e circa 20k righe al giorno per station_id.

Prima di creare l'indice:

EXPLAIN ANALYZE
SELECT DISTINCT ON(station_id) station_id, submitted_at, level_sensor
FROM station_logs ORDER BY station_id, submitted_at DESC;
 Unico (costo = 4347852.14..4450301.72 righe = 89 larghezza = 20) (tempo effettivo = 22202.080..27619.167 righe = 98 loop = 1)
   -> Ordina (costo = 4347852.14..4399076.93 righe = 20489916 larghezza = 20) (tempo effettivo = 22202.077..26540.827 righe = 20489812 loop = 1)
         Chiave di ordinamento: station_id, submit_at DESC
         Metodo di ordinamento: unione esterna Disco: 681040kB
         -> Scansione Seq su log_stazione (costo = 0,00..598895,16 righe = 20489916 larghezza = 20) (tempo effettivo = 0,023..3443,587 righe = 20489812 loop = $
 Tempo di pianificazione: 0,072 ms
 Tempo di esecuzione: 27690.644 ms

Creazione indice:

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

Dopo aver creato l'indice, per la stessa query:

 Unico (costo = 0,56..2156367,51 righe = 89 larghezza = 20) (tempo effettivo = 0,184..16263,413 righe = 98 loop = 1)
   -> Scansione indice utilizzando station_id__submitted_at su station_logs (costo = 0,56..2105142,98 righe = 20489812 larghezza = 20) (tempo effettivo = 0,181..1 $
 Tempo di pianificazione: 0,206 ms
 Tempo di esecuzione: 16263.490 ms

C'è un modo per rendere più veloce questa query? Ad esempio 1 secondo, 16 secondi è ancora troppo.


2
Quanti ID di stazione distinti ci sono, ovvero quante righe restituisce la query? E quale versione di Postgres?
ypercubeᵀᴹ

Postgre 9.6, circa 400 unici station_id e circa 20k record al giorno per station_id
Kokizzu

Questa query restituisce un "ultimo valore di level_sensor basato su submit_at, per ogni station_id". DISTINCT ON comporta una scelta casuale tranne nei casi in cui non ne hai bisogno.
philipxy,

Risposte:


18

Solo per 400 stazioni, questa query sarà enormemente più veloce:

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 qui
(confrontando i piani per questa query, l'alternativa di Abelisto e l'originale)

Risulta EXPLAIN ANALYZEcome previsto dal PO:

 Loop nidificato (costo = 0,56..356,65 righe = 102 larghezza = 20) (tempo effettivo = 0,034..0,979 righe = 98 loop = 1)
   -> Scansione Seq su stazioni s (costo = 0,00..3,02 righe = 102 larghezza = 4) (tempo effettivo = 0,009..0,016 righe = 102 loop = 1)
   -> Limite (costo = 0,56..3,45 righe = 1 larghezza = 16) (tempo effettivo = 0,009..0,009 righe = 1 loop = 102)
         -> Scansione indice utilizzando station_id__submitted_at su station_logs (costo = 0,56..664062,38 righe = 230223 larghezza = 16) (tempo effettivo = 0,009 $
               Indice cond: (station_id = s.id)
 Tempo di pianificazione: 0,542 ms
 Tempo di esecuzione: 1.013 ms   - !!

L'unico indice di cui hai bisogno è quello che si è creato: station_id__submitted_at. Il UNIQUEvincolo uniq_sid_satfa anche il lavoro, in pratica. Mantenere entrambi sembra uno spreco di spazio su disco e prestazioni di scrittura.

Ho aggiunto NULLS LASTa ORDER BYnella query perché submitted_atnon è definito NOT NULL. Idealmente, se applicabile !, aggiungere un NOT NULLvincolo alla colonna submitted_at, eliminare l'indice aggiuntivo e rimuoverlo NULLS LASTdalla query.

Se submitted_atpuò essere NULL, creare questa UNIQUEindice per sostituire sia l'indice corrente e vincolo univoco:

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

Ritenere:

Ciò presuppone una tabella separatastation con una riga per pertinente station_id(in genere il PK) - che dovresti avere in entrambi i modi. Se non lo hai, crealo. Ancora una volta, molto velocemente con questa tecnica rCTE:

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;

Lo uso anche nel violino. È possibile utilizzare una query simile per risolvere l'attività direttamente, senza stationtabella, se non si è convinti di crearla.

Istruzioni dettagliate, spiegazioni e alternative:

Ottimizza l'indice

La tua query dovrebbe essere molto veloce ora. Solo se hai ancora bisogno di ottimizzare le prestazioni di lettura ...

Potrebbe avere senso aggiungere level_sensorl'ultima colonna all'indice per consentire scansioni solo dell'indice , come commentato joanolo .
Contro: rende l'indice più grande, il che aggiunge un piccolo costo a tutte le query che lo utilizzano.
Pro: se in realtà ottieni solo scansioni dell'indice, la query a portata di mano non deve assolutamente visitare le pagine heap, il che lo rende circa il doppio più veloce. Ma questo potrebbe essere un guadagno non sostanziale per la query molto veloce ora.

Tuttavia , non mi aspetto che funzioni per il tuo caso. Hai nominato:

... circa 20.000 file al giorno per station_id.

In genere, ciò indica un carico di scrittura incessante (1 station_idogni 5 secondi). E sei interessato all'ultima riga. Le scansioni solo indice funzionano solo per le pagine heap che sono visibili a tutte le transazioni (il bit nella mappa di visibilità è impostato). Dovresti eseguire VACUUMimpostazioni estremamente aggressive per la tabella per tenere il passo con il carico di scrittura e non funzionerebbe ancora per la maggior parte del tempo. Se i miei presupposti sono corretti, le scansioni solo indice sono fuori, non aggiungere level_sensorall'indice.

OTOH, se le mie ipotesi valgono e il tuo tavolo sta diventando molto grande , un indice BRIN potrebbe aiutare. Relazionato:

O ancora più specializzato e più efficiente: un indice parziale solo per le ultime aggiunte per tagliare la maggior parte delle righe irrilevanti:

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

Scegli un timestamp per il quale sai che devono esistere righe più giovani. Devi aggiungere una WHEREcondizione corrispondente a tutte le query, come:

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

È necessario adattare periodicamente l'indice e la query.
Risposte correlate con maggiori dettagli:


Ogni volta che so che voglio un loop nidificato (spesso), l'uso di LATERAL è un aumento delle prestazioni per una serie di situazioni.
Paul Draper,

6

Prova in modo classico:

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

SPIEGARE ANALISI di 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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.