PostgreSQL DISTINCT ON avec différents ORDER BY

216

Je veux exécuter cette requête:

SELECT DISTINCT ON (address_id) purchases.address_id, purchases.*
FROM purchases
WHERE purchases.product_id = 1
ORDER BY purchases.purchased_at DESC

Mais je reçois cette erreur:

PG :: Erreur: ERREUR: les expressions SELECT DISTINCT ON doivent correspondre aux expressions ORDER BY initiales

L'ajout en address_idtant que première ORDER BYexpression fait taire l'erreur, mais je ne veux vraiment pas ajouter de tri address_id. Est-il possible de faire sans passer commande par address_id?

sl_bug
la source
Votre clause de commande a acheté_et non address_id.Pouvez-vous clarifier votre question.
Teja
ma commande a été achetée car je le veux, mais postgres demande également une adresse (voir message d'erreur).
sl_bug
3
Réponse
Personnellement, je pense que l'exigence de DISTINCT ON pour correspondre à ORDER BY est très discutable, car il existe une variété de cas d'utilisation légitimes pour les faire différer. Il y a un post sur postgresql.uservoice essayant de changer cela pour ceux qui pensent de la même manière. postgresql.uservoice.com/forums/21853-general/suggestions/…
point
a obtenu exactement le même problème, et face à la même limitation. Pour le moment, je l'ai divisé en une sous-requête, puis en commandant, mais cela semble sale.
Guy Park

Réponses:

208

La documentation dit:

DISTINCT ON (expression [, ...]) ne conserve que la première ligne de chaque ensemble de lignes où les expressions données sont égales. [...] Notez que la "première ligne" de chaque ensemble est imprévisible sauf si ORDER BY est utilisé pour garantir que la ligne souhaitée apparaît en premier. [...] Les expressions DISTINCT ON doivent correspondre aux expressions ORDER BY les plus à gauche.

Documentation officielle

Vous devrez donc ajouter le address_idà la commande avant le.

Alternativement, si vous recherchez la ligne complète qui contient le produit acheté le plus récent pour chacun address_idet ce résultat trié parpurchased_at alors vous essayez de résoudre un plus grand problème N par groupe qui peut être résolu par les approches suivantes:

La solution générale qui devrait fonctionner dans la plupart des SGBD:

SELECT t1.* FROM purchases t1
JOIN (
    SELECT address_id, max(purchased_at) max_purchased_at
    FROM purchases
    WHERE product_id = 1
    GROUP BY address_id
) t2
ON t1.address_id = t2.address_id AND t1.purchased_at = t2.max_purchased_at
ORDER BY t1.purchased_at DESC

Une solution plus orientée PostgreSQL basée sur la réponse de @ hkf:

SELECT * FROM (
  SELECT DISTINCT ON (address_id) *
  FROM purchases 
  WHERE product_id = 1
  ORDER BY address_id, purchased_at DESC
) t
ORDER BY purchased_at DESC

Problème clarifié, étendu et résolu ici: sélection de lignes ordonnées par une colonne et distinctes d'une autre

Mosty Mostacho
la source
40
Cela fonctionne, mais donne un mauvais ordre. C'est pourquoi je veux me débarrasser de address_id dans la clause d'ordre
sl_bug
1
La documentation est claire: vous ne pouvez pas, car la ligne sélectionnée sera imprévisible
Mosty Mostacho
3
Mais peut-être existe-t-il une autre façon de sélectionner les derniers achats pour les adresses disticnt?
sl_bug
1
Si vous avez besoin de commander par purchases.purchased_at, vous pouvez ajouter à vos purchased_at conditions DISTINCTS: SELECT DISTINCT ON (purchases.purchased_at, address_id). Cependant, deux enregistrements avec le même adresse_id mais différentes valeurs de purchase_at entraîneront des doublons dans l'ensemble renvoyé. Assurez-vous que vous connaissez les données que vous interrogez.
Brendan Benson,
23
L'esprit de la question est clair. Pas besoin de choisir la sémantique. Il est triste que la réponse acceptée et la plus votée ne vous aide pas à résoudre le problème.
nicooga
55

Vous pouvez trier par address_id dans une sous-requête, puis trier par ce que vous voulez dans une requête externe.

SELECT * FROM 
    (SELECT DISTINCT ON (address_id) purchases.address_id, purchases.* 
    FROM "purchases" 
    WHERE "purchases"."product_id" = 1 ORDER BY address_id DESC ) 
ORDER BY purchased_at DESC
hkf
la source
3
Mais cela sera plus lent qu'une seule requête, non?
sl_bug
2
Très marginalement oui. Bien que puisque vous avez des achats. * Dans votre original select, je ne pense pas que ce soit du code de production?
hkf
8
J'ajouterais que pour les nouvelles versions de postgres, vous devez alias la sous-requête. Par exemple: SELECT * FROM (SELECT DISTINCT ON (address_id) achats.address_id, achats. * FROM "achats" O "" achats "." Product_id "= 1 ORDER BY address_id DESC) AS tmp ORDER BY tmp.purchased_at DESC
aembke
Cela reviendrait address_iddeux fois (sans besoin). De nombreux clients ont des problèmes avec les noms de colonne en double. ORDER BY address_id DESCest inutile et trompeur. Il ne fait rien d'utile dans cette requête. Le résultat est un choix arbitraire dans chaque ensemble de lignes avec le même address_id, pas la ligne avec le dernier purchased_at. La question ambiguë ne le demandait pas explicitement, mais c'est presque certainement l'intention du PO. En bref: n'utilisez pas cette requête . J'ai posté des alternatives avec explication.
Erwin Brandstetter
A travaillé pour moi. Très bonne réponse.
Matt West
46

Une sous-requête peut le résoudre:

SELECT *
FROM  (
    SELECT DISTINCT ON (address_id) *
    FROM   purchases
    WHERE  product_id = 1
    ) p
ORDER  BY purchased_at DESC;

Les expressions principales dans ORDER BYdoivent être d'accord avec les colonnes dans DISTINCT ON, vous ne pouvez donc pas classer par différentes colonnes dans la même SELECT.

N'utilisez un ORDER BYélément supplémentaire dans la sous-requête que si vous souhaitez sélectionner une ligne particulière dans chaque ensemble:

SELECT *
FROM  (
    SELECT DISTINCT ON (address_id) *
    FROM   purchases
    WHERE  product_id = 1
    ORDER  BY address_id, purchased_at DESC  -- get "latest" row per address_id
    ) p
ORDER  BY purchased_at DESC;

Si c'est purchased_atpossible NULL, réfléchissez DESC NULLS LAST. Mais assurez-vous de faire correspondre votre index si vous avez l'intention de l'utiliser. Voir:

Connexes, avec plus d'explications:

Erwin Brandstetter
la source
Vous ne pouvez pas utiliser DISTINCT ONsans correspondance ORDER BY. La première requête nécessite un ORDER BY address_idà l'intérieur de la sous-requête.
Aristotle Pagaltzis
4
@AristotlePagaltzis: Mais vous le pouvez . D'où que vous veniez, c'est incorrect. Vous pouvez utiliser DISTINCT ONsans ORDER BYdans la même requête. Vous obtenez une ligne arbitraire de chaque ensemble de pairs défini par la DISTINCT ONclause dans ce cas. Essayez-le ou suivez les liens ci-dessus pour plus de détails et des liens vers le manuel. ORDER BYdans la même requête (la même SELECT) ne peut tout simplement pas être en désaccord avec DISTINCT ON. Je l'ai expliqué aussi.
Erwin Brandstetter
Tu as raison. J'étais aveugle à l'implication de la ORDER BYnote «imprévisible sauf si utilisé» dans les documents, car cela n'a pas de sens pour moi que la fonctionnalité soit implémentée pour pouvoir traiter des ensembles de valeurs non consécutifs… mais ne vous permettra pas de exploiter cela avec un ordre explicite. Énervant.
Aristote Pagaltzis
@AristotlePagaltzis: En effet, Postgres utilise en interne l'un des (au moins) deux algorithmes distincts: parcourez une liste triée ou travaillez avec des valeurs de hachage - selon ce qui promet d'être plus rapide. Dans le dernier cas, le résultat n'est pas DISTINCT ON(encore) trié par expressions.
Erwin Brandstetter
2
Je vous remercie. Vos réponses sont toujours claires et utiles!
Andrey Deineko
10

La fonction de fenêtre peut résoudre cela en un seul passage:

SELECT DISTINCT ON (address_id) 
   LAST_VALUE(purchases.address_id) OVER wnd AS address_id
FROM "purchases"
WHERE "purchases"."product_id" = 1
WINDOW wnd AS (
   PARTITION BY address_id ORDER BY purchases.purchased_at DESC
   ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
savenkov
la source
7
Ce serait bien si quelqu'un expliquait la requête.
Gajus
@Gajus: Brève explication: cela ne fonctionne pas, ne renvoie que distinct address_id. Le principe pourrait cependant fonctionner. Exemples connexes: stackoverflow.com/a/22064571/939860 ou stackoverflow.com/a/11533808/939860 . Mais il existe des requêtes plus courtes et / ou plus rapides pour le problème en question.
Erwin Brandstetter
5

Pour tous ceux qui utilisent Flask-SQLAlchemy, cela a fonctionné pour moi

from app import db
from app.models import Purchases
from sqlalchemy.orm import aliased
from sqlalchemy import desc

stmt = Purchases.query.distinct(Purchases.address_id).subquery('purchases')
alias = aliased(Purchases, stmt)
distinct = db.session.query(alias)
distinct.order_by(desc(alias.purchased_at))
reubano
la source
2
Oui, ou encore plus simple, j'ai pu utiliser:query.distinct(foo).from_self().order(bar)
Laurent Meyer
@LaurentMeyer veux-tu dire Purchases.query?
reubano
Oui, je voulais dire Purchases.query
Laurent Meyer
-2

Vous pouvez également le faire en utilisant la clause group by

   SELECT purchases.address_id, purchases.* FROM "purchases"
    WHERE "purchases"."product_id" = 1 GROUP BY address_id,
purchases.purchased_at ORDER purchases.purchased_at DESC
vaishali
la source
Ceci est incorrect (sauf si ne purchasescontient que les deux colonnes address_idet purchased_at). À cause de cela GROUP BY, vous devrez utiliser une fonction d'agrégation pour obtenir la valeur de chaque colonne non utilisée pour le regroupement, de sorte que ces valeurs proviendront toutes de différentes lignes du groupe, sauf si vous passez par une gymnastique laide et inefficace. Cela ne peut être résolu qu'en utilisant des fonctions de fenêtre plutôt que GROUP BY.
Aristotle Pagaltzis