PostgreSQL DISTINCT ON z innym ORDER BY

216

Chcę uruchomić to zapytanie:

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

Ale pojawia się ten błąd:

PG :: Błąd: BŁĄD: WYBIERZ ODRÓŻNIENIE wyrażeń musi pasować do wyrażeń ORDER BY

Dodanie address_idjako pierwszego ORDER BYwyrażenia wycisza błąd, ale tak naprawdę nie chcę dodawać sortowania address_id. Czy można to zrobić bez zamówienia przez address_id?

sl_bug
źródło
Twoja klauzula zamówienia została zakupiona, a nie adres_id. Czy możesz wyjaśnić swoje pytanie.
Teja,
moje zamówienie ma zakup, ponieważ go chcę, ale postgres prosi również o adres (patrz komunikat o błędzie).
sl_bug
Osobiście uważam, że wymaganie DISTINCT ON w celu dopasowania ORDER BY jest bardzo wątpliwe, ponieważ istnieje wiele uzasadnionych przypadków użycia, aby je rozróżnić. Jest post na postgresql.uservoice próbujący to zmienić dla tych, którzy czują się podobnie. postgresql.uservoice.com/forums/21853-general/suggestions/…
średnik 16'19
dostałem dokładnie ten sam problem i ten sam problem. W tej chwili podzieliłem go na pod-zapytanie, a następnie zamawiałem, ale jest brudny.
Guy Park

Odpowiedzi:

208

Dokumentacja mówi:

DISTINCT ON (wyrażenie [, ...]) utrzymuje tylko pierwszy wiersz każdego zestawu wierszy, w którym dane wyrażenia są równe. [...] Należy pamiętać, że „pierwszy wiersz” każdego zestawu jest nieprzewidywalny, chyba że zostanie zastosowane polecenie ORDER BY, aby zapewnić, że żądany wiersz pojawi się jako pierwszy. [...] Wyrażenie DISTINCT ON musi pasować do wyrażenia ORDER BY najbardziej na lewo.

Oficjalna dokumentacja

Musisz dodać address_iddo zamówienia.

Alternatywnie, jeśli szukasz pełnego wiersza zawierającego najnowszy zakupiony produkt dla każdego z address_idtych wyników i posortowanego według purchased_attego, próbujesz rozwiązać największy problem N na grupę, który można rozwiązać za pomocą następujących metod:

Ogólne rozwiązanie, które powinno działać w większości 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

Bardziej zorientowane na PostgreSQL rozwiązanie oparte na odpowiedzi @ 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 wyjaśniony, rozszerzony i rozwiązany tutaj: wybieranie wierszy uporządkowanych według jednej kolumny i odrębnych w innej

Mosty Mostacho
źródło
40
Działa, ale daje nieprawidłowe zamówienie. Właśnie dlatego chcę pozbyć się adres_id w zamówieniu
sl_bug
1
Dokumentacja jest jasna: nie możesz, ponieważ wybrany wiersz będzie nieprzewidywalny
Mosty Mostacho
3
Ale czy może istnieć inny sposób wyboru najnowszych zakupów dla różnych adresów?
sl_bug
1
Jeśli trzeba, aby przez purchases.purchased_at można dodać purchased_at do odmiennych warunkach: SELECT DISTINCT ON (purchases.purchased_at, address_id). Jednak dwa rekordy o tym samym adresie_id, ale różnych wartościach wartość_zakupu, spowodują duplikaty w zwróconym zestawie. Upewnij się, że znasz dane, o które pytasz.
Brendan Benson
23
Duch pytania jest jasny. Nie trzeba wybierać semantyki. To smutne, że zaakceptowana i najczęściej głosowana odpowiedź nie pomaga rozwiązać problemu.
nicooga,
55

Możesz zamówić według adresu_id w podzapytaniu, a następnie uporządkować według tego, co chcesz w zapytaniu zewnętrznym.

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
źródło
3
Ale to będzie wolniejsze niż tylko jedno zapytanie, nie?
sl_bug
2
Bardzo marginalnie tak. Chociaż skoro masz zakupy. * W oryginale select, nie sądzę, że jest to kod produkcyjny?
hkf
8
Dodałbym, że w przypadku nowszych wersji postgresu należy użyć aliasu podzapytania. Na przykład: WYBIERZ * OD (WYBIERZ ODLEGŁOŚĆ NA (adres_id). Zakupy. Adres_id, zakupy. * OD „zakupów” GDZIE „zakupy”. „ID_produktu” = 1 ZAMÓWIENIE NA ADRES adresu DESC) JAKO tmp ZAMÓWIENIE według tmp.purchased_at DESC
aembke
Wróciłby address_iddwukrotnie (bez potrzeby). Wielu klientów ma problemy ze zduplikowanymi nazwami kolumn. ORDER BY address_id DESCjest bezcelowe i mylące. Nie robi nic przydatnego w tym zapytaniu. Wynikiem jest dowolny wybór z każdego zestawu wierszy z tym samym address_id, a nie wiersz z najnowszym purchased_at. Niejednoznaczne pytanie nie wymagało tego wprost, ale prawie na pewno intencja PO. W skrócie: nie używaj tego zapytania . Zamieściłem alternatywy z wyjaśnieniem.
Erwin Brandstetter,
Pracował dla mnie. Świetna odpowiedź.
Matt West
46

Podzapytanie może go rozwiązać:

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

Wyrażenia wiodące ORDER BYmuszą zgadzać się z kolumnami w DISTINCT ON, więc nie można uporządkować według różnych kolumn w tym samym SELECT.

Użyj dodatkowego ORDER BYw podzapytaniu tylko wtedy, gdy chcesz wybrać konkretny wiersz z każdego zestawu:

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;

Jeśli purchased_atmożesz NULL, zastanów się DESC NULLS LAST. Pamiętaj jednak, aby dopasować swój indeks, jeśli zamierzasz go używać. Widzieć:

Powiązane, z dodatkowymi wyjaśnieniami:

Erwin Brandstetter
źródło
Nie możesz używać DISTINCT ONbez dopasowania ORDER BY. Pierwsze zapytanie wymaga ORDER BY address_idwewnętrznego podzapytania.
Aristotle Pagaltzis
4
@AristotlePagaltzis: Ale możliwe . Gdziekolwiek to masz, jest to nieprawidłowe. Możesz użyć DISTINCT ONbez ORDER BYw tym samym zapytaniu. Otrzymujesz dowolny wiersz z każdego zestawu elementów równorzędnych zdefiniowanych DISTINCT ONw tym przypadku w klauzuli. Wypróbuj lub skorzystaj z linków powyżej, aby uzyskać szczegółowe informacje i linki do instrukcji. ORDER BYw tym samym zapytaniu (tym samym SELECT) po prostu nie może się nie zgodzić DISTINCT ON. Też to wyjaśniłem.
Erwin Brandstetter,
Masz rację. Byłem ślepy na implikacje uwagi „nieprzewidywalna, jeśli nie ORDER BYjest używana” w dokumentach, ponieważ nie ma dla mnie sensu, aby ta funkcja została zaimplementowana w celu radzenia sobie z niesekwencyjnymi zestawami wartości… a jednak nie pozwoli ci wykorzystaj to z wyraźnym uporządkowaniem. Denerwujący.
Arystoteles Pagaltzis
@AristotlePagaltzis: To dlatego, że wewnętrznie Postgres korzysta z jednego (co najmniej) dwóch różnych algorytmów: albo przegląda posortowaną listę, albo pracuje z wartościami skrótu - w zależności od tego, co zapowiada się szybciej. W późniejszym przypadku wynik nie jest sortowany według DISTINCT ONwyrażeń (jeszcze).
Erwin Brandstetter,
2
Dziękuję Ci. Twoje odpowiedzi są zawsze krystalicznie czyste i pomocne!
Andrey Deineko,
10

Funkcja okna może rozwiązać to w jednym przebiegu:

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
źródło
7
Byłoby miło, gdyby ktoś wyjaśnił zapytanie.
Gajus,
@Gajus: Krótkie wyjaśnienie: nie działa, zwraca tylko wyraźne address_id. Zasada może jednak zadziałać. Powiązane przykłady: stackoverflow.com/a/22064571/939860 lub stackoverflow.com/a/11533808/939860 . Istnieją jednak krótsze i / lub szybsze zapytania dotyczące danego problemu.
Erwin Brandstetter,
5

Dla każdego używającego Flask-SQLAlchemy, to zadziałało dla mnie

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
źródło
2
Tak, a nawet łatwiej, mogłem użyć:query.distinct(foo).from_self().order(bar)
Laurent Meyer
@LaurentMeyer masz na myśli Purchases.query?
reubano
Tak, miałem na myśli Purchases.query
Laurent Meyer
-2

Możesz to również zrobić za pomocą klauzuli 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
źródło
Jest to niepoprawne (chyba że purchasesma tylko dwie kolumny address_idi purchased_at). Z tego powodu GROUP BYmusisz użyć funkcji agregującej, aby uzyskać wartość każdej kolumny nieużywanej do grupowania, więc wszystkie wartości będą pochodzić z różnych wierszy grupy, chyba że przejdziesz przez brzydką i nieefektywną gimnastykę. Można to naprawić tylko przy użyciu funkcji okna, a nie GROUP BY.
Aristotle Pagaltzis