web-dev-qa-db-fra.com

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

Comme le titre l'indique, j'aimerais sélectionner la première ligne de chaque ensemble de lignes groupées avec un GROUP BY.

Plus précisément, si j'ai une table purchases qui ressemble à ceci:

SELECT * FROM purchases;

Ma sortie:

 id | client | total 
--- + ---------- + ------
 1 | Joe | 5 
 2 | Sally | 3 
 3 | Joe | 2 
 4 | Sally | 1

J'aimerais demander la id du plus gros achat (total) effectué par chaque customer. Quelque chose comme ça:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY total DESC;

Production attendue:

 FIRST (id) | client | PREMIER (total) 
---------- + ---------- + -------------
 1 | Joe | 5 
 2 | Sally | 3 
1009
David Wolever

Sur Oracle 9.2+ (pas 8i + comme indiqué à l'origine), SQL Server 2005+, PostgreSQL 8.4+, DB2, Firebird 3.0+, Teradata, Sybase, Vertica:

WITH summary AS (
    SELECT p.id, 
           p.customer, 
           p.total, 
           ROW_NUMBER() OVER(PARTITION BY p.customer 
                                 ORDER BY p.total DESC) AS rk
      FROM PURCHASES p)
SELECT s.*
  FROM summary s
 WHERE s.rk = 1

Pris en charge par n'importe quelle base de données:

Mais vous devez ajouter une logique pour rompre les liens:

  SELECT MIN(x.id),  -- change to MAX if you want the highest
         x.customer, 
         x.total
    FROM PURCHASES x
    JOIN (SELECT p.customer,
                 MAX(total) AS max_total
            FROM PURCHASES p
        GROUP BY p.customer) y ON y.customer = x.customer
                              AND y.max_total = x.total
GROUP BY x.customer, x.total
896
OMG Ponies

Dans PostgreSQL c'est généralement plus simple et plus rapide (plus d'optimisation des performances ci-dessous):

_SELECT DISTINCT ON (customer)
       id, customer, total
FROM   purchases
ORDER  BY customer, total DESC, id;_

Ou plus court (si pas aussi clair) avec des nombres ordinaux de colonnes de sortie:

_SELECT DISTINCT ON (2)
       id, customer, total
FROM   purchases
ORDER  BY 2, 3 DESC, 1;
_

Si total peut être NULL (cela ne fera pas mal, mais vous voudrez faire correspondre les index existants):

_...
ORDER  BY customer, total DESC NULLS LAST, id;_

Points majeurs

  • DISTINCT ON est une extension PostgreSQL du standard (où seul DISTINCT sur l'ensemble de la liste SELECT est défini).

  • Répertoriez n'importe quel nombre d'expressions dans la clause _DISTINCT ON_. La valeur de ligne combinée définit les doublons. Le manuel:

    Bien entendu, deux lignes sont considérées comme distinctes si elles diffèrent par au moins une valeur de colonne. Les valeurs nulles sont considérées égales dans cette comparaison.

    Gras accent mien.

  • _DISTINCT ON_ peut être combiné avec ORDER BY. Les expressions principales doivent correspondre aux expressions principales _DISTINCT ON_ dans le même ordre. Vous pouvez ajouter des expressions supplémentaires à _ORDER BY_ pour sélectionner une ligne particulière dans chaque groupe de pairs. J'ai ajouté id comme dernier élément pour rompre les liens:

    "Choisissez la rangée avec le plus petit id de chaque groupe partageant le plus élevé total."

    Pour ordonner les résultats d’une manière qui ne concorde pas avec l’ordre de tri déterminant le premier par groupe, vous pouvez imbriquer la requête ci-dessus dans une requête externe avec un autre _ORDER BY_. Comme:

  • Si total peut être NULL, vous voulez probablement la ligne avec la plus grande valeur non nulle. Ajouter NULLS LAST comme démontré. Détails:

  • La liste SELECT] n'est contrainte en aucune façon par les expressions dans _DISTINCT ON_ ou _ORDER BY_. (Pas nécessaire dans le cas simple ci-dessus):

    • Vous n'avez pas à inclure aucune des expressions dans _DISTINCT ON_ ou _ORDER BY_.

    • Vous pouvez inclure toute autre expression dans la liste SELECT. Ceci est essentiel pour remplacer des requêtes beaucoup plus complexes par des sous-requêtes et des fonctions d'agrégat/fenêtre.

  • J'ai testé avec les versions 8.3 à 12 de Postgres. Mais la fonctionnalité existe depuis au moins la version 7.1, donc toujours.

Indice

L'indice parfait de la requête ci-dessus serait un index multi-colonnes couvrant les trois colonnes dans la séquence correspondante et avec l'ordre de tri correspondant:

_CREATE INDEX purchases_3c_idx ON purchases (customer, total DESC, id);
_

Peut-être trop spécialisé. Mais utilisez-le si les performances en lecture de la requête sont cruciales. Si vous avez _DESC NULLS LAST_ dans la requête, utilisez la même chose dans l'index afin que l'ordre de tri corresponde et que l'index soit applicable.

Efficacité/Optimisation des performances

Pesez le coût et les avantages avant de créer des index personnalisés pour chaque requête. Le potentiel de l'indice ci-dessus dépend en grande partie de distribution des données.

L'index est utilisé car il fournit des données pré-triées. Dans Postgres 9.2 ou version ultérieure, la requête peut également bénéficier d'un analyse avec index uniquement si l'index est plus petit que la table sous-jacente. L'index doit cependant être scanné dans son intégralité.

Référence

J'avais un repère simple, qui est maintenant obsolète. Je l'ai remplacé par un repère détaillé dans cette réponse séparée .

1001

Ceci est un problème commun plus-n-par-groupe , qui a déjà été testé et hautement testé solutions optimisées . Personnellement, je préfère le solution de jointure à gauche de Bill Karwin (le message original avec de nombreuses autres solutions ).

Notez que de nombreuses solutions à ce problème courant peuvent être trouvées de manière surprenante dans l’une des sources les plus officielles, MySQL manual! Voir Exemples de requêtes courantes :: Les lignes contenant le maximum par groupe d'une certaine colonne }.

43
TMS

Dans Postgres, vous pouvez utiliser array_agg comme ceci:

SELECT  customer,
        (array_agg(id ORDER BY total DESC))[1],
        max(total)
FROM purchases
GROUP BY customer

Cela vous donnera la id du plus gros achat de chaque client.

Quelques points à noter:

  • array_agg est une fonction d'agrégat, elle fonctionne donc avec GROUP BY.
  • array_agg vous permet de spécifier un ordre limité à lui-même, afin de ne pas contraindre la structure de la requête. Il existe également une syntaxe pour la manière dont vous triez les valeurs NULL, si vous devez effectuer quelque chose de différent de la valeur par défaut.
  • Une fois que nous construisons le tableau, nous prenons le premier élément. (Les tableaux Postgres sont indexés 1, pas indexés 0).
  • Vous pouvez utiliser array_agg de la même manière pour votre troisième colonne de sortie, mais max(total) est plus simple.
  • Contrairement à DISTINCT ON, utiliser array_agg vous permet de conserver votre GROUP BY, au cas où vous le souhaiteriez pour d'autres raisons.
23
Paul A Jungwirth

La solution n’est pas très efficace, comme le souligne Erwin, en raison de la présence de sous-requêtes

select * from purchases p1 where total in
(select max(total) from purchases where p1.customer=customer) order by total desc;
11
user2407394

J'utilise cette manière (postgresql uniquement): https://wiki.postgresql.org/wiki/First/last_%28aggregate%29

-- Create a function that always returns the first non-NULL item
CREATE OR REPLACE FUNCTION public.first_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $1;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.first (
        sfunc    = public.first_agg,
        basetype = anyelement,
        stype    = anyelement
);

-- Create a function that always returns the last non-NULL item
CREATE OR REPLACE FUNCTION public.last_agg ( anyelement, anyelement )
RETURNS anyelement LANGUAGE sql IMMUTABLE STRICT AS $$
        SELECT $2;
$$;

-- And then wrap an aggregate around it
CREATE AGGREGATE public.last (
        sfunc    = public.last_agg,
        basetype = anyelement,
        stype    = anyelement
);

Ensuite, votre exemple devrait fonctionner presque tel quel:

SELECT FIRST(id), customer, FIRST(total)
FROM  purchases
GROUP BY customer
ORDER BY FIRST(total) DESC;

CAVEAT: Il ignore les lignes NULL


Edit 1 - Utilisez plutôt l'extension postgres

Maintenant, j'utilise cette manière: http://pgxn.org/dist/first_last_agg/

Pour installer sur Ubuntu 14.04:

apt-get install postgresql-server-dev-9.3 git build-essential -y
git clone git://github.com/wulczer/first_last_agg.git
cd first_last_app
make && Sudo make install
psql -c 'create extension first_last_agg'

C'est une extension postgres qui vous donne la première et la dernière fonction. apparemment plus rapide que la voie ci-dessus.


Edit 2 - Commande et filtrage

Si vous utilisez des fonctions d'agrégat (comme celles-ci), vous pouvez ordonner les résultats sans avoir besoin d'avoir les données déjà commandées:

http://www.postgresql.org/docs/current/static/sql-expressions.html#SYNTAX-AGGREGATES

Ainsi, l'exemple équivalent avec la commande serait quelque chose comme:

SELECT first(id order by id), customer, first(total order by id)
  FROM purchases
 GROUP BY customer
 ORDER BY first(total);

Bien sûr, vous pouvez commander et filtrer comme bon vous semble au sein de l’agrégat; c'est une syntaxe très puissante.

7
matiu

Solution très rapide

SELECT a.* 
FROM
    purchases a 
    JOIN ( 
        SELECT customer, min( id ) as id 
        FROM purchases 
        GROUP BY customer 
    ) b USING ( id );

et vraiment très rapide si la table est indexée par id:

create index purchases_id on purchases (id);

La requête:

SELECT purchases.*
FROM purchases
LEFT JOIN purchases as p 
ON 
  p.customer = purchases.customer 
  AND 
  purchases.total < p.total
WHERE p.total IS NULL

COMMENT ÇA MARCHE! (J'ai été là)

Nous voulons nous assurer que nous n'avons que le total le plus élevé pour chaque achat.


Quelques éléments théoriques (sautez cette partie si vous voulez seulement comprendre la requête)

Soit Total une fonction T (client, id) où il retourne une valeur à partir du nom et de l'id. vouloir prouver soit 

  • ∀x T (client, id)> T (client, x) (ce total est supérieur à tous les autrestotal pour ce client)

OR

  • ¬∃x T (client, id) <T (client, x) (il n'existe pas de total plus élevé pour Ce client)

La première approche nécessitera que nous obtenions tous les enregistrements de ce nom, ce que je n’aime pas vraiment. 

Le second nécessitera un moyen intelligent de dire qu’il ne peut y avoir d’enregistrement plus élevé que celui-ci.


Retour à SQL

Si nous partons rejoint la table sur le nom et le total étant inférieur à la table jointe:

      LEFT JOIN purchases as p 
      ON 
      p.customer = purchases.customer 
      AND 
      purchases.total < p.total

nous nous assurons que tous les enregistrements ayant un autre enregistrement avec le total le plus élevé pour le même utilisateur à rejoindre:

purchases.id, purchases.customer, purchases.total, p.id, p.customer, p.total
1           , Tom           , 200             , 2   , Tom   , 300
2           , Tom           , 300
3           , Bob           , 400             , 4   , Bob   , 500
4           , Bob           , 500
5           , Alice         , 600             , 6   , Alice   , 700
6           , Alice         , 700

Cela nous aidera à filtrer le total le plus élevé pour chaque achat sans regroupement requis:

WHERE p.total IS NULL

purchases.id, purchases.name, purchases.total, p.id, p.name, p.total
2           , Tom           , 300
4           , Bob           , 500
6           , Alice         , 700

Et c'est la réponse dont nous avons besoin.

5
khaled_gomaa

Utilisez la fonction ARRAY_AGG pour PostgreSQL , U-SQL , IBM DB2 et Google BigQuery SQL :

SELECT customer, (ARRAY_AGG(id ORDER BY total DESC))[1], MAX(total)
FROM purchases
GROUP BY customer
3
Valentin

La solution "prise en charge par n'importe quelle base de données" acceptée par OMG Ponies a bien fonctionné.

Ici, je propose une approche identique, mais plus complète et plus propre, quelle que soit la base de données. Les égalités sont prises en compte (supposons que l'on souhaite obtenir une seule ligne pour chaque client, voire plusieurs enregistrements pour un total maximum par client), et d'autres champs d'achat (par exemple, purchase_payment_id) seront sélectionnés pour les vraies lignes correspondantes dans la table des achats.

Pris en charge par n'importe quelle base de données:

select * from purchase
join (
    select min(id) as id from purchase
    join (
        select customer, max(total) as total from purchase
        group by customer
    ) t1 using (customer, total)
    group by customer
) t2 using (id)
order by customer

Cette requête est relativement rapide, en particulier lorsqu'il existe un index composite tel que (client, total) dans la table des achats.

Remarque:

  1. t1, t2 sont des alias de sous-requête qui pourraient être supprimés en fonction de la base de données.

  2. Caveat : la clause using (...) n'est actuellement pas prise en charge dans MS-SQL et la base de données Oracle à partir de cette modification de janvier 2017. Vous devez l'étendre vous-même, par exemple. on t2.id = purchase.id etc. La syntaxe USING fonctionne dans SQLite, MySQL et PostgreSQL.

2
Johnny Wong

Dans SQL Server, vous pouvez faire ceci:

SELECT *
FROM (
SELECT ROW_NUMBER()
OVER(PARTITION BY customer
ORDER BY total DESC) AS StRank, *
FROM Purchases) n
WHERE StRank = 1

Explication: Ici Groupe par est fait sur la base du client puis commandé par total puis chaque groupe de ce groupe reçoit le numéro de série StRank et nous prenons le premier client dont le StRank est 1

1
Diwas Poudel

Pour SQl Server, le moyen le plus efficace consiste à: 

with
ids as ( --condition for split table into groups
    select i from (values (9),(12),(17),(18),(19),(20),(22),(21),(23),(10)) as v(i) 
) 
,src as ( 
    select * from yourTable where  <condition> --use this as filter for other conditions
)
,joined as (
    select tops.* from ids 
    cross apply --it`s like for each rows
    (
        select top(1) * 
        from src
        where CommodityId = ids.i 
    ) as tops
)
select * from joined

et n'oubliez pas de créer un index clusterisé pour les colonnes utilisées

0
BazSTR
  • Si vous souhaitez sélectionner une ligne (par votre condition spécifique) dans l’ensemble des lignes agrégées. 

  • Si vous souhaitez utiliser une autre fonction d'agrégation (sum/avg) en plus de max/min. Ainsi, vous ne pouvez pas utiliser la moindre idée avec DISTINCT ON

Vous pouvez utiliser la sous-requête suivante:

SELECT  
    (  
       SELECT **id** FROM t2   
       WHERE id = ANY ( ARRAY_AGG( tf.id ) ) AND amount = MAX( tf.amount )   
    ) id,  
    name,   
    MAX(amount) ma,  
    SUM( ratio )  
FROM t2  tf  
GROUP BY name

Vous pouvez remplacer amount = MAX( tf.amount ) par toute condition de votre choix avec une seule restriction: cette sous-requête ne doit pas renvoyer plus d'une ligne.

Mais si vous voulez faire de telles choses, vous recherchez probablement des fonctions de fenêtre

0
Eugen Konkov