sql >> Base de Datos >  >> RDS >> PostgreSQL

Obtener valores de la primera y última fila por grupo

Hay varias formas más simples y 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);

O más corto:

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

Simple y fácil de entender. También más rápido en mis pruebas anteriores. Explicación detallada de DISTINCT ON :

  • ¿Seleccionar la primera fila en cada grupo GROUP BY?

2x función de ventana, 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 explícita La cláusula solo acorta el código, no afecta el rendimiento.

first_value() de tipo compuesto

Las funciones agregadas min() o max() no acepte tipos compuestos como entrada. Tendría que crear funciones agregadas personalizadas (que no es tan difícil).
Pero las funciones de ventana first_value() y last_value() hacer . Basándonos en eso, podemos idear soluciones simples:

Consulta sencilla

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;

La salida tiene todos los datos, pero los valores de la última semana se introducen en un registro anónimo (opcionalmente convertido a text) ). Es posible que necesite valores descompuestos.

Resultado descompuesto con uso oportunista del tipo de tabla

Para eso necesitamos un tipo compuesto bien conocido. Una definición de tabla adaptada permitiría el uso oportunista del tipo de tabla directamente:

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

week y value vienen primero, así que ahora podemos ordenar por el tipo de tabla en sí:

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 descompuesto del tipo de fila definido por el usuario

Eso probablemente no sea posible en la mayoría de los casos. Registre un tipo compuesto con CREATE TYPE (permanente) o con CREATE TEMP TABLE (durante la duración de la sesión):

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;

Funciones agregadas personalizadas first() &last()

Cree funciones y agregados una vez por base de datos:

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

Entonces:

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;

Probablemente la solución más elegante. Más rápido con el módulo adicional first_last_agg proporcionando una implementación en C.
Compare las instrucciones en Postgres Wiki.

Relacionado:

  • Calcular el crecimiento de seguidores a lo largo del tiempo para cada influencer

db<>violín aquí (mostrando todo)
Sqlfiddle antiguo

Cada una de estas consultas fue sustancialmente más rápida que la respuesta actualmente aceptada en una prueba rápida en una tabla con 50k filas con EXPLAIN ANALYZE .

Hay más formas. Dependiendo de la distribución de datos, los diferentes estilos de consulta pueden ser (mucho) más rápidos aún. Ver:

  • Optimizar la consulta GROUP BY para recuperar la fila más reciente por usuario