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

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

Esistono vari modi più semplici e veloci.

2x DISTINCT ON

SELECT *
FROM  (
   SELECT DISTINCT ON (name)
          name, week AS first_week, value AS first_val
   FROM   tbl
   ORDER  BY name, week
   ) f
JOIN (
   SELECT DISTINCT ON (name)
          name, week AS last_week, value AS last_val
   FROM   tbl
   ORDER  BY name, week DESC
   ) l USING (name);

O più breve:

SELECT *
FROM  (SELECT DISTINCT ON (1) name, week AS first_week, value AS first_val FROM tbl ORDER BY 1,2) f
JOIN  (SELECT DISTINCT ON (1) name, week AS last_week , value AS last_val  FROM tbl ORDER BY 1,2 DESC) l USING (name);

Semplice e facile da capire. Anche più veloce nei miei vecchi test. Spiegazione dettagliata per DISTINCT ON :

  • Seleziona la prima riga in ogni gruppo GROUP BY?

2x funzione finestra, 1x DISTINCT ON

SELECT DISTINCT ON (name)
       name, week AS first_week, value AS first_val
     , first_value(week)  OVER w AS last_week
     , first_value(value) OVER w AS last_value
FROM   tbl t
WINDOW w AS (PARTITION BY name ORDER BY week DESC)
ORDER  BY name, week;

La WINDOW esplicita clausola accorcia solo il codice, nessun effetto sulle prestazioni.

first_value() di tipo composito

Le funzioni aggregate min() o max() non accettare tipi compositi come input. Dovresti creare funzioni aggregate personalizzate (che non è così difficile).
Ma le funzioni della finestra first_value() e last_value() fai . Partendo da ciò possiamo escogitare soluzioni semplici:

Richiesta semplice

SELECT DISTINCT ON (name)
       name, week AS first_week, value AS first_value
     ,(first_value((week, value)) OVER (PARTITION BY name ORDER BY week DESC))::text AS l
FROM   tbl t
ORDER  BY name, week;

L'output contiene tutti i dati, ma i valori dell'ultima settimana vengono inseriti in un record anonimo (facoltativamente trasmesso su text ). Potresti aver bisogno di valori scomposti.

Risultato scomposto con uso opportunistico del tipo di tabella

Per questo abbiamo bisogno di un noto tipo composito. Una definizione di tabella adattata consentirebbe l'uso opportunistico del tipo di tabella stesso direttamente:

CREATE TABLE tbl (week int, value int, name text);  -- optimized column order

week e value vieni prima, quindi ora possiamo ordinare in base al tipo di tabella stesso:

SELECT (l).name, first_week, first_val
     , (l).week AS last_week, (l).value AS last_val
FROM  (
   SELECT DISTINCT ON (name)
          week AS first_week, value AS first_val
        , first_value(t) OVER (PARTITION BY name ORDER BY week DESC) AS l
   FROM   tbl t
   ORDER  BY name, week
   ) sub;

Risultato scomposto dal tipo di riga definito dall'utente

Probabilmente non è possibile nella maggior parte dei casi. Registra un tipo composto con CREATE TYPE (permanente) o con CREATE TEMP TABLE (per la durata della sessione):

CREATE TEMP TABLE nv(last_week int, last_val int);  -- register composite type
SELECT name, first_week, first_val, (l).last_week, (l).last_val
FROM (
   SELECT DISTINCT ON (name)
          name, week AS first_week, value AS first_val
        , first_value((week, value)::nv) OVER (PARTITION BY name ORDER BY week DESC) AS l
   FROM   tbl t
   ORDER  BY name, week
   ) sub;

Funzioni aggregate personalizzate first() &last()

Crea funzioni e aggregati una volta per database:

CREATE OR REPLACE FUNCTION public.first_agg (anyelement, anyelement)
  RETURNS anyelement
  LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS
'SELECT $1;'

CREATE AGGREGATE public.first(anyelement) (
  SFUNC = public.first_agg
, STYPE = anyelement
, PARALLEL = safe
);


CREATE OR REPLACE FUNCTION public.last_agg (anyelement, anyelement)
  RETURNS anyelement
  LANGUAGE sql IMMUTABLE STRICT PARALLEL SAFE AS
'SELECT $2';

CREATE AGGREGATE public.last(anyelement) (
  SFUNC = public.last_agg
, STYPE = anyelement
, PARALLEL = safe
);

Quindi:

SELECT name
     , first(week) AS first_week, first(value) AS first_val
     , last(week)  AS last_week , last(value)  AS last_val
FROM  (SELECT * FROM tbl ORDER BY name, week) t
GROUP  BY name;

Probabilmente la soluzione più elegante. Più veloce con il modulo aggiuntivo first_last_agg fornendo un'implementazione C.
Confronta le istruzioni nel Wiki di Postgres.

Correlati:

  • Calcolo della crescita dei follower nel tempo per ogni influencer

db<>gioca qui (mostra tutto)
Sqlfiddle vecchio

Ognuna di queste domande è stata sostanzialmente più veloce della risposta attualmente accettata in un rapido test su una tabella con 50.000 righe con EXPLAIN ANALYZE .

Ci sono più modi. A seconda della distribuzione dei dati, stili di query diversi potrebbero essere (molto) più veloci. Vedi:

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