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

1326

Comme le titre le suggère, je voudrais sélectionner la première ligne de chaque ensemble de lignes regroupées avec un GROUP BY.

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

SELECT * FROM purchases;

Ma sortie:

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

Je voudrais demander idle plus gros achat ( total) effectué par chacun customer. Quelque chose comme ça:

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

Production attendue:

PREMIER (id) | client | PREMIER (total)
---------- + ---------- + -------------
        1 | Joe | 5
        2 | Sally | 3
David Wolever
la source
puisque vous ne recherchez que les plus grands, pourquoi ne pas les interroger MAX(total)?
phil294
4
@ phil294 interroger pour max (total) n'associera pas ce total à la valeur 'id' de la ligne sur laquelle il s'est produit.
gwideman

Réponses:

1117

Sur Oracle 9.2+ (et non 8i + comme indiqué initialement), 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 de la 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
Poneys OMG
la source
2
Informix 12.x prend également en charge les fonctions de fenêtre (le CTE doit cependant être converti en table dérivée). Et Firebird 3.0 prendra également en charge les fonctions
Windows
37
ROW_NUMBER() OVER(PARTITION BY [...])ainsi que d'autres optimisations m'ont aidé à faire passer une requête de 30 secondes à quelques millisecondes. Merci! (PostgreSQL 9.2)
Sam
8
S'il y a plusieurs achats avec également le plus élevé totalpour un client, la première requête renvoie un gagnant arbitraire (en fonction des détails de mise en œuvre; le idpeut changer à chaque exécution!). En règle générale (pas toujours), vous souhaitez une ligne par client, définie par des critères supplémentaires tels que "celui avec le plus petit id". Pour corriger, ajoutez idà la ORDER BYliste de row_number(). Ensuite, vous obtenez le même résultat qu'avec la 2e requête, ce qui est très inefficace dans ce cas. En outre, vous auriez besoin d'une autre sous-requête pour chaque colonne supplémentaire.
Erwin Brandstetter
2
BigQuery de Google prend également en charge la commande ROW_NUMBER () de la première requête. A fonctionné comme un charme pour nous
Praxiteles
2
Notez que la première version avec la fonction window fonctionne à partir de la version 3.25.0 de SQLite: sqlite.org/windowfunctions.html#history
brianz
1150

Dans PostgreSQL, cela 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 un nombre ordinal de colonnes de sortie:

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

Si totalpeut être NULL (ne fera pas de mal de toute façon, mais vous voudrez faire correspondre les index existants ):

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

Points majeurs

  • DISTINCT ONest une extension PostgreSQL de la norme (où seule DISTINCTla SELECTliste entière est définie).

  • Répertoriez n'importe quel nombre d'expressions dans la DISTINCT ONclause, la valeur de ligne combinée définit les doublons. Le manuel:

    De toute évidence, 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 comme égales dans cette comparaison.

    Accentuation mienne.

  • DISTINCT ONpeut être combiné avec ORDER BY. Les expressions principales dans ORDER BYdoivent être dans l'ensemble des expressions dans DISTINCT ON, mais vous pouvez réorganiser librement l'ordre parmi celles-ci. Exemple. Vous pouvez ajouter des expressions supplémentairesORDER BY pour sélectionner une ligne particulière dans chaque groupe de pairs. Ou, comme le dit le manuel :

    Les DISTINCT ONexpressions doivent correspondre aux ORDER BY expressions les plus à gauche . La ORDER BYclause contient normalement des expressions supplémentaires qui déterminent la priorité souhaitée des lignes dans chaque DISTINCT ONgroupe.

    J'ai ajouté idcomme dernier élément pour rompre les liens:
    "Choisissez la ligne avec le plus petit idde chaque groupe partageant le plus haut total."

    Pour classer les résultats d'une manière qui n'est pas conforme à l'ordre de tri déterminant le premier par groupe, vous pouvez imbriquer la requête ci-dessus dans une requête externe avec une autre ORDER BY. Exemple.

  • Si totalpeut être NULL, vous voulez très probablement la ligne avec la plus grande valeur non nulle. Ajoutez NULLS LASTcomme démontré. Voir:

  • La SELECTliste n'est pas contraint par des expressions dans DISTINCT ONou ORDER BYde quelque façon. (Pas nécessaire dans le cas simple ci-dessus):

    • Vous n'avez pas besoin d' inclure d'expressions dans DISTINCT ONou ORDER BY.

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

  • J'ai testé avec Postgres versions 8.3 - 12. Mais la fonctionnalité est là au moins depuis la version 7.1, donc en gros toujours.

Indice

L' index parfait pour la requête ci-dessus serait un index multi-colonnes couvrant les trois colonnes dans une séquence correspondante et avec un 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 de lecture pour la requête particulière sont cruciales. Si vous en avez DESC NULLS LASTdans la requête, utilisez la même chose dans l'index pour que l'ordre de tri corresponde et que l'index soit applicable.

Efficacité / Optimisation des performances

Évaluez les coûts 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 la 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'une analyse d'index uniquement si l'index est plus petit que la table sous-jacente. L'index doit cependant être analysé dans son intégralité.

Référence

J'avais ici une référence simple qui est désormais dépassée. Je l'ai remplacé par une référence détaillée dans cette réponse séparée .

Erwin Brandstetter
la source
28
C'est une excellente réponse pour la plupart des tailles de base de données, mais je tiens à souligner qu'à mesure que vous approchez, ~ un million de lignes DISTINCT ONdevient extrêmement lent. L'implémentation trie toujours la table entière et la parcourt pour rechercher les doublons, en ignorant tous les index (même si vous avez créé l'index multi-colonnes requis). Voir explexextended.com/2009/05/03/postgresql-optimizing-distinct pour une solution possible.
Meekohi
14
Utiliser des ordinaux pour "raccourcir le code" est une idée terrible. Que diriez-vous de laisser les noms des colonnes pour les rendre lisibles?
KOTJMF
13
@KOTJMF: Je vous suggère alors de choisir votre préférence personnelle. Je démontre les deux options pour éduquer. Le raccourci de syntaxe peut être utile pour les expressions longues dans la SELECTliste.
Erwin Brandstetter du
1
@jangorecki: La référence d'origine date de 2011, je n'ai plus la configuration. Mais il était temps de lancer des tests avec pg 9.4 et pg 9.5 de toute façon. Voir les détails dans la réponse ajoutée. . Vous pourriez ajouter un commentaire avec le résultat de votre installation ci-dessous?
Erwin Brandstetter
2
@PirateApp: Pas du haut de ma tête. DISTINCT ONn'est utile que pour obtenir une ligne par groupe de pairs.
Erwin Brandstetter
134

Référence

Test de la plupart des candidats intéressants avec Postgres 9.4 et 9.5 avec une table à mi - chemin réaliste de 200K lignes dans purchaseset 10k distinctscustomer_id ( moy. 20 lignes par client ).

Pour Postgres 9.5, j'ai effectué un deuxième test auprès de 86446 clients distincts. Voir ci-dessous ( moyenne de 2,3 lignes par client ).

Installer

Table principale

CREATE TABLE purchases (
  id          serial
, customer_id int  -- REFERENCES customer
, total       int  -- could be amount of money in Cent
, some_column text -- to make the row bigger, more realistic
);

J'utilise une serial(contrainte PK ajoutée ci-dessous) et un entier customer_idcar c'est une configuration plus typique. Également ajouté some_columnpour compenser généralement plus de colonnes.

Données factices, PK, index - une table typique a également quelques tuples morts:

INSERT INTO purchases (customer_id, total, some_column)    -- insert 200k rows
SELECT (random() * 10000)::int             AS customer_id  -- 10k customers
     , (random() * random() * 100000)::int AS total     
     , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int)
FROM   generate_series(1,200000) g;

ALTER TABLE purchases ADD CONSTRAINT purchases_id_pkey PRIMARY KEY (id);

DELETE FROM purchases WHERE random() > 0.9; -- some dead rows

INSERT INTO purchases (customer_id, total, some_column)
SELECT (random() * 10000)::int             AS customer_id  -- 10k customers
     , (random() * random() * 100000)::int AS total     
     , 'note: ' || repeat('x', (random()^2 * random() * random() * 500)::int)
FROM   generate_series(1,20000) g;  -- add 20k to make it ~ 200k

CREATE INDEX purchases_3c_idx ON purchases (customer_id, total DESC, id);

VACUUM ANALYZE purchases;

customer table - pour une requête supérieure

CREATE TABLE customer AS
SELECT customer_id, 'customer_' || customer_id AS customer
FROM   purchases
GROUP  BY 1
ORDER  BY 1;

ALTER TABLE customer ADD CONSTRAINT customer_customer_id_pkey PRIMARY KEY (customer_id);

VACUUM ANALYZE customer;

Dans mon deuxième test pour 9.5, j'ai utilisé la même configuration, mais avec random() * 100000pour générer customer_idpour obtenir seulement quelques lignes par customer_id.

Tailles des objets pour la table purchases

Généré avec cette requête .

               what                | bytes/ct | bytes_pretty | bytes_per_row
-----------------------------------+----------+--------------+---------------
 core_relation_size                | 20496384 | 20 MB        |           102
 visibility_map                    |        0 | 0 bytes      |             0
 free_space_map                    |    24576 | 24 kB        |             0
 table_size_incl_toast             | 20529152 | 20 MB        |           102
 indexes_size                      | 10977280 | 10 MB        |            54
 total_size_incl_toast_and_indexes | 31506432 | 30 MB        |           157
 live_rows_in_text_representation  | 13729802 | 13 MB        |            68
 ------------------------------    |          |              |
 row_count                         |   200045 |              |
 live_tuples                       |   200045 |              |
 dead_tuples                       |    19955 |              |

Requêtes

1. row_number()dans CTE, ( voir autre réponse )

WITH cte AS (
   SELECT id, customer_id, total
        , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn
   FROM   purchases
   )
SELECT id, customer_id, total
FROM   cte
WHERE  rn = 1;

2. row_number()en sous-requête (mon optimisation)

SELECT id, customer_id, total
FROM   (
   SELECT id, customer_id, total
        , row_number() OVER(PARTITION BY customer_id ORDER BY total DESC) AS rn
   FROM   purchases
   ) sub
WHERE  rn = 1;

3. DISTINCT ON( voir autre réponse )

SELECT DISTINCT ON (customer_id)
       id, customer_id, total
FROM   purchases
ORDER  BY customer_id, total DESC, id;

4. rCTE avec LATERALsous-requête ( voir ici )

WITH RECURSIVE cte AS (
   (  -- parentheses required
   SELECT id, customer_id, total
   FROM   purchases
   ORDER  BY customer_id, total DESC
   LIMIT  1
   )
   UNION ALL
   SELECT u.*
   FROM   cte c
   ,      LATERAL (
      SELECT id, customer_id, total
      FROM   purchases
      WHERE  customer_id > c.customer_id  -- lateral reference
      ORDER  BY customer_id, total DESC
      LIMIT  1
      ) u
   )
SELECT id, customer_id, total
FROM   cte
ORDER  BY customer_id;

5. customertable avec LATERAL( voir ici )

SELECT l.*
FROM   customer c
,      LATERAL (
   SELECT id, customer_id, total
   FROM   purchases
   WHERE  customer_id = c.customer_id  -- lateral reference
   ORDER  BY total DESC
   LIMIT  1
   ) l;

6. array_agg()avec ORDER BY( voir autre réponse )

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

Résultats

Temps d'exécution pour les requêtes ci-dessus avec EXPLAIN ANALYZE(et toutes les options désactivées ), le meilleur des 5 exécutions .

Toutes les requêtes ont utilisé un index analyse uniquement sur purchases2_3c_idx(entre autres étapes). Certains d'entre eux uniquement pour la plus petite taille de l'indice, d'autres plus efficacement.

A. Postgres 9.4 avec 200k lignes et ~ 20 par customer_id

1. 273.274 ms  
2. 194.572 ms  
3. 111.067 ms  
4.  92.922 ms  
5.  37.679 ms  -- winner
6. 189.495 ms

B. La même chose avec Postgres 9.5

1. 288.006 ms
2. 223.032 ms  
3. 107.074 ms  
4.  78.032 ms  
5.  33.944 ms  -- winner
6. 211.540 ms  

C. Identique à B., mais avec environ 2,3 lignes par customer_id

1. 381.573 ms
2. 311.976 ms
3. 124.074 ms  -- winner
4. 710.631 ms
5. 311.976 ms
6. 421.679 ms

Repères associés

En voici un nouveau par des tests «ogr» avec 10 millions de lignes et 60 000 «clients» uniques sur Postgres 11.5 (en date de septembre 2019). Les résultats sont toujours en ligne avec ce que nous avons vu jusqu'à présent:

Référence originale (obsolète) de 2011

J'ai exécuté trois tests avec PostgreSQL 9.1 sur une table réelle de 65579 lignes et des index btree à une colonne sur chacune des trois colonnes impliquées et j'ai pris le meilleur temps d'exécution de 5 exécutions.
Comparaison de la première requête de @OMGPonies ( A) à la solution ciDISTINCT ON - dessus ( B):

  1. Sélectionnez la table entière, ce qui donne 5958 lignes dans ce cas.

    A: 567.218 ms
    B: 386.673 ms
  2. Condition d'utilisation WHERE customer BETWEEN x AND yrésultant en 1 000 lignes.

    A: 249.136 ms
    B:  55.111 ms
  3. Sélectionnez un seul client avec WHERE customer = x.

    A:   0.143 ms
    B:   0.072 ms

Même test répété avec l'index décrit dans l'autre réponse

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

1A: 277.953 ms  
1B: 193.547 ms

2A: 249.796 ms -- special index not used  
2B:  28.679 ms

3A:   0.120 ms  
3B:   0.048 ms
Erwin Brandstetter
la source
5
Merci pour une excellente référence. Je me demandais si l'interrogation des données d' événements où vous avez un horodatage au lieu du total bénéficierait du nouvel index BRIN. Cela peut potentiellement accélérer les requêtes temporelles.
jangorecki
3
@jangorecki: Toute immense table contenant des données triées physiquement peut bénéficier d'un index BRIN.
Erwin Brandstetter
@ErwinBrandstetter Dans les exemples 2. row_number()et 5. customer table with LATERAL, qu'est-ce qui garantit que l'identifiant sera le plus petit?
Artem Novikov
@ArtemNovikov: Rien. L'objectif est de récupérer, par customer_id ligne avec le plus haut total. C'est une coïncidence trompeuse dans les données de test de la question que le iddans les lignes sélectionnées est également le plus petit par customer_id.
Erwin Brandstetter
1
@ArtemNovikov: pour autoriser les analyses d'index uniquement.
Erwin Brandstetter
55

C'est courant problème, qui a déjà des solutions bien testées et hautement optimisées . Personnellement, je préfère la solution de jointure gauche de Bill Karwin (le message d'origine avec beaucoup d'autres solutions ).

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

TMS
la source
22
En quoi le manuel MySQL est-il en quelque sorte «officiel» pour les questions Postgres / SQLite (sans parler de SQL)? De plus, pour être clair, la DISTINCT ONversion est beaucoup plus courte, plus simple et fonctionne généralement mieux dans Postgres que les alternatives avec self LEFT JOINou semi-anti-join avec NOT EXISTS. Il est également "bien testé".
Erwin Brandstetter
3
En plus de ce qu'Erwin a écrit, je dirais que l'utilisation d'une fonction de fenêtre (qui est une fonctionnalité SQL courante de nos jours) est presque toujours plus rapide que l'utilisation d'une jointure avec une table dérivée
a_horse_with_no_name
6
Bonnes références. Je ne savais pas que cela s'appelait le problème le plus important par groupe. Je vous remercie.
David Mann
La question ne concerne pas le plus grand n par groupe mais le premier n.
reinierpost
1
Dans un cas de deux champs de commande que j'ai essayé, "la solution de jointure gauche par Bill Karwin" donne de mauvaises performances. Voir mon commentaire ci-dessous stackoverflow.com/a/8749095/684229
Johnny Wong
30

Dans Postgres, vous pouvez utiliser array_aggcomme ceci:

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

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

Quelques points à noter:

  • array_aggest une fonction d'agrégation, donc elle fonctionne avec GROUP BY.
  • array_aggvous permet de spécifier un ordre limité à lui-même, afin qu'il ne limite pas la structure de la requête entière. Il existe également une syntaxe pour la façon dont vous triez les valeurs NULL, si vous devez faire quelque chose de différent de la valeur par défaut.
  • Une fois que nous avons construit le tableau, nous prenons le premier élément. (Les tableaux Postgres sont indexés 1, pas indexés 0).
  • Vous pouvez utiliser array_aggde manière similaire pour votre troisième colonne de sortie, maismax(total) c'est plus simple.
  • Contrairement à DISTINCT ON, l'utilisation array_aggvous permet de conserver votre GROUP BY, au cas où vous le souhaiteriez pour d'autres raisons.
Paul A Jungwirth
la source
14

La solution n'est pas très efficace comme l'a souligné Erwin, en raison de la présence de SubQ

select * from purchases p1 where total in
(select max(total) from purchases where p1.customer=customer) order by total desc;
user2407394
la source
Merci, oui d'accord avec vous, la jointure entre subq et requête externe prend en fait plus de temps. "In" ne sera pas un problème ici car le subq ne donnera qu'une seule ligne. BTW, quelle erreur de syntaxe pointez-vous ??
user2407394
ohh .. utilisé pour "Teradata" .. édité maintenant..cependant, rompre les liens n'est pas nécessaire ici car il doit trouver le total le plus élevé pour chaque client ..
user2407394
Vous savez que vous obtenez plusieurs lignes pour un seul client en cas d'égalité? Que cela soit souhaitable dépend des exigences exactes. Normalement, ce n'est pas le cas. Pour la question posée, le titre est assez clair.
Erwin Brandstetter
Cela ne ressort pas clairement de la question, si le même client a acheté = Max pour 2 identifiants différents, je pense que nous devrions afficher les deux.
user2407394
10

J'utilise de cette façon (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

J'utilise maintenant cette méthode: 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 les premières et dernières fonctions; apparemment plus rapide que la méthode ci-dessus.


Edit 2 - Tri et filtrage

Si vous utilisez des fonctions d'agrégation (comme celles-ci), vous pouvez ordonner les résultats, sans avoir besoin d'avoir les données déjà ordonné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 dans l'agrégat; c'est une syntaxe très puissante.

matiu
la source
En utilisant également cette approche de fonction personnalisée. Suffisamment universel et simple. Pourquoi compliquer les choses, cette solution est-elle nettement moins performante que les autres?
Sergey Shcherbakov
9

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 trucs 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 donnée le nom et l'id Pour prouver que le total donné (T (client, id)) est le plus élevé nous devons prouver que nous voulons prouver soit

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

OU

  • ¬∃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 pour ce nom que je n'aime pas vraiment.

Le second aura besoin d'un moyen intelligent pour dire qu'il ne peut y avoir d'enregistrement supérieur à celui-ci.


Retour à SQL

Si nous avons quitté 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 qui ont 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 nécessaire:

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.

khaled_gomaa
la source
8

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);
Alejandro Salamanca Mazuelo
la source
La clause USING est très standard. C'est juste que certains systèmes de base de données mineurs ne l'ont pas.
Holger Jakobs
2
Cela ne trouve pas l'achat des clients avec le plus grand total
Johnny Wong
7

Dans SQL Server, vous pouvez procéder comme suit:

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

Explication: Ici, Group by est effectué sur la base du client, puis commandez-le au total, puis chacun de ces groupes reçoit un numéro de série en tant que StRank et nous supprimons le premier client dont le StRank est 1

Diwas Poudel
la source
Je vous remercie! Cela fonctionnait parfaitement et était très facile à comprendre et à mettre en œuvre.
ruohola
4

Dans PostgreSQL, une autre possibilité est d'utiliser la first_valuefonction window en combinaison avec SELECT DISTINCT:

select distinct customer_id,
                first_value(row(id, total)) over(partition by customer_id order by total desc, id)
from            purchases;

J'ai créé un composite (id, total), donc les deux valeurs sont retournées par le même agrégat. Vous pouvez bien sûr toujours postuler first_value()deux fois.

pbillen
la source
3

La solution acceptée par OMG Ponies "Pris en charge par n'importe quelle base de données" a une bonne vitesse de mon test.

Ici, je propose une même approche, mais une solution de base de données plus complète et propre. Les liens sont pris en compte (supposons le désir d'obtenir une seule ligne pour chaque client, même plusieurs enregistrements pour le 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 d'achat.

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, surtout lorsqu'il existe un index composite comme (client, total) sur la table d'achat.

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. Attention : la using (...)clause n'est actuellement pas prise en charge dans MS-SQL et Oracle db à partir de cette édition de janvier 2017. Vous devez la développer vous-même, par exemple, on t2.id = purchase.idetc. La syntaxe USING fonctionne dans SQLite, MySQL et PostgreSQL.

Johnny Wong
la source
2

Snowflake / Teradata prend en charge la QUALIFYclause qui fonctionne comme HAVINGpour les fonctions fenêtrées:

SELECT id, customer, total
FROM PURCHASES
QUALIFY ROW_NUMBER() OVER(PARTITION BY p.customer ORDER BY p.total DESC) = 1
Lukasz Szozda
la source
1
  • Si vous souhaitez sélectionner une ligne (en fonction de certaines conditions spécifiques) dans l'ensemble de lignes agrégées.

  • Si vous souhaitez utiliser une autre sum/avgfonction d'agrégation ( ) en plus de max/min. Ainsi, vous ne pouvez pas utiliser l'indice avecDISTINCT 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 n'importe quelle condition que vous voulez avec une restriction: cette sous-requête ne doit pas renvoyer plus d'une ligne

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

Eugen Konkov
la source
1

Pour SQl Server, le moyen le plus efficace est:

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

BazSTR
la source