web-dev-qa-db-fra.com

PostgreSQL DISTINCT ON avec différents ORDER BY

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

Ajouter address_id en tant que première expression ORDER BY réduit au silence l'erreur, mais je ne souhaite vraiment pas ajouter de tri sur address_id. Est-il possible de faire sans commander par address_id?

177
sl_bug

La documentation dit:

DISTINCT ON (expression [ ...]) conserve uniquement la première ligne de chaque ensemble de lignes où les expressions données sont considérées comme égales. [...] Notez que la "première ligne" de chaque ensemble est imprévisible sauf si ORDER BY est utilisé pour s'assurer 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 par.

Alternativement, si vous recherchez la ligne complète contenant le dernier produit acheté pour chaque address_id et que le résultat est trié par purchased_at, vous essayez de résoudre le plus grand problème de N par groupe qui puisse être résolus 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 sur une autre

169
Mosty Mostacho

Vous pouvez commander par adresse_id dans une sous-requête, puis 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
51
hkf

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 BY doivent correspondre aux colonnes dans DISTINCT ON, de sorte que vous ne pouvez pas classer par colonnes différentes dans la même SELECT.

Utilisez uniquement un ORDER BY supplémentaire dans la sous-requête si vous souhaitez sélectionner une ligne particulière dans chaque jeu:

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 purchased_at peut être NULL, considérons DESC NULLS LAST.
En relation, avec plus d'explications:

37
Erwin Brandstetter

La fonction window peut résoudre ce problème 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)
10
savenkov

Pour 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))
4
reubano
SELECT DISTINCT ON (address_id) purchases.address_id, purchases.*
FROM purchases
WHERE purchases.product_id = 1
ORDER BY address_id, purchases.purchased_at DESC

ORDER BY adresse_id, achats.purchased_at DESC

address_id doit être ajouté dans l'ordre pour pour la fonction DISTINCT ON ()

0
REMITH