O PostgreSQL DISTINCT ON com diferentes pedidos por

216

Eu quero executar esta consulta:

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

Mas eu recebo este erro:

PG :: Erro: ERRO: As expressões SELECT DISTINCT ON devem corresponder às expressões ORDER BY iniciais

A adição address_idcomo primeira ORDER BYexpressão silencia o erro, mas eu realmente não quero adicionar a classificação address_id. É possível fazer sem pedir address_id?

sl_bug
fonte
A cláusula de seu pedido foi comprada - e não o endereço_id. Você pode esclarecer sua dúvida.
Teja
meu pedido foi comprado porque eu o desejo, mas o postgres também pede um endereço (consulte a mensagem de erro).
sl_bug
Pessoalmente, acho que exigir DISTINCT ON para corresponder a ORDER BY é muito questionável, pois há uma variedade de casos de uso legítimos para diferenciá-los. Há um post no postgresql.uservoice tentando mudar isso para aqueles que se sentem da mesma forma. postgresql.uservoice.com/forums/21853-general/suggestions/…
ponto
tem exatamente o mesmo problema e enfrenta a mesma limitação. No momento, dividi-o em uma subconsulta e depois pedi-lo, mas parece sujo.
Guy Park

Respostas:

207

A documentação diz:

DISTINCT ON (expressão [, ...]) mantém apenas a primeira linha de cada conjunto de linhas em que as expressões dadas são avaliadas como iguais. [...] Observe que a "primeira linha" de cada conjunto é imprevisível, a menos que ORDER BY seja usado para garantir que a linha desejada apareça primeiro. [...] As expressões DISTINCT ON devem corresponder às expressões ORDER BY mais à esquerda.

Documentação oficial

Então você terá que adicionar o address_id ao pedido por.

Como alternativa, se você estiver procurando a linha completa que contém o produto comprado mais recente para cada um address_ide o resultado classificado porpurchased_at então está tentando resolver o maior problema de N por grupo que pode ser resolvido pelas seguintes abordagens:

A solução geral que deve funcionar na maioria dos DBMSs:

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

Uma solução mais orientada para o PostgreSQL baseada na resposta da @ 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 esclarecido, estendido e resolvido aqui: Selecionando linhas ordenadas por alguma coluna e distintas em outra

Mosty Mostacho
fonte
40
Funciona, mas fornece pedidos incorretos. É por isso que querem se livrar de address_id na cláusula fim
sl_bug
1
A documentação é clara: você não pode porque a linha selecionada será imprevisível
Mosty Mostacho
3
Mas pode haver outra maneira de selecionar as compras mais recentes para endereços distintos?
21123 sl_bug
1
Se você precisa de ordem por purchases.purchased_at, você pode adicionar purchased_at às suas condições distintas: SELECT DISTINCT ON (purchases.purchased_at, address_id). No entanto, dois registros com o mesmo endereço_id, mas com valores diferentes de purchase_at resultarão em duplicatas no conjunto retornado. Verifique se você conhece os dados que está consultando.
Brendan Benson
23
O espírito da pergunta é claro. Não há necessidade de escolher semântica. É triste que a resposta aceita e a mais votada não ajude a resolver o problema.
Nicooga 7/04
55

Você pode solicitar por address_id em uma subconsulta e depois pelo que deseja em uma 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
fonte
3
Mas isso será mais lento do que apenas uma consulta, não?
sl_bug
2
Muito marginalmente sim. Embora desde que você tenha comprado. * No seu original select, não acho que seja um código de produção?
Hkf 20/03
8
Eu acrescentaria que, para versões mais recentes do postgres, você precisa alias a subconsulta. Por exemplo: SELECT * FROM (SELECT DISTINCT ON (address_id) compras.address_id, compras. * FROM "compras" WHERE "compras". "Product_id" = 1 PEDIDO POR address_id DESC) COMO tmp PEDIDO POR tmp.purchased_at DESC
aembke
Isso retornaria address_idduas vezes (sem necessidade). Muitos clientes têm problemas com nomes de colunas duplicados. ORDER BY address_id DESCé inútil e enganoso. Não faz nada útil nesta consulta. O resultado é uma seleção arbitrária de cada conjunto de linhas com o mesmo address_id, não da linha com a mais recente purchased_at. A pergunta ambígua não pediu isso explicitamente, mas essa é quase certamente a intenção do OP. Em resumo: não use esta consulta . Postei alternativas com explicações.
Erwin Brandstetter
Trabalhou para mim. Ótima resposta.
Matt West
46

Uma subconsulta pode resolvê-lo:

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

As expressões iniciais em ORDER BYprecisam concordar com as colunas DISTINCT ON, portanto, você não pode ordenar por colunas diferentes na mesma SELECT.

Use apenas um adicional ORDER BYna subconsulta se desejar escolher uma linha específica 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;

Se purchased_atpuder NULL, considere DESC NULLS LAST. Mas certifique-se de corresponder ao seu índice, se você pretende usá-lo. Vejo:

Relacionado, com mais explicações:

Erwin Brandstetter
fonte
Você não pode usar DISTINCT ONsem uma correspondência ORDER BY. A primeira consulta requer uma ORDER BY address_iddentro da subconsulta.
Aristóteles Pagaltzis
4
@AristotlePagaltzis: Mas você pode . De onde você tirou isso, está incorreto. Você pode usar DISTINCT ONsem ORDER BYna mesma consulta. Você obtém uma linha arbitrária de cada conjunto de pares definido pela DISTINCT ONcláusula nesse caso. Experimente ou siga os links acima para obter detalhes e links para o manual. ORDER BYna mesma consulta (o mesmo SELECT) simplesmente não pode discordar DISTINCT ON. Eu expliquei isso também.
Erwin Brandstetter
Huh, você está certo. Eu estava cego quanto à implicação da ORDER BYnota "imprevisível, a menos que seja usado" nos documentos, porque não faz sentido para mim que o recurso seja implementado para poder lidar com conjuntos de valores não consecutivos ... mas não permitirá que você explorar isso com uma ordem explícita. Irritante.
Aristóteles Pagaltzis
@AristotlePagaltzis: Isso ocorre porque, internamente, o Postgres usa um dos (pelo menos) dois algoritmos distintos: atravessar uma lista classificada ou trabalhar com valores de hash - o que promete ser mais rápido. No caso posterior, o resultado ainda não está classificado por DISTINCT ONexpressões.
Erwin Brandstetter
2
Obrigado. Suas respostas são sempre claras e úteis!
Andrey Deineko
10

A função Window pode resolver isso de uma só 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
fonte
7
Seria bom se alguém explicasse a consulta.
Gajus
@ Gajus: Breve explicação: não funciona, apenas retorna distinto address_id. O princípio poderia funcionar, no entanto. Exemplos relacionados: stackoverflow.com/a/22064571/939860 ou stackoverflow.com/a/11533808/939860 . Mas existem consultas mais curtas e / ou mais rápidas para o problema em questão.
Erwin Brandstetter
5

Para quem usa o Flask-SQLAlchemy, isso funcionou para mim

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
fonte
2
Sim, ou ainda mais fácil, eu era capaz de usar:query.distinct(foo).from_self().order(bar)
Laurent Meyer
@LaurentMeyer, você quer dizer Purchases.query?
reubano
Sim, eu quis dizer Purchases.query
Laurent Meyer
-2

Você também pode fazer isso usando a 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
fonte
Isso está incorreto (a menos que purchasestenha apenas as duas colunas address_ide purchased_at). Por isso GROUP BY, você precisará usar uma função agregada para obter o valor de cada coluna não usada para agrupamento, para que todos os valores sejam provenientes de linhas diferentes do grupo, a menos que você pratique uma ginástica feia e ineficiente. Isso pode ser corrigido apenas usando as funções da janela em vez de GROUP BY.
Aristóteles Pagaltzis