PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Ottimizza la query GROUP BY per recuperare l'ultima riga per utente

Per prestazioni di lettura ottimali è necessario un indice multicolonna:

CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);

Per eseguire solo indicizza scansioni possibile, aggiungi la colonna payload altrimenti non necessaria in un indice di copertura con il INCLUDE clausola (Postgres 11 o successivo):

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);

Vedi:

  • La copertura degli indici in PostgreSQL aiuta a UNIRE le colonne?

Fallback per le versioni precedenti:

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);

Perché DESC NULLS LAST ?

  • Indice non utilizzato nell'intervallo di query di date

Per pochi righe per user_id o tavolini DISTINCT ON è in genere più veloce e semplice:

  • Seleziona la prima riga in ogni gruppo GROUP BY?

Per molti righe per user_id una scansione con salto dell'indice (o scansione dell'indice allentata ) è (molto) più efficiente. Questo non è implementato fino a Postgres 12 - il lavoro è in corso per Postgres 14. Ma ci sono modi per emularlo in modo efficiente.

Le espressioni tabelle comuni richiedono Postgres 8.4+ .
LATERAL richiede Postgres 9.3+ .
Le seguenti soluzioni vanno oltre ciò che è trattato nel Wiki Postgres .

1. Nessuna tabella separata con utenti unici

Con un users separato tabella, soluzioni in 2. di seguito sono in genere più semplici e veloci. Salta avanti.

1a. CTE ricorsivo con LATERAL unisciti

WITH RECURSIVE cte AS (
   (                                -- parentheses required
   SELECT user_id, log_date, payload
   FROM   log
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT l.*
   FROM   cte c
   CROSS  JOIN LATERAL (
      SELECT l.user_id, l.log_date, l.payload
      FROM   log l
      WHERE  l.user_id > c.user_id  -- lateral reference
      AND    log_date <= :mydate    -- repeat condition
      ORDER  BY l.user_id, l.log_date DESC NULLS LAST
      LIMIT  1
      ) l
   )
TABLE  cte
ORDER  BY user_id;

Questo è semplice da recuperare colonne arbitrarie e probabilmente il migliore nell'attuale Postgres. Maggiori spiegazioni nel capitolo 2a. sotto.

1b. CTE ricorsiva con subquery correlata

WITH RECURSIVE cte AS (
   (                                           -- parentheses required
   SELECT l AS my_row                          -- whole row
   FROM   log l
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT (SELECT l                            -- whole row
           FROM   log l
           WHERE  l.user_id > (c.my_row).user_id
           AND    l.log_date <= :mydate        -- repeat condition
           ORDER  BY l.user_id, l.log_date DESC NULLS LAST
           LIMIT  1)
   FROM   cte c
   WHERE  (c.my_row).user_id IS NOT NULL       -- note parentheses
   )
SELECT (my_row).*                              -- decompose row
FROM   cte
WHERE  (my_row).user_id IS NOT NULL
ORDER  BY (my_row).user_id;

Comodo per recuperare una singola colonna o l'intera riga . L'esempio utilizza l'intero tipo di riga della tabella. Sono possibili altre varianti.

Per affermare che una riga è stata trovata nell'iterazione precedente, testa una singola colonna NOT NULL (come la chiave primaria).

Ulteriori spiegazioni per questa domanda nel capitolo 2b. sotto.

Correlati:

  • Interroga le ultime N righe correlate per riga
  • GRUPPO PER una colonna, ordinando per un'altra in PostgreSQL

2. Con users separati tabella

Il layout della tabella non è importante fintanto che esattamente una riga per user_id pertinente è garantito. Esempio:

CREATE TABLE users (
   user_id  serial PRIMARY KEY
 , username text NOT NULL
);

Idealmente, la tabella è ordinata fisicamente in sincronia con il log tavolo. Vedi:

  • Ottimizza l'intervallo di query del timestamp di Postgres

Oppure è abbastanza piccolo (bassa cardinalità) da non avere importanza. Altrimenti, l'ordinamento delle righe nella query può aiutare a ottimizzare ulteriormente le prestazioni. Vedi l'aggiunta di Gang Liang. Se l'ordinamento fisico degli users la tabella corrisponde all'indice su log , questo potrebbe essere irrilevante.

2a. LATERAL unisciti

SELECT u.user_id, l.log_date, l.payload
FROM   users u
CROSS  JOIN LATERAL (
   SELECT l.log_date, l.payload
   FROM   log l
   WHERE  l.user_id = u.user_id         -- lateral reference
   AND    l.log_date <= :mydate
   ORDER  BY l.log_date DESC NULLS LAST
   LIMIT  1
   ) l;

JOIN LATERAL permette di fare riferimento a FROM precedente elementi sullo stesso livello di query. Vedi:

  • Qual ​​è la differenza tra LATERAL JOIN e una sottoquery in PostgreSQL?

Risulta in una ricerca nell'indice (solo) per utente.

Non restituisce alcuna riga per gli utenti mancanti in users tavolo. In genere, una chiave straniera il vincolo che impone l'integrità referenziale lo escluderebbe.

Inoltre, nessuna riga per gli utenti senza una voce corrispondente nel log - conforme alla domanda originaria. Per mantenere quegli utenti nel risultato, usa LEFT JOIN LATERAL ... ON true invece di CROSS JOIN LATERAL :

  • Richiama più volte una funzione di restituzione di set con un argomento array

Usa LIMIT n invece di LIMIT 1 per recuperare più di una riga (ma non tutti) per utente.

In effetti, tutti questi fanno lo stesso:

JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...

L'ultimo ha una priorità più bassa, però. JOIN esplicito si lega prima della virgola. Quella sottile differenza può essere importante con più tabelle di join. Vedi:

  • "riferimento non valido alla voce della clausola FROM per la tabella" nella query di Postgres

2b. Sottoquery correlata

Buona scelta per recuperare una colonna singola da una unica riga . Esempio di codice:

  • Ottimizza la query massima per gruppo

Lo stesso è possibile per più colonne , ma hai bisogno di più intelligenza:

CREATE TEMP TABLE combo (log_date date, payload int);

SELECT user_id, (combo1).*              -- note parentheses
FROM (
   SELECT u.user_id
        , (SELECT (l.log_date, l.payload)::combo
           FROM   log l
           WHERE  l.user_id = u.user_id
           AND    l.log_date <= :mydate
           ORDER  BY l.log_date DESC NULLS LAST
           LIMIT  1) AS combo1
   FROM   users u
   ) sub;

Come LEFT JOIN LATERAL sopra, questa variante include tutti utenti, anche senza voci nel log . Ottieni NULL per combo1 , che puoi facilmente filtrare con un WHERE clausola nella query esterna, se necessario.
Nitpick:nella query esterna non è possibile distinguere se la sottoquery non ha trovato una riga o se tutti i valori delle colonne sono NULL - stesso risultato. Hai bisogno di un NOT NULL colonna nella sottoquery per evitare questa ambiguità.

Una sottoquery correlata può restituire solo un valore singolo . Puoi avvolgere più colonne in un tipo composto. Ma per scomporlo in seguito, Postgres richiede un noto tipo composito. I record anonimi possono essere scomposti solo fornendo un elenco di definizioni di colonna.
Utilizza un tipo registrato come il tipo riga di una tabella esistente. Oppure registra un tipo composto in modo esplicito (e permanente) con CREATE TYPE . Oppure crea una tabella temporanea (rilasciata automaticamente alla fine della sessione) per registrare temporaneamente il tipo di riga. Sintassi del cast:(log_date, payload)::combo

Infine, non vogliamo scomporre combo1 allo stesso livello di interrogazione. A causa di una debolezza nel pianificatore di query, questo valuterebbe la sottoquery una volta per ogni colonna (ancora vero in Postgres 12). Invece, rendila una sottoquery e scomponi nella query esterna.

Correlati:

  • Ottieni i valori dalla prima e dall'ultima riga per gruppo

Dimostrazione di tutte e 4 le query con 100.000 voci di registro e 1.000 utenti:
db<>gioca qui - pg 11
Sqlfiddle vecchio