PostgreSQL DISTINCT ON con diversi ORDER BY


216

Voglio eseguire questa query:

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

Ma ottengo questo errore:

PG :: Errore: ERRORE: le espressioni SELECT DISTINCT ON devono corrispondere alle espressioni ORDER BY iniziali

L'aggiunta address_idcome prima ORDER BYespressione mette a tacere l'errore, ma non voglio davvero aggiungere l'ordinamento address_id. È possibile fare a meno di ordinare per address_id?


La clausola del tuo ordine è stata acquistata_non indirizzo_id. Puoi chiarire la tua domanda.
Teja,

il mio ordine è stato acquistato perché lo voglio, ma Postgres richiede anche l'indirizzo (vedi messaggio di errore).
sl_bug


Personalmente penso che richiedere a DISTINCT ON di abbinare ORDER BY sia molto discutibile, poiché ci sono una varietà di casi d'uso legittimi per farli differire. C'è un post su postgresql.uservoice che cerca di cambiarlo per coloro che si sentono allo stesso modo. postgresql.uservoice.com/forums/21853-general/suggestions/…
punto

ottenne lo stesso identico problema e affrontò la stessa limitazione. Al momento l'ho suddiviso in una sottoquery e poi l'ordinazione, ma mi sembra sporco.
Guy Park,

Risposte:


208

La documentazione dice:

DISTINCT ON (espressione [, ...]) mantiene solo la prima riga di ogni serie di righe in cui le espressioni fornite valutano uguali. [...] Notare che la "prima riga" di ogni set è imprevedibile a meno che non venga utilizzato ORDER BY per garantire che la riga desiderata appaia per prima. [...] Le espressioni DISTINCT ON devono corrispondere alle espressioni ORDER BY più a sinistra.

Documentazione ufficiale

Quindi dovrai aggiungere address_idl'ordine all'ordine.

In alternativa, se stai cercando la riga completa che contiene il prodotto acquistato più recente per ciascuno address_ide il risultato ordinato per purchased_atallora stai cercando di risolvere un grave problema N per gruppo che può essere risolto con i seguenti approcci:

La soluzione generale che dovrebbe funzionare nella maggior parte dei 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 soluzione più orientata a PostgreSQL basata sulla risposta di @ 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

Il problema è stato chiarito, esteso e risolto qui: selezione delle righe ordinate da una colonna e distinte su un'altra


40
Funziona, ma dà un ordine errato. Ecco perché voglio sbarazzarmi di address_id nella clausola order
sl_bug

1
La documentazione è chiara: non puoi perché la riga selezionata sarà imprevedibile
Mosty Mostacho

3
Ma potrebbe esserci un altro modo per selezionare gli ultimi acquisti per indirizzi distici?
sl_bug

1
Se avete bisogno di ordine da purchases.purchased_at, è possibile aggiungere purchased_at alle condizioni distinte: SELECT DISTINCT ON (purchases.purchased_at, address_id). Tuttavia, due record con lo stesso address_id ma diversi valori acquistati_at comporteranno duplicati nel set restituito. Assicurati di essere a conoscenza dei dati che stai interrogando.
Brendan Benson,

23
Lo spirito della domanda è chiaro. Non c'è bisogno di scegliere la semantica. È triste che la risposta accettata e più votata non ti aiuti a risolvere il problema.
nicooga,

55

Puoi ordinare in base a address_id in una sottoquery, quindi ordinare in base a ciò che desideri in una query esterna.

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

3
Ma questo sarà più lento di una sola query, no?
sl_bug

2
Molto marginalmente sì. Anche se da quando hai un acquisto. * Nel tuo originale select, non penso che questo sia un codice di produzione?
hkf

8
Aggiungo che per le versioni più recenti di Postgres è necessario alias la subquery. Ad esempio: SELEZIONA * DA (SELEZIONA DISTINCT SU (indirizzo_id) acquisti.indirizzo_id, acquisti. * DA "acquisti" DOVE "acquisti". "Product_id" = 1 ORDINA PER indirizzo_ID DESC) COME tmp ORDINA tmp.purchased_at DESC
aembke

Ciò ritornerebbe address_iddue volte (senza necessità). Molti client hanno problemi con nomi di colonne duplicati. ORDER BY address_id DESCè inutile e fuorviante. Non fa nulla di utile in questa query. Il risultato è una scelta arbitraria da ogni set di righe con lo stesso address_id, non dalla riga con l'ultima purchased_at. L'ambigua domanda non lo ha chiesto esplicitamente, ma è quasi certamente l'intenzione del PO. In breve: non utilizzare questa query . Ho pubblicato alternative con spiegazione.
Erwin Brandstetter,

Ha funzionato per me. Bella risposta.
Matt West,

46

Una sottoquery può risolverlo:

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

Le espressioni principali ORDER BYdevono essere in accordo con le colonne DISTINCT ON, quindi non è possibile ordinare in base a colonne diverse nella stessa SELECT.

Utilizzare un ulteriore ORDER BYnella sottoquery solo se si desidera selezionare una riga specifica da ciascun set:

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_atpossibile NULL, considera DESC NULLS LAST. Assicurati di abbinare il tuo indice se intendi usarlo. Vedere:

Correlato, con ulteriori spiegazioni:


Non è possibile utilizzare DISTINCT ONsenza una corrispondenza ORDER BY. La prima query richiede un ORDER BY address_iddentro la sottoquery.
Aristotele Pagaltzis,

4
@AristotlePagaltzis: Ma puoi . Ovunque tu l'abbia ottenuto, non è corretto. È possibile utilizzare DISTINCT ONsenza ORDER BYnella stessa query. In questo caso si ottiene una riga arbitraria da ogni set di peer definiti dalla DISTINCT ONclausola. Provalo o segui i collegamenti sopra per dettagli e collegamenti al manuale. ORDER BYnella stessa query (la stessa SELECT) non riesco proprio a non essere d'accordo DISTINCT ON. Ho spiegato anche quello.
Erwin Brandstetter,

Eh, hai ragione. Sono stato cieco alle implicazioni della ORDER BYnota "imprevedibile a meno che non sia usata" nei documenti perché non ha senso per me che la funzionalità è implementata per essere in grado di gestire insiemi di valori non consecutivi ... eppure non ti permetterà di sfruttalo con un ordine esplicito. Fastidioso.
Aristotele Pagaltzis,

@AristotlePagaltzis: Questo perché Postgres utilizza internamente uno (almeno) di due algoritmi distinti: o attraversa un elenco ordinato o lavora con valori di hash - qualunque prometta di essere più veloce. Nel caso successivo il risultato non è ordinato in base alle DISTINCT ONespressioni (ancora).
Erwin Brandstetter,

2
Grazie. Le tue risposte sono sempre cristalline e utili!
Andrey Deineko l'

10

La funzione finestra può risolverlo in un solo passaggio:

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)

7
Sarebbe bello se qualcuno spiegasse la domanda.
Gajus,

@Gajus: breve spiegazione: non funziona, restituisce solo distinti address_id. Il principio potrebbe funzionare, però. Esempi correlati: stackoverflow.com/a/22064571/939860 o stackoverflow.com/a/11533808/939860 . Ma ci sono domande più brevi e / o più veloci per il problema in questione.
Erwin Brandstetter,

5

Per chiunque usi Flask-SQLAlchemy, questo ha funzionato per me

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))

2
Sì, o ancora più semplice, sono stato in grado di utilizzare:query.distinct(foo).from_self().order(bar)
Laurent Meyer il

@LaurentMeyer intendi Purchases.query?
Reubano,

Sì, intendevo acquisti.query
Laurent Meyer

-2

Puoi farlo anche usando la clausola 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

Questo non è corretto (a meno che non purchasesabbia solo le due colonne address_ide purchased_at). Per questo motivo GROUP BY, dovrai utilizzare una funzione aggregata per ottenere il valore di ogni colonna non utilizzata per il raggruppamento, quindi i loro valori verranno tutti da diverse file del gruppo a meno che non passi attraverso una ginnastica brutta e inefficiente. Questo può essere risolto solo usando le funzioni della finestra piuttosto che GROUP BY.
Aristotele Pagaltzis,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.