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
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
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
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;
_
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.
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.
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é.
Pour quelques lignes par client (cardinalité élevée dans la colonne customer
), cette méthode est très efficace. D'autant plus si vous avez quand même besoin d'une sortie triée. L'avantage diminue avec le nombre croissant de lignes par client.
Idéalement, vous avez assez de --- [_work_mem
_ ) == pour traiter l’étape de tri impliquée dans RAM et ne pas déborder sur le disque. Mais régler généralement _work_mem
_ trop haut peut avoir des effets néfastes. Considérez _SET LOCAL
_ pour les requêtes exceptionnellement volumineuses. Trouvez combien vous avez besoin avec _EXPLAIN ANALYZE
_. La mention " Disque: " dans l'étape de tri indique le besoin de plus d'informations:
Pour plusieurs lignes par client (faible cardinalité dans la colonne customer
), a scan d'index lâche = = (ou "skip scan") serait (beaucoup) plus efficace, mais cela n’a pas été implémenté jusqu’à Postgres 12. (Une implémentation pour les analyses à index uniquement est en développement pour Postgres 13. Voir ici et ici .)
Pour l'instant, il existe techniques d'interrogation plus rapides pour le remplacer. 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:
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 .
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 }.
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.array_agg
de la même manière pour votre troisième colonne de sortie, mais max(total)
est plus simple.DISTINCT ON
, utiliser array_agg
vous permet de conserver votre GROUP BY
, au cas où vous le souhaiteriez pour d'autres raisons.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;
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
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.
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.
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
OR
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.
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
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:
t1, t2 sont des alias de sous-requête qui pourraient être supprimés en fonction de la base de données.
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.
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
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
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