PostgreSQL DISTINCT ON con diferentes ORDER BY

216

Quiero ejecutar esta consulta:

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

Pero me sale este error:

PG :: Error: ERROR: las expresiones SELECT DISTINCT ON deben coincidir con las expresiones ORDER BY iniciales

Agregar address_idcomo primera ORDER BYexpresión silencia el error, pero realmente no quiero agregar la clasificación address_id. ¿Es posible hacerlo sin ordenar por address_id?

sl_bug
fuente
Su cláusula de pedido ha comprado_en lugar de dirección_. ¿Puede aclarar su pregunta?
Teja
mi pedido tiene compra porque lo quiero, pero postgres también pide dirección (ver mensaje de error).
sl_bug
Personalmente, creo que requerir DISTINCT ON para que coincida con ORDER BY es muy cuestionable, ya que hay una variedad de casos de uso legítimos para que difieran. Hay una publicación en postgresql.uservoice tratando de cambiar esto para aquellos que sienten lo mismo. postgresql.uservoice.com/forums/21853-general/suggestions/…
punto
tiene exactamente el mismo problema y enfrenta la misma limitación. Por el momento lo he dividido en una subconsulta y luego ordenando, pero se siente sucio.
Guy Park el

Respuestas:

208

La documentación dice:

DISTINCT ON (expresión [, ...]) mantiene solo la primera fila de cada conjunto de filas donde las expresiones dadas se evalúan como iguales. [...] Tenga en cuenta que la "primera fila" de cada conjunto es impredecible a menos que ORDER BY se utilice para garantizar que la fila deseada aparezca primero. [...] Las expresiones DISTINCT ON deben coincidir con las expresiones ORDER BY de la izquierda.

Documentación oficial

Entonces tendrás que agregar el address_id al orden por.

Alternativamente, si está buscando la fila completa que contiene el producto comprado más reciente para cada uno address_idy ese resultado ordenado porpurchased_at entonces, está tratando de resolver el mayor problema de N por grupo que se puede resolver mediante los siguientes enfoques:

La solución general que debería funcionar en la mayoría de los DBMS:

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

Una solución más orientada a PostgreSQL basada en la respuesta 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

Problema aclarado, extendido y resuelto aquí: selección de filas ordenadas por una columna y distintas en otra

Mosty Mostacho
fuente
40
Funciona, pero da un orden incorrecto. Es por eso que quiero deshacerme de address_id en orden cláusula
sl_bug
1
La documentación es clara: no puede porque la fila seleccionada será impredecible
Mosty Mostacho
3
Pero, ¿puede haber otra forma de seleccionar las últimas compras para direcciones discretas?
sl_bug
1
Si necesita ordenado por purchases.purchased_at, puede agregar purchased_at a sus condiciones distintas: SELECT DISTINCT ON (purchases.purchased_at, address_id). Sin embargo, dos registros con la misma dirección_id pero diferentes valores adquiridos_at generarán duplicados en el conjunto devuelto. Asegúrese de conocer los datos que está consultando.
Brendan Benson el
23
El espíritu de la pregunta es claro. No es necesario elegir la semántica. Es triste que la respuesta aceptada y más votada no te ayude a resolver el problema.
nicooga
55

Puede ordenar por address_id en una subconsulta, luego ordenar por lo que desee en una consulta externa.

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
fuente
3
Pero esto será más lento que una sola consulta, ¿no?
sl_bug
2
Muy marginalmente sí. Aunque ya que tienes compras. * En tu original select, ¿no creo que este es un código de producción?
hkf
8
Agregaría que para las versiones más nuevas de postgres necesita alias la subconsulta. Por ejemplo: SELECT * FROM (SELECT DISTINCT ON (address_id) compras.address_id, compras. * FROM "compras" DONDE "compras". "Product_id" = 1 ORDER BY address_id DESC) AS tmp ORDER BY tmp.purchased_at DESC
aembke
Esto volvería address_iddos veces (sin necesidad). Muchos clientes tienen problemas con nombres de columnas duplicados. ORDER BY address_id DESCEs inútil y engañoso. No hace nada útil en esta consulta. El resultado es una selección arbitraria de cada conjunto de filas con el mismo address_id, no la fila con la última purchased_at. La pregunta ambigua no lo solicitó explícitamente, pero esa es casi seguramente la intención del OP. En resumen: no use esta consulta . Publiqué alternativas con explicación.
Erwin Brandstetter
Trabajó para mi. Gran respuesta.
Matt West
46

Una subconsulta puede resolverlo:

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

Las expresiones iniciales en ORDER BYtienen que estar de acuerdo con las columnas DISTINCT ON, por lo que no puede ordenar por diferentes columnas en el mismoSELECT .

Solo use un adicional ORDER BYen la subconsulta si desea elegir una fila particular de cada conjunto:

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_atpuede ser NULL, considere DESC NULLS LAST. Pero asegúrese de que coincida con su índice si tiene la intención de usarlo. Ver:

Relacionado, con más explicaciones:

Erwin Brandstetter
fuente
No se puede usar DISTINCT ONsin una coincidencia ORDER BY. La primera consulta requiere un ORDER BY address_iddentro de la subconsulta.
Aristóteles Pagaltzis
44
@AristotlePagaltzis: Pero usted puede . De donde sea que lo hayas obtenido, es incorrecto. Puede usar DISTINCT ONsin ORDER BYen la misma consulta. Obtiene una fila arbitraria de cada conjunto de pares definidos por la DISTINCT ONcláusula en este caso. Pruébelo o siga los enlaces anteriores para obtener detalles y enlaces al manual. ORDER BYen la misma consulta (la misma SELECT) simplemente no puede estar en desacuerdo con DISTINCT ON. Yo también expliqué eso.
Erwin Brandstetter
Huh, tienes razon. Fui ciego a la implicación de la ORDER BYnota "impredecible a menos que se use" en los documentos porque no tiene sentido para mí que la función esté implementada para poder manejar conjuntos de valores no consecutivos ... pero no te permitirá explotar eso con un orden explícito. Molesto.
Aristóteles Pagaltzis
@AristotlePagaltzis: Eso se debe a que, internamente, Postgres utiliza uno de (al menos) dos algoritmos distintos: recorrer una lista ordenada o trabajar con valores hash, lo que promete ser más rápido. En el último caso, el resultado no está ordenado por DISTINCT ONexpresiones (todavía).
Erwin Brandstetter
2
Gracias. ¡Tus respuestas son siempre claras y útiles!
Andrey Deineko
10

La función de ventana puede resolver eso de una sola vez:

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
fuente
77
Sería bueno si alguien explicara la consulta.
Gajus
@Gajus: Breve explicación: no funciona, solo devuelve distinto address_id. Sin embargo, el principio podría funcionar. Ejemplos relacionados: stackoverflow.com/a/22064571/939860 o stackoverflow.com/a/11533808/939860 . Pero hay consultas más cortas y / o más rápidas para el problema en cuestión.
Erwin Brandstetter
5

Para cualquiera que use Flask-SQLAlchemy, esto funcionó para mí

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
fuente
2
Sí, o incluso más fácil, pude usar:query.distinct(foo).from_self().order(bar)
Laurent Meyer
@LaurentMeyer quieres decir Purchases.query?
reubano
Sí, quise decir Compras.consulta
Laurent Meyer
-2

También puede hacerlo utilizando la cláusula 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
fuente
Esto es incorrecto (a menos que purchasessolo tenga las dos columnasaddress_id y purchased_at). Debido a esto GROUP BY, necesitará usar una función agregada para obtener el valor de cada columna que no se usa para agrupar, por lo que todos los valores provendrán de diferentes filas del grupo a menos que pase por una gimnasia fea e ineficiente. Esto solo se puede solucionar mediante el uso de funciones de ventana en lugar de GROUP BY.
Aristóteles Pagaltzis