PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Obter valores da primeira e da última linha por grupo


Existem várias maneiras mais simples e rápidas.

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

Ou mais curto:
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);

Simples e fácil de se entender. Também mais rápido em meus testes antigos. Explicação detalhada para DISTINCT ON :
  • Selecionar a primeira linha em cada grupo GROUP BY?

2x função de janela, 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;

A WINDOW explícita cláusula apenas encurta o código, sem afetar o desempenho.

first_value() do tipo composto


As funções agregadas min() ou max() não aceite tipos compostos como entrada. Você teria que criar funções de agregação personalizadas (o que não é tão difícil).
Mas as funções de janela first_value() e last_value() faça . Com base nisso, podemos conceber soluções simples:

Consulta simples

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;

A saída tem todos os dados, mas os valores da última semana são colocados em um registro anônimo (opcionalmente convertido para text ). Você pode precisar de valores decompostos.

Resultado decomposto com uso oportunista do tipo de tabela


Para isso, precisamos de um tipo composto bem conhecido. Uma definição de tabela adaptada permitiria o uso oportunista do próprio tipo de tabela diretamente:
CREATE TABLE tbl (week int, value int, name text);  -- optimized column order

week e value vem primeiro, então agora podemos classificar pelo próprio tipo de tabela:
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;

Resultado decomposto do tipo de linha definido pelo usuário


Isso provavelmente não é possível na maioria dos casos. Registre um tipo composto com CREATE TYPE (permanente) ou com CREATE TEMP TABLE (durante a sessão):
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;

Funções de agregação personalizadas first() &last()


Crie funções e agregações uma vez por banco de dados:
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
);

Então:
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;

Provavelmente a solução mais elegante. Mais rápido com o módulo adicional first_last_agg fornecendo uma implementação em C.
Compare as instruções no Postgres Wiki.

Relacionado:
  • Calculando o crescimento de seguidores ao longo do tempo para cada influenciador


db<>mexa aqui (mostrando tudo)
antigo sqlfiddle

Cada uma dessas consultas foi substancialmente mais rápida do que a resposta atualmente aceita em um teste rápido em uma tabela com 50 mil linhas com EXPLAIN ANALYZE .

Existem mais maneiras. Dependendo da distribuição de dados, diferentes estilos de consulta podem ser (muito) mais rápidos. Ver:
  • Otimize a consulta GROUP BY para recuperar a última linha por usuário