PostgreSQL DISTINCT ON с разными ORDER BY

216

Я хочу выполнить этот запрос:

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

Но я получаю эту ошибку:

PG :: Ошибка: ОШИБКА: выражения SELECT DISTINCT ON должны соответствовать начальным выражениям ORDER BY

Добавление в address_idкачестве первого ORDER BYвыражения заглушает ошибку, но я действительно не хочу добавлять сортировку address_id. Можно ли обойтись без заказа по address_id?

sl_bug
источник
Ваше положение о заказе куплено_ не по адресу_идентификатора. Можете ли вы прояснить свой вопрос.
Теджа
В моем заказе есть покупка, потому что я хочу его, но postgres также запрашивает адрес (см. сообщение об ошибке).
sl_bug
3
Полностью ответили здесь - stackoverflow.com/questions/9796078/… Благодаря stackoverflow.com/users/268273/mosty-mostacho
sl_bug
Лично я считаю, что требование DISTINCT ON для соответствия ORDER BY очень сомнительно, поскольку существует множество законных вариантов использования для их различия. На postgresql.uservoice есть пост, в котором это можно изменить для тех, кто чувствует себя так же. postgresql.uservoice.com/forums/21853-general/suggestions/…
точка с запятой
получил точно такую ​​же проблему, и столкнулся с тем же ограничением. В данный момент я разбил его на подзапрос, а затем упорядочил, но он кажется грязным.
Парень Парк

Ответы:

208

Документация гласит:

DISTINCT ON (выражение [, ...]) сохраняет только первую строку каждого набора строк, где заданные выражения оцениваются как равные. [...] Обратите внимание, что «первая строка» каждого набора непредсказуема, если только ORDER BY не используется, чтобы гарантировать, что желаемая строка появляется первой. [...] Выражение (я) DISTINCT ON должно соответствовать крайнему левому выражению ORDER BY.

Официальная документация

Так что вам придется добавить address_idв заказ по.

В качестве альтернативы, если вы ищете полную строку, содержащую самый последний приобретенный продукт для каждого address_idи отсортированный результат, purchased_atто вы пытаетесь решить проблему наибольшего N на группу, которая может быть решена с помощью следующих подходов:

Общее решение, которое должно работать в большинстве СУБД:

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

Более ориентированное на PostgreSQL решение, основанное на ответе @ 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

Здесь проблема прояснена, расширена и решена: выбор строк, упорядоченных по одному столбцу и отличающихся по другому

Мосты Мостачо
источник
40
Это работает, но дает неправильный порядок. Вот почему я хочу избавиться от address_id в предложении заказа
sl_bug
1
Документация ясна: Вы не можете, потому что выбранная строка будет непредсказуемой
Мосты Мостачо
3
Но может быть есть другой способ выбрать последние покупки для разных адресов?
sl_bug
1
Если вам необходимо заказать по purchases.purchased_at, вы можете добавить purchased_at ваших DISTINCT условий: SELECT DISTINCT ON (purchases.purchased_at, address_id). Однако две записи с одним и тем же address_id, но разными значениями приобрела в результате будут получены дубликаты в возвращенном наборе. Убедитесь, что вы осведомлены о данных, которые запрашиваете.
Брендан Бенсон
23
Суть вопроса ясна. Не нужно выбирать семантику. Печально, что принятый и получивший наибольшее количество голосов ответ не поможет вам решить проблему.
nicooga
55

Вы можете упорядочить по address_id в подзапросе, затем упорядочить по желанию во внешнем запросе.

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
Но это будет медленнее, чем один запрос, не так ли?
sl_bug
2
Очень незначительно да. Хотя, поскольку у вас есть покупки. * В вашем оригинале select, я не думаю, что это рабочий код?
hkf
8
Я бы добавил, что для более новых версий postgres вам нужно создать псевдоним подзапроса. Например: SELECT * FROM (.. SELECT DISTINCT ON (address_id) purchases.address_id, покупки * FROM "покупки" , где "покупок" "PRODUCT_ID" = 1 ORDER BY address_id DESC) КАК TMP ORDER BY DESC tmp.purchased_at
aembke
Это вернется address_idдважды (без необходимости). У многих клиентов возникают проблемы с повторяющимися именами столбцов. ORDER BY address_id DESCбессмысленно и вводит в заблуждение. Ничего полезного в этом запросе нет. Результатом является произвольный выбор из каждого набора строк с одинаковыми address_id, а не строки с самой последней purchased_at. Неоднозначный вопрос явно не задавался этим, но это почти наверняка намерение ОП. Короче говоря: не используйте этот запрос . Я разместил альтернативы с объяснением.
Эрвин Брандштеттер
Работал на меня. Отличный ответ.
Мэтт Уэст
46

Подзапрос может решить:

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

Начальные выражения в ORDER BYдолжны согласовываться со столбцами в DISTINCT ON, так что вы не можете упорядочить по разным столбцам в одном SELECT.

Используйте дополнительный ORDER BYв подзапросе, только если вы хотите выбрать конкретную строку из каждого набора:

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;

Если purchased_atможно NULL, посмотрим DESC NULLS LAST. Но убедитесь, что ваш индекс совпадает, если вы собираетесь его использовать. Видеть:

Связанные с более подробным объяснением:

Эрвин Брандштеттер
источник
Вы не можете использовать DISTINCT ONбез соответствия ORDER BY. Первый запрос требует ORDER BY address_idвнутри подзапроса.
Аристотель Пагальцис
4
@AristotlePagaltzis: Но вы можете . Откуда вы это взяли, это неправильно. Вы можете использовать DISTINCT ONбез ORDER BYв том же запросе. В этом случае вы получаете произвольную строку из каждого набора пиров, определенных в DISTINCT ONпредложении. Попробуйте или перейдите по ссылкам выше для получения подробной информации и ссылок на руководство. ORDER BYв том же запросе (тот же SELECT) просто не может не согласиться DISTINCT ON. Я тоже это объяснил.
Эрвин Брандштеттер
Да, ты прав. Я ORDER BYне видел смысла примечания «непредсказуемый, если не используется» в документах, потому что для меня не имеет смысла, что эта функция реализована так, чтобы иметь возможность обрабатывать непоследовательные наборы значений ... но не позволит вам использовать это с явным порядком. Раздражает.
Аристотель Пагальцис
@AristotlePagaltzis: Это потому, что внутри Postgres использует один из (как минимум) двух разных алгоритмов: либо перебирает отсортированный список, либо работает со значениями хеш-функций - в зависимости от того, что обещает быть быстрее. В последнем случае результат не сортируется по DISTINCT ONвыражениям (пока).
Эрвин Брандштеттер
2
Спасибо. Ваши ответы всегда кристально чисты и полезны!
Андрей Дейнеко
10

Оконная функция может решить это за один проход:

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
Было бы хорошо, если бы кто-то объяснил запрос.
Gajus
@Gajus: краткое объяснение: это не работает, только возвращает отчетливый address_id. Принцип может работать, хотя. Связанные примеры: stackoverflow.com/a/22064571/939860 или stackoverflow.com/a/11533808/939860 . Но есть более короткие и / или более быстрые запросы для решения проблемы.
Эрвин Брандштеттер
5

Для тех, кто использует Flask-SQLAlchemy, это работает для меня

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
источник
2
Да, или даже проще, я смог использовать:query.distinct(foo).from_self().order(bar)
Лоран Мейер
@ LaurentMeyer ты имеешь в виду Purchases.query?
Reubano
Да, я имел в виду Purchase.query
Лоран Мейер
-2

Вы также можете сделать это с помощью предложения 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
Вайшали
источник
Это неверно (если purchasesтолько нет двух столбцов address_idи purchased_at). Из-за GROUP BYэтого вам нужно будет использовать статистическую функцию, чтобы получить значение каждого столбца, не используемого для группировки, поэтому все значения будут поступать из разных строк группы, если вы не пройдете некрасивую и неэффективную гимнастику. Это можно исправить только с помощью оконных функций, а не GROUP BY.
Аристотель Пагальцис