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 purchases
table 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 id
le 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
sql
sqlite
postgresql
group-by
greatest-n-per-group
David Wolever
la source
la source
MAX(total)
?Réponses:
Sur Oracle 9.2+ (et non 8i + comme indiqué initialement), SQL Server 2005+, PostgreSQL 8.4+, DB2, Firebird 3.0+, Teradata, Sybase, Vertica:
Pris en charge par n'importe quelle base de données:
Mais vous devez ajouter de la logique pour rompre les liens:
la source
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)total
pour un client, la première requête renvoie un gagnant arbitraire (en fonction des détails de mise en œuvre; leid
peut 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 petitid
". Pour corriger, ajoutezid
à laORDER BY
liste derow_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.Dans PostgreSQL, cela est généralement plus simple et plus rapide (plus d'optimisation des performances ci-dessous):
Ou plus court (si pas aussi clair) avec un nombre ordinal de colonnes de sortie:
Si
total
peut être NULL (ne fera pas de mal de toute façon, mais vous voudrez faire correspondre les index existants ):Points majeurs
DISTINCT ON
est une extension PostgreSQL de la norme (où seuleDISTINCT
laSELECT
liste entière est définie).Répertoriez n'importe quel nombre d'expressions dans la
DISTINCT ON
clause, la valeur de ligne combinée définit les doublons. Le manuel:Accentuation mienne.
DISTINCT ON
peut être combiné avecORDER BY
. Les expressions principales dansORDER BY
doivent être dans l'ensemble des expressions dansDISTINCT 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 :J'ai ajouté
id
comme dernier élément pour rompre les liens:"Choisissez la ligne avec le plus petit
id
de chaque groupe partageant le plus hauttotal
."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
total
peut être NULL, vous voulez très probablement la ligne avec la plus grande valeur non nulle. AjoutezNULLS LAST
comme démontré. Voir:La
SELECT
liste n'est pas contraint par des expressions dansDISTINCT ON
ouORDER BY
de quelque façon. (Pas nécessaire dans le cas simple ci-dessus):Vous n'avez pas besoin d' inclure d'expressions dans
DISTINCT ON
ouORDER BY
.Vous pouvez inclure toute autre expression dans la
SELECT
liste. 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:
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 LAST
dans 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é.
Pour quelques lignes par client (cardinalité élevée en colonne
customer
), c'est très efficace. Encore plus si vous avez quand même besoin d'une sortie triée. L'avantage diminue avec un nombre croissant de lignes par client.Idéalement, vous en avez assez
work_mem
pour traiter l'étape de tri impliquée dans la RAM et ne pas renverser sur le disque. Mais généralement, un réglagework_mem
trop élevé peut avoir des effets négatifs. Considérez lesSET LOCAL
requêtes exceptionnellement importantes. Trouvez combien vous en avez besoinEXPLAIN ANALYZE
. La mention de " Disque: " dans l'étape de tri indique le besoin de plus:Pour de nombreuses lignes par client (faible cardinalité dans la colonne
customer
), une analyse d'index lâche (alias "ignorer l'analyse") serait (beaucoup) plus efficace, mais elle n'est pas mise en œuvre jusqu'à Postgres 12. (Une implémentation pour les analyses d'index uniquement se trouve dans développement pour Postgres 13. Voir ici et ici .)Pour l'instant, il existe des techniques de requête plus rapides pour remplacer cela. En particulier si vous avez une table séparée contenant des clients uniques, ce qui est le cas d'utilisation typique. Mais aussi si vous ne le faites pas:
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 .
la source
DISTINCT ON
devient 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.SELECT
liste.DISTINCT ON
n'est utile que pour obtenir une ligne par groupe de pairs.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
purchases
et 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
J'utilise une
serial
(contrainte PK ajoutée ci-dessous) et un entiercustomer_id
car c'est une configuration plus typique. Également ajoutésome_column
pour compenser généralement plus de colonnes.Données factices, PK, index - une table typique a également quelques tuples morts:
customer
table - pour une requête supérieureDans mon deuxième test pour 9.5, j'ai utilisé la même configuration, mais avec
random() * 100000
pour générercustomer_id
pour obtenir seulement quelques lignes parcustomer_id
.Tailles des objets pour la table
purchases
Généré avec cette requête .
Requêtes
1.
row_number()
dans CTE, ( voir autre réponse )2.
row_number()
en sous-requête (mon optimisation)3.
DISTINCT ON
( voir autre réponse )4. rCTE avec
LATERAL
sous-requête ( voir ici )5.
customer
table avecLATERAL
( voir ici )6.
array_agg()
avecORDER BY
( voir autre réponse )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
B. La même chose avec Postgres 9.5
C. Identique à B., mais avec environ 2,3 lignes par
customer_id
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
):Sélectionnez la table entière, ce qui donne 5958 lignes dans ce cas.
Condition d'utilisation
WHERE customer BETWEEN x AND y
résultant en 1 000 lignes.Sélectionnez un seul client avec
WHERE customer = x
.Même test répété avec l'index décrit dans l'autre réponse
la source
2. row_number()
et5. customer table with LATERAL
, qu'est-ce qui garantit que l'identifiant sera le plus petit?customer_id
ligne avec le plus hauttotal
. C'est une coïncidence trompeuse dans les données de test de la question que leid
dans les lignes sélectionnées est également le plus petit parcustomer_id
.C'est courant plus grand n par groupeproblè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 .
la source
DISTINCT ON
version est beaucoup plus courte, plus simple et fonctionne généralement mieux dans Postgres que les alternatives avec selfLEFT JOIN
ou semi-anti-join avecNOT EXISTS
. Il est également "bien testé".Dans Postgres, vous pouvez utiliser
array_agg
comme ceci:Cela vous donnera la
id
le plus gros achat de chaque client.Quelques points à noter:
array_agg
est une fonction d'agrégation, donc elle fonctionne avecGROUP BY
.array_agg
vous 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.array_agg
de manière similaire pour votre troisième colonne de sortie, maismax(total)
c'est plus simple.DISTINCT ON
, l'utilisationarray_agg
vous permet de conserver votreGROUP BY
, au cas où vous le souhaiteriez pour d'autres raisons.la source
La solution n'est pas très efficace comme l'a souligné Erwin, en raison de la présence de SubQ
la source
J'utilise de cette façon (postgresql uniquement): https://wiki.postgresql.org/wiki/First/last_%28aggregate%29
Ensuite, votre exemple devrait fonctionner presque tel quel:
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:
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:
Ainsi, l'exemple équivalent, avec la commande serait quelque chose comme:
Bien sûr, vous pouvez commander et filtrer comme bon vous semble dans l'agrégat; c'est une syntaxe très puissante.
la source
La requête:
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
OU
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:
nous nous assurons que tous les enregistrements qui ont un autre enregistrement avec le total le plus élevé pour le même utilisateur à rejoindre:
Cela nous aidera à filtrer le total le plus élevé pour chaque achat sans regroupement nécessaire:
Et c'est la réponse dont nous avons besoin.
la source
Solution très rapide
et vraiment très rapide si la table est indexée par id:
la source
Dans SQL Server, vous pouvez procéder comme suit:
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
la source
Utilisez la
ARRAY_AGG
fonction pour PostgreSQL , U-SQL , IBM DB2 et Google BigQuery SQL :la source
Dans PostgreSQL, une autre possibilité est d'utiliser la
first_value
fonction window en combinaison avecSELECT DISTINCT
: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 postulerfirst_value()
deux fois.la source
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:
Cette requête est relativement rapide, surtout lorsqu'il existe un index composite comme (client, total) sur la table d'achat.
Remarque:
t1, t2 sont des alias de sous-requête qui pourraient être supprimés en fonction de la base de données.
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.id
etc. La syntaxe USING fonctionne dans SQLite, MySQL et PostgreSQL.la source
Snowflake / Teradata prend en charge la
QUALIFY
clause qui fonctionne commeHAVING
pour les fonctions fenêtrées:la source
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/avg
fonction d'agrégation ( ) en plus demax/min
. Ainsi, vous ne pouvez pas utiliser l'indice avecDISTINCT ON
Vous pouvez utiliser la sous-requête suivante:
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 ligneMais si vous voulez faire de telles choses, vous cherchez probablement des fonctions de fenêtre
la source
Pour SQl Server, le moyen le plus efficace est:
et n'oubliez pas de créer un index clusterisé pour les colonnes utilisées
la source