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?


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


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

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:


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)

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

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

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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.