PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Obtenir les valeurs de la première et de la dernière ligne par groupe

Il existe différents moyens plus simples et plus rapides.

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 plus court :

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 et facile à comprendre. Aussi le plus rapide dans mes anciens tests. Explication détaillée pour DISTINCT ON :

  • Sélectionner la première ligne de chaque groupe GROUP BY ?

Fonction de fenêtre 2x, 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 explicite La clause ne fait que raccourcir le code, aucun effet sur les performances.

first_value() de type composite

Les fonctions d'agrégation min() ou max() n'accepte pas les types composites en entrée. Vous auriez à créer des fonctions d'agrégation personnalisées (ce qui n'est pas si difficile).
Mais la fenêtre fonctionne first_value() et last_value() faire . Sur cette base, nous pouvons concevoir des solutions simples :

Requête simple

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 sortie contient toutes les données, mais les valeurs de la semaine dernière sont insérées dans un enregistrement anonyme (éventuellement converti en text ). Vous aurez peut-être besoin de valeurs décomposées.

Résultat décomposé avec utilisation opportuniste du type table

Pour cela, nous avons besoin d'un type composite bien connu. Une définition de table adaptée permettrait l'utilisation opportuniste du type de table lui-même directement :

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

week et value viennent en premier, donc maintenant nous pouvons trier par le type de tableau lui-même :

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;

Résultat décomposé du type de ligne défini par l'utilisateur

Ce n'est probablement pas possible dans la plupart des cas. Enregistrez un type composite avec CREATE TYPE (permanent) ou avec CREATE TEMP TABLE (pour la durée de la session) :

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;

Fonctions d'agrégation personnalisées first() &last()

Créez des fonctions et des agrégats une fois par base de données :

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

Ensuite :

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;

Probablement la solution la plus élégante. Plus rapide avec le module supplémentaire first_last_agg fournissant une implémentation C.
Comparez les instructions dans le wiki Postgres.

Connexe :

  • Calculer la croissance des abonnés au fil du temps pour chaque influenceur

db<>jouez ici (tout afficher)
Ancien sqlfiddle

Chacune de ces requêtes était nettement plus rapide que la réponse actuellement acceptée dans un test rapide sur une table de 50 000 lignes avec EXPLAIN ANALYZE .

Il y a plus de façons. Selon la distribution des données, différents styles de requête peuvent être (beaucoup) plus rapides, pour le moment. Voir :

  • Optimiser la requête GROUP BY pour récupérer la dernière ligne par utilisateur