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

Я хочу запустить этот запрос:

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

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

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

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

Ответ 1

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

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

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

Ответ 2

Вы можете заказать по адресу 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

A подзапрос может решить эту проблему:

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.
Связанный, с большим количеством объяснений:

Ответ 4

Функция окна может решить, что за один проход:

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)

Ответ 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))

Ответ 6

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

Ответ 7

Вы также можете сделать это, используя предложение 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