PostgreSQL: recupera la riga che ha il valore Max per una colonna


96

Ho a che fare con una tabella Postgres (chiamata "lives") che contiene record con colonne per time_stamp, usr_id, transaction_id e lives_remaining. Ho bisogno di una query che mi dia il totale lives_remaining più recente per ogni usr_id

  1. Sono presenti più utenti (diversi usr_id)
  2. time_stamp non è un identificatore univoco: a volte gli eventi utente (uno per riga nella tabella) si verificano con lo stesso time_stamp.
  3. trans_id è unico solo per intervalli di tempo molto piccoli: nel tempo si ripete
  4. Le vite rimanenti (per un dato utente) possono sia aumentare che diminuire nel tempo

esempio:

time_stamp | lives_remaining | usr_id | trans_id
-----------------------------------------
  07:00 | 1 | 1 | 1    
  09:00 | 4 | 2 | 2    
  10:00 | 2 | 3 | 3    
  10:00 | 1 | 2 | 4    
  11:00 | 4 | 1 | 5    
  11:00 | 3 | 1 | 6    
  13:00 | 3 | 3 | 1    

Poiché avrò bisogno di accedere ad altre colonne della riga con i dati più recenti per ogni dato usr_id, ho bisogno di una query che dia un risultato come questo:

time_stamp | lives_remaining | usr_id | trans_id
-----------------------------------------
  11:00 | 3 | 1 | 6    
  10:00 | 1 | 2 | 4    
  13:00 | 3 | 3 | 1    

Come accennato, ogni usr_id può guadagnare o perdere vite e, a volte, questi eventi con data e ora si verificano così ravvicinati che hanno lo stesso timestamp! Pertanto questa query non funzionerà:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp) AS max_timestamp 
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp = b.time_stamp

Invece, ho bisogno di utilizzare sia time_stamp (primo) che trans_id (secondo) per identificare la riga corretta. Devo anche quindi passare quelle informazioni dalla sottoquery alla query principale che fornirà i dati per le altre colonne delle righe appropriate. Questa è la query compromessa su cui sono riuscito a lavorare:

SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM 
      (SELECT usr_id, max(time_stamp || '*' || trans_id) 
       AS max_timestamp_transid
       FROM lives GROUP BY usr_id ORDER BY usr_id) a 
JOIN lives b ON a.max_timestamp_transid = b.time_stamp || '*' || b.trans_id 
ORDER BY b.usr_id

Ok, quindi funziona, ma non mi piace. Richiede una query all'interno di una query, un self join e mi sembra che potrebbe essere molto più semplice afferrando la riga che MAX ha trovato avere il timestamp e trans_id più grandi. La tabella "lives" ha decine di milioni di righe da analizzare, quindi vorrei che questa query fosse il più veloce ed efficiente possibile. Sono nuovo in RDBM e Postgres in particolare, quindi so che devo fare un uso efficace degli indici appropriati. Sono un po 'perso su come ottimizzare.

Ho trovato una discussione simile qui . Posso eseguire un tipo di Postgres equivalente a una funzione analitica Oracle?

Qualche consiglio sull'accesso alle informazioni sulle colonne correlate utilizzate da una funzione aggregata (come MAX), sulla creazione di indici e sulla creazione di query migliori sarebbe molto apprezzato!

PS È possibile utilizzare quanto segue per creare il mio caso di esempio:

create TABLE lives (time_stamp timestamp, lives_remaining integer, 
                    usr_id integer, trans_id integer);
insert into lives values ('2000-01-01 07:00', 1, 1, 1);
insert into lives values ('2000-01-01 09:00', 4, 2, 2);
insert into lives values ('2000-01-01 10:00', 2, 3, 3);
insert into lives values ('2000-01-01 10:00', 1, 2, 4);
insert into lives values ('2000-01-01 11:00', 4, 1, 5);
insert into lives values ('2000-01-01 11:00', 3, 1, 6);
insert into lives values ('2000-01-01 13:00', 3, 3, 1);

Josh, potrebbe non piacerti il ​​fatto che la query si unisca automaticamente ecc., Ma va bene per quanto riguarda l'RDBMS.
vladr

1
Ciò in cui l'auto-join finirà per tradursi è una semplice mappatura dell'indice, dove il SELECT interno (quello con MAX) scansiona l'indice eliminando le voci irrilevanti e dove il SELECT esterno prende solo il resto delle colonne dalla tabella corrispondente all'indice ristretto.
vladr

Vlad, grazie per i suggerimenti e la spiegazione. Mi ha aperto gli occhi su come iniziare a comprendere il funzionamento interno del database e su come ottimizzare le query. Quassnoi, grazie per l'ottima query e il suggerimento sulla chiave primaria; Anche Bill. Molto utile.
Joshua Berry,

grazie per avermi mostrato come ottenere MAX BY2 colonne!

Risposte:


90

Su una tabella con 158k righe pseudo-casuali (usr_id distribuito uniformemente tra 0 e 10k, trans_iddistribuito uniformemente tra 0 e 30),

Per costo query, di seguito, mi riferisco alla stima dei costi dell'ottimizzatore basato sui costi di Postgres (con i xxx_costvalori predefiniti di Postgres ), che è una stima della funzione ponderata delle risorse di I / O e CPU richieste; puoi ottenerlo avviando PgAdminIII ed eseguendo "Query / Explain (F7)" sulla query con "Opzioni Query / Explain" impostato su "Analizza"

  • Domanda di Quassnoy ha una stima di costo 745k (!), E viene completata in 1,3 secondi (dato un indice composito su ( usr_id, trans_id, time_stamp))
  • La query di Bill ha una stima del costo di 93k e viene completata in 2,9 secondi (dato un indice composto su ( usr_id, trans_id))
  • Query 1 # di sotto ha una stima dei costi di 16k, e completa in 800ms (dato un indice composto sul ( usr_id, trans_id, time_stamp))
  • Query 2 # di sotto ha una stima dei costi di 14k, e completa in 800ms (dato un indice di funzione composta su ( usr_id, EXTRACT(EPOCH FROM time_stamp), trans_id))
    • questo è specifico di Postgres
  • Query # 3 sottostante (Postgres 8.4+) viene preventivo e completamento tempo paragonabile (o meglio) interrogazione # 2 (dato un indice composito su ( usr_id, time_stamp, trans_id)); ha il vantaggio di scansionare la livestabella una sola volta e, se dovessi aumentare temporaneamente (se necessario) work_mem per sistemare l'ordinamento in memoria, sarà di gran lunga la più veloce di tutte le query.

Tutti i tempi sopra includono il recupero dell'intero set di risultati di 10k righe.

Il tuo obiettivo è una stima dei costi minimi e un tempo di esecuzione delle query minimo, con un'enfasi sul costo stimato. L'esecuzione della query può dipendere in modo significativo dalle condizioni di runtime (ad esempio, se le righe rilevanti sono già completamente memorizzate nella cache o meno), mentre la stima dei costi non lo è. D'altra parte, tieni presente che la stima dei costi è esattamente quella, una stima.

Il miglior tempo di esecuzione della query si ottiene quando si esegue su un database dedicato senza carico (es. Giocando con pgAdminIII su un PC di sviluppo). Il tempo di query varierà in produzione in base al carico effettivo della macchina / diffusione dell'accesso ai dati. Quando una query appare leggermente più veloce (<20%) dell'altra ma ha un costo molto più elevato, in genere sarà più saggio scegliere quella con un tempo di esecuzione maggiore ma un costo inferiore.

Quando ti aspetti che non ci sarà concorrenza per la memoria sulla tua macchina di produzione nel momento in cui viene eseguita la query (ad esempio, la cache RDBMS e la cache del file system non saranno distrutte da query simultanee e / o attività del file system), l'ora della query ottenuta in modalità standalone (ad esempio pgAdminIII su un PC di sviluppo) sarà rappresentativa. In caso di conflitto sul sistema di produzione, il tempo di query si ridurrà proporzionalmente al rapporto di costo stimato, poiché la query con il costo inferiore non si basa tanto sulla cache mentre la query con un costo maggiore rivisiterà gli stessi dati più e più volte (attivando I / O aggiuntivo in assenza di una cache stabile), ad esempio:

              cost | time (dedicated machine) |     time (under load) |
-------------------+--------------------------+-----------------------+
some query A:   5k | (all data cached)  900ms | (less i/o)     1000ms |
some query B:  50k | (all data cached)  900ms | (lots of i/o) 10000ms |

Non dimenticare di eseguire ANALYZE livesuna volta dopo aver creato gli indici necessari.


Domanda n. 1

-- incrementally narrow down the result set via inner joins
--  the CBO may elect to perform one full index scan combined
--  with cascading index lookups, or as hash aggregates terminated
--  by one nested index lookup into lives - on my machine
--  the latter query plan was selected given my memory settings and
--  histogram
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
    SELECT
      usr_id,
      MAX(time_stamp) AS time_stamp_max
     FROM
      lives
     GROUP BY
      usr_id
  ) AS l2
 ON
  l1.usr_id     = l2.usr_id AND
  l1.time_stamp = l2.time_stamp_max
 INNER JOIN (
    SELECT
      usr_id,
      time_stamp,
      MAX(trans_id) AS trans_max
     FROM
      lives
     GROUP BY
      usr_id, time_stamp
  ) AS l3
 ON
  l1.usr_id     = l3.usr_id AND
  l1.time_stamp = l3.time_stamp AND
  l1.trans_id   = l3.trans_max

Domanda n. 2

-- cheat to obtain a max of the (time_stamp, trans_id) tuple in one pass
-- this results in a single table scan and one nested index lookup into lives,
--  by far the least I/O intensive operation even in case of great scarcity
--  of memory (least reliant on cache for the best performance)
SELECT
  l1.*
 FROM
  lives AS l1
 INNER JOIN (
   SELECT
     usr_id,
     MAX(ARRAY[EXTRACT(EPOCH FROM time_stamp),trans_id])
       AS compound_time_stamp
    FROM
     lives
    GROUP BY
     usr_id
  ) AS l2
ON
  l1.usr_id = l2.usr_id AND
  EXTRACT(EPOCH FROM l1.time_stamp) = l2.compound_time_stamp[1] AND
  l1.trans_id = l2.compound_time_stamp[2]

Aggiornamento 2013/01/29

Infine, a partire dalla versione 8.4, Postgres supporta la funzione Window, il che significa che puoi scrivere qualcosa di semplice ed efficiente come:

Domanda n. 3

-- use Window Functions
-- performs a SINGLE scan of the table
SELECT DISTINCT ON (usr_id)
  last_value(time_stamp) OVER wnd,
  last_value(lives_remaining) OVER wnd,
  usr_id,
  last_value(trans_id) OVER wnd
 FROM lives
 WINDOW wnd AS (
   PARTITION BY usr_id ORDER BY time_stamp, trans_id
   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
 );

Con un indice composto su (usr_id, trans_id, times_tamp), intendi qualcosa come "CREATE INDEX lives_blah_idx ON lives (usr_id, trans_id, time_stamp)"? O devo creare tre indici separati per ogni colonna? Dovrei attenermi all'impostazione predefinita "USARE btree", giusto?
Joshua Berry,

1
Sì alla prima scelta: intendo CREATE INDEX lives_blah_idx ON lives (usr_id, trans_id, time_stamp). :) Saluti.
vladr

Grazie anche per aver fatto il confronto dei costi vladr! Risposta molto completa!
Adam,

@vladr Ho appena trovato la tua risposta. Sono un po 'confuso, come dici tu la query 1 ha un costo di 16k e la query 2 un costo di 14k. Ma più in basso nella tabella dici che la query 1 ha un costo di 5k e la query 2 ha un costo di 50k. Quindi quale query è preferibile utilizzare? :) grazie
Houman

1
@Kave, la tabella è per una coppia ipotetica di query per illustrare un esempio, non le due query dell'OP. Rinominare per ridurre la confusione.
vladr

77

Proporrei una versione pulita basata su DISTINCT ON(vedi documenti ):

SELECT DISTINCT ON (usr_id)
    time_stamp,
    lives_remaining,
    usr_id,
    trans_id
FROM lives
ORDER BY usr_id, time_stamp DESC, trans_id DESC;

6
Questa è una risposta molto breve e valida. Ha anche un buon riferimento! Questa dovrebbe essere la risposta accettata.
Prakhar Agrawal,

Questo sembrava funzionare per me sulla mia applicazione leggermente diversa dove nient'altro avrebbe funzionato. Sicuramente dovrebbe essere sollevato per una maggiore visibilità.
Jim Factor

8

Ecco un altro metodo, che non utilizza sottoquery correlate o GROUP BY. Non sono esperto nell'ottimizzazione delle prestazioni di PostgreSQL, quindi ti suggerisco di provare sia questo che le soluzioni fornite da altre persone per vedere quale funziona meglio per te.

SELECT l1.*
FROM lives l1 LEFT OUTER JOIN lives l2
  ON (l1.usr_id = l2.usr_id AND (l1.time_stamp < l2.time_stamp 
   OR (l1.time_stamp = l2.time_stamp AND l1.trans_id < l2.trans_id)))
WHERE l2.usr_id IS NULL
ORDER BY l1.usr_id;

Presumo che trans_idsia unico almeno su un dato valore di time_stamp.


4

Mi piace lo stile della risposta di Mike Woodhouse nell'altra pagina che hai citato. È particolarmente conciso quando l'oggetto che viene ingrandito è solo una singola colonna, nel qual caso la sottoquery può usare solo MAX(some_col)e GROUP BYle altre colonne, ma nel tuo caso hai una quantità in 2 parti da massimizzare, puoi comunque farlo usando ORDER BYpiù LIMIT 1invece (come fatto da Quassnoi):

SELECT * 
FROM lives outer
WHERE (usr_id, time_stamp, trans_id) IN (
    SELECT usr_id, time_stamp, trans_id
    FROM lives sq
    WHERE sq.usr_id = outer.usr_id
    ORDER BY trans_id, time_stamp
    LIMIT 1
)

Trovo WHERE (a, b, c) IN (subquery)bello usare la sintassi del costruttore di righe perché riduce la quantità di verbosità necessaria.


3

In effetti c'è una soluzione hacky per questo problema. Supponiamo che tu voglia selezionare l'albero più grande di ogni foresta in una regione.

SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1]
FROM tree JOIN forest ON (tree.forest = forest.id)
GROUP BY forest.id

Quando si raggruppano gli alberi in base alle foreste, verrà visualizzato un elenco di alberi non ordinato e sarà necessario trovare quello più grande. La prima cosa che dovresti fare è ordinare le righe in base alle loro dimensioni e selezionare la prima dell'elenco. Può sembrare inefficiente, ma se hai milioni di righe sarà molto più veloce delle soluzioni che includono JOINle WHEREcondizioni e .

BTW, nota che ORDER_BYfor array_aggè stato introdotto in Postgresql 9.0


Hai un errore. Devi scrivere ORDER BY tree_size.size DESC. Inoltre, per il compito dell'autore il codice sarà simile a questo: SELECT usr_id, (array_agg(time_stamp ORDER BY time_stamp DESC))[1] AS timestamp, (array_agg(lives_remaining ORDER BY time_stamp DESC))[1] AS lives_remaining, (array_agg(trans_id ORDER BY time_stamp DESC))[1] AS trans_id FROM lives GROUP BY usr_id
alexkovelsky

2

C'è una nuova opzione in Postgressql 9.5 chiamata DISTINCT ON

SELECT DISTINCT ON (location) location, time, report
    FROM weather_reports
    ORDER BY location, time DESC;

Elimina le righe duplicate e lascia solo la prima riga come definita dalla clausola ORDER BY.

vedere la documentazione ufficiale


1
SELECT  l.*
FROM    (
        SELECT DISTINCT usr_id
        FROM   lives
        ) lo, lives l
WHERE   l.ctid = (
        SELECT ctid
        FROM   lives li
        WHERE  li.usr_id = lo.usr_id
        ORDER BY
          time_stamp DESC, trans_id DESC
        LIMIT 1
        )

La creazione di un indice su (usr_id, time_stamp, trans_id)migliorerà notevolmente questa query.

Dovresti sempre, sempre averne una specie PRIMARY KEYnelle tue tabelle.


0

Penso che tu abbia un grosso problema qui: non esiste un "contatore" che aumenta in modo monotono per garantire che una data riga sia avvenuta più tardi rispetto a un'altra. Prendi questo esempio:

timestamp   lives_remaining   user_id   trans_id
10:00       4                 3         5
10:00       5                 3         6
10:00       3                 3         1
10:00       2                 3         2

Non è possibile determinare da questi dati quale sia la voce più recente. È il secondo o l'ultimo? Non esiste una funzione di ordinamento o max () che puoi applicare a nessuno di questi dati per darti la risposta corretta.

Aumentare la risoluzione del timestamp sarebbe di enorme aiuto. Poiché il motore di database serializza le richieste, con una risoluzione sufficiente è possibile garantire che non ci siano due timestamp uguali.

In alternativa, usa un trans_id che non si trasferirà per molto, molto tempo. Avere un trans_id che rolla sopra significa che non puoi dire (per lo stesso timestamp) se trans_id 6 è più recente di trans_id 1 a meno che tu non faccia dei calcoli complicati.


Sì, idealmente una colonna di sequenza (autoincremento) sarebbe in ordine.
vladr

L'ipotesi di cui sopra era che per piccoli incrementi di tempo, trans_id non sarebbe stato rollover. Sono d'accordo che la tabella necessita di un indice primario univoco, come un trans_id non ripetibile. (PS Sono felice di avere abbastanza punti karma / reputazione per commentare!)
Joshua Berry,

Vlad afferma che trans_id ha un ciclo piuttosto breve che gira frequentemente. Anche se consideri solo le due righe centrali della mia tabella (trans_id = 6 e 1), non puoi ancora dire quale sia la più recente. Pertanto, l'utilizzo di max (trans_id) per un dato timestamp non funzionerà.
Barry Brown,

Sì, mi affido alla garanzia dell'autore dell'applicazione che la tupla (time_stamp, trans_id) è unica per un determinato utente. Se non è il caso, "SELECT l1.usr_id, l1.lives_left, ... FROM ... WHERE ..." deve diventare "SELECT l1.usr_id, MAX / MIN (l1.lives_left), ... FROM. .. WHERE ... GROUP BY l1.usr_id, ...
vladr

0

Un'altra soluzione che potresti trovare utile.

SELECT t.*
FROM
    (SELECT
        *,
        ROW_NUMBER() OVER(PARTITION BY usr_id ORDER BY time_stamp DESC) as r
    FROM lives) as t
WHERE t.r = 1
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.