PostgreSQL DISTINCT ON mit verschiedenen ORDER BY

216

Ich möchte diese Abfrage ausführen:

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

Aber ich bekomme diesen Fehler:

PG :: Fehler: FEHLER: SELECT DISTINCT ON-Ausdrücke müssen mit den anfänglichen ORDER BY-Ausdrücken übereinstimmen

Durch Hinzufügen address_idals ersten ORDER BYAusdruck wird der Fehler stummgeschaltet, aber ich möchte wirklich keine Sortierung hinzufügen address_id. Kann man ohne Bestellung auskommen address_id?

sl_bug
quelle
Ihre Bestellklausel hat_at nicht address_id gekauft. Können Sie Ihre Frage klarstellen?
Teja
Meine Bestellung wurde gekauft, weil ich es möchte, aber Postgres fragt auch nach der Adresse (siehe Fehlermeldung).
sl_bug
Persönlich halte ich es für sehr fraglich, dass DISTINCT ON mit ORDER BY übereinstimmen muss, da es eine Vielzahl legitimer Anwendungsfälle gibt, in denen sie sich unterscheiden. Es gibt einen Beitrag auf postgresql.uservoice, der versucht, dies für diejenigen zu ändern, die sich ähnlich fühlen. postgresql.uservoice.com/forums/21853-general/suggestions/…
Semikolon
bekam genau das gleiche Problem und stand vor der gleichen Einschränkung. Im Moment habe ich es in eine Unterabfrage aufgeteilt und dann bestellt, aber es fühlt sich schmutzig an.
Guy Park

Antworten:

207

Die Dokumentation sagt:

DISTINCT ON (Ausdruck [, ...]) behält nur die erste Zeile jedes Satzes von Zeilen bei, in denen die angegebenen Ausdrücke gleich sind. [...] Beachten Sie, dass die "erste Zeile" jedes Satzes nicht vorhersehbar ist, es sei denn, ORDER BY wird verwendet, um sicherzustellen, dass die gewünschte Zeile zuerst angezeigt wird. [...] Die Ausdrücke DISTINCT ON müssen mit den Ausdrücken ganz links ORDER BY übereinstimmen.

Offizielle Dokumentation

Sie müssen also address_iddie Bestellung bis hinzufügen .

Wenn Sie alternativ nach der vollständigen Zeile suchen, die für jedes Produkt das zuletzt gekaufte Produkt enthält address_idund deren Ergebnis nach sortiert purchased_atist, versuchen Sie alternativ, ein Problem mit dem größten N pro Gruppe zu lösen, das mit den folgenden Ansätzen gelöst werden kann:

Die allgemeine Lösung, die in den meisten DBMS funktionieren sollte:

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

Eine PostgreSQL-orientierte Lösung basierend auf der Antwort von @ 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

Problem hier geklärt, erweitert und gelöst: Auswahl von Zeilen, die nach einer Spalte geordnet und in einer anderen getrennt sind

Mosty Mostacho
quelle
40
Es funktioniert, gibt aber falsche Reihenfolge. Deshalb möchte ich address_id in der order-Klausel loswerden
sl_bug
1
Die Dokumentation ist klar: Sie können nicht, weil die ausgewählte Zeile unvorhersehbar sein wird
Mosty Mostacho
3
Aber kann es eine andere Möglichkeit geben, die neuesten Einkäufe für bestimmte Adressen auszuwählen?
sl_bug
1
Wenn Sie nach purchase.purchased_at bestellen müssen, können Sie purchase_at zu Ihren DISTINCT-Bedingungen hinzufügen : SELECT DISTINCT ON (purchases.purchased_at, address_id). Zwei Datensätze mit derselben Adresse_ID, aber unterschiedlichen Werten für gekaufte_at führen jedoch zu Duplikaten im zurückgegebenen Satz. Stellen Sie sicher, dass Sie die Daten kennen, die Sie abfragen.
Brendan Benson
23
Der Geist der Frage ist klar. Sie müssen sich nicht mit Semantik befassen. Es ist traurig, dass die akzeptierte und am meisten gewählte Antwort Ihnen nicht hilft, das Problem zu lösen.
Nicooga
55

Sie können nach address_id in einer Unterabfrage und dann nach Ihren Wünschen in einer äußeren Abfrage bestellen.

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
quelle
3
Aber das ist langsamer als nur eine Abfrage, oder?
sl_bug
2
Sehr marginal ja. Obwohl, da Sie einen Kauf getätigt haben. * In Ihrem Original select, glaube ich nicht, dass dies Produktionscode ist?
HKF
8
Ich würde hinzufügen, dass Sie für neuere Versionen von Postgres die Unterabfrage aliasen müssen. Zum Beispiel: SELECT * FROM (SELECT DISTINCT ON (Adress-ID) Käufe.Adresse_ID, Käufe. * FROM "Käufe" WO "Käufe". "Produkt_ID" = 1 BESTELLUNG NACH Adress_ID DESC) AS tmp ORDER BY tmp.purchased_at DESC
aembke
Dies würde address_idzweimal (ohne Notwendigkeit) zurückkehren. Viele Clients haben Probleme mit doppelten Spaltennamen. ORDER BY address_id DESCist sinnlos und irreführend. In dieser Abfrage ist nichts nützlich. Das Ergebnis ist eine willkürliche Auswahl aus jedem Satz von Zeilen mit derselben address_id, nicht aus der Zeile mit der neuesten purchased_at. Die zweideutige Frage hat dies nicht explizit gefordert, aber das ist mit ziemlicher Sicherheit die Absicht des OP. Kurz gesagt: Verwenden Sie diese Abfrage nicht . Ich habe Alternativen mit Erklärung gepostet.
Erwin Brandstetter
Hat für mich gearbeitet. Gute Antwort.
Matt West
46

Eine Unterabfrage kann es lösen:

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

Führende Ausdrücke in ORDER BYmüssen mit Spalten in übereinstimmen DISTINCT ON, damit Sie nicht nach verschiedenen Spalten in derselben sortieren können SELECT.

Verwenden Sie eine zusätzliche ORDER BYZeile in der Unterabfrage nur, wenn Sie aus jedem Satz eine bestimmte Zeile auswählen möchten:

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;

Wenn purchased_atmöglich NULL, überlegen Sie DESC NULLS LAST. Stellen Sie jedoch sicher, dass Ihr Index übereinstimmt, wenn Sie ihn verwenden möchten. Sehen:

Verwandte, mit mehr Erklärung:

Erwin Brandstetter
quelle
Sie können nicht DISTINCT ONohne Matching verwenden ORDER BY. Die erste Abfrage erfordert eine ORDER BY address_idinterne Abfrage .
Aristoteles Pagaltzis
4
@ AristotelesPagaltzis: Aber du kannst . Woher du das hast, ist es falsch. Sie können DISTINCT ONohne ORDER BYin der gleichen Abfrage verwenden. Sie erhalten eine willkürliche Zeile von jeder Gruppe von Peers, die DISTINCT ONin diesem Fall durch die Klausel definiert sind . Probieren Sie es aus oder folgen Sie den obigen Links für Details und Links zum Handbuch. ORDER BYin der gleichen Abfrage (die gleiche SELECT) kann einfach nicht widersprechen DISTINCT ON. Das habe ich auch erklärt.
Erwin Brandstetter
Huh, du hast recht. Ich war blind für die Implikation des ORDER BYHinweises „Unvorhersehbar, wenn nicht verwendet wird“ in den Dokumenten, da es für mich keinen Sinn macht, dass die Funktion implementiert ist, um mit nicht aufeinanderfolgenden Wertesätzen umgehen zu können Nutzen Sie das mit einer expliziten Bestellung. Nervig.
Aristoteles Pagaltzis
@AristotlePagaltzis: Das liegt daran, dass Postgres intern einen von (mindestens) zwei unterschiedlichen Algorithmen verwendet: entweder eine sortierte Liste durchlaufen oder mit Hash-Werten arbeiten - je nachdem, was schneller sein soll. Im späteren Fall wird das Ergebnis DISTINCT ON(noch) nicht nach Ausdrücken sortiert .
Erwin Brandstetter
2
Danke dir. Ihre Antworten sind immer kristallklar und hilfreich!
Andrey Deineko
10

Die Fensterfunktion kann das in einem Durchgang lösen:

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
quelle
7
Es wäre schön, wenn jemand die Frage erklären würde.
Gajus
@ Gajus: Kurze Erklärung: Es funktioniert nicht, gibt nur deutlich zurück address_id. Das Prinzip könnte jedoch funktionieren. Verwandte Beispiele: stackoverflow.com/a/22064571/939860 oder stackoverflow.com/a/11533808/939860 . Es gibt jedoch kürzere und / oder schnellere Abfragen für das vorliegende Problem.
Erwin Brandstetter
5

Für alle, die Flask-SQLAlchemy verwenden, hat dies bei mir funktioniert

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
quelle
2
Ja, oder noch einfacher, ich konnte verwenden:query.distinct(foo).from_self().order(bar)
Laurent Meyer
@LaurentMeyer meinst du Purchases.query?
Reubano
Ja, ich meinte Purchases.query
Laurent Meyer
-2

Sie können dies auch tun, indem Sie die group by-Klausel verwenden

   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
quelle
Dies ist falsch (es sei denn, purchasesnur die beiden Spalten address_idund purchased_at). Aus diesem Grund GROUP BYmüssen Sie eine Aggregatfunktion verwenden, um den Wert jeder Spalte zu ermitteln, die nicht für die Gruppierung verwendet wird. Daher stammen alle Werte aus verschiedenen Zeilen der Gruppe, es sei denn, Sie führen hässliche und ineffiziente Gymnastik durch. Dies kann nur mithilfe von Fensterfunktionen und nicht mithilfe von Fensterfunktionen behoben werden GROUP BY.
Aristoteles Pagaltzis