Rails — отдельное включение после соединения

Я использую Rails 4.2 с PostgreSQL. У меня есть модель Product и модель Purchase с Product has many Purchases. Я хочу найти отдельные недавно купленные товары. Сначала я пробовал:

Product.joins(:purchases)
.select("DISTINCT products.*, purchases.updated_at") #postgresql requires order column in select
.order("purchases.updated_at DESC")

Однако это приводит к дублированию, поскольку он пытается найти все кортежи, где пара (product.id и purchases.updated_at) имеет уникальное значение. Однако я просто хочу выбрать продукты с разными id после соединения. Если идентификатор продукта появляется в соединении несколько раз, выберите только первый. Итак, я также пробовал:

Product.joins(:purchases)
.select("DISTINCT ON (product.id) purchases.updated_at, products.*")
.order("product.id, purchases.updated_at") #postgres requires that DISTINCT ON must match the leftmost order by clause

Это не работает, потому что мне нужно указать product.id в предложении order из-за ограничение, которое выводит неожиданный порядок.

Каков путь рельсов для достижения этого?


person aandis    schedule 25.09.2015    source источник


Ответы (5)


Используйте подзапрос и добавьте другое предложение ORDER BY во внешний SELECT:

SELECT *
FROM  (
   SELECT DISTINCT ON (pr.id)
          pu.updated_at, pr.*
   FROM   Product pr
   JOIN   Purchases pu ON pu.product_id = pr.id  -- guessing
   ORDER  BY pr.id, pu.updated_at DESC NULLS LAST
   ) sub
ORDER  BY updated_at DESC NULLS LAST;

Подробности для DISTINCT ON:

Или какой-либо другой метод запроса:

Но если все, что вам нужно от Purchases, это updated_at, вы можете получить это дешевле с помощью простого агрегата в подзапросе, прежде чем присоединяться:

SELECT *
FROM   Product pr
JOIN  (
   SELECT product_id, max(updated_at) AS updated_at
   FROM   Purchases 
   GROUP  BY 1
   ) pu ON pu.product_id = pr.id  -- guessing
ORDER  BY pu.updated_at DESC NULLS LAST;

О NULLS LAST:

Или еще проще, но не так быстро при получении всех строк:

SELECT pr.*, max(updated_at) AS updated_at
FROM   Product pr
JOIN   Purchases pu ON pu.product_id = pr.id
GROUP  BY pr.id  -- must be primary key
ORDER  BY 2 DESC NULLS LAST;

Product.id необходимо определить как первичный ключ, чтобы это работало. Подробности:

Если вы выбираете только небольшой выбор (например, с предложением WHERE, ограничивающим только один или несколько pr.id), это будет быстрее.

person Erwin Brandstetter    schedule 25.09.2015
comment
Есть ли rails способ сделать это? - person aandis; 25.09.2015
comment
@zack: Конечно, есть. Должно быть легко перевести, но я не эксперт по Rails. В конечном итоге код должен быть переведен в SQL перед отправкой на сервер БД. - person Erwin Brandstetter; 25.09.2015
comment
@zack, ну, тебе, вероятно, придется использовать строки SQL. Приведенный выше SQL использует функции, которые, как мне кажется, не поддерживает Arel. Поэтому вам придется использовать такие вещи, как .select('DISTINCT ON (... и .order('whatever DESC NULLS FIRST'). Или, возможно, вы могли бы взломать Arel для поддержки этих модификаторов, но это заняло бы довольно много времени. Arel находится под капотом ActiveRecord в Rails. - person D-side; 25.09.2015
comment
@D-сторона, я понял, как это сделать. Пожалуйста, проверьте мой ответ. - person aandis; 25.09.2015
comment
@zack: я добавил еще одну альтернативу. - person Erwin Brandstetter; 29.09.2015

Итак, основываясь на ответе @ErwinBrandstetter, я наконец нашел правильный способ сделать это. Запрос для поиска отдельных недавних покупок:

SELECT *
FROM  (
   SELECT DISTINCT ON (pr.id)
          pu.updated_at, pr.*
   FROM   Product pr
   JOIN   Purchases pu ON pu.product_id = pr.id
   ) sub
ORDER  BY updated_at DESC NULLS LAST;

order_by внутри подзапроса не нужен, так как мы все равно упорядочиваем во внешнем запросе.

Рельсовый способ сделать это -

inner_query = Product.joins(:purchases)
  .select("DISTINCT ON (products.id) products.*, purchases.updated_at as date") #This selects all the unique purchased products.

result = Product.from("(#{inner_query.to_sql}) as unique_purchases")
  .select("unique_purchases.*").order("unique_purchases.date DESC")

Второй (и лучший) способ сделать это, предложенный @ErwinBrandstetter, -

SELECT *
FROM   Product pr
JOIN  (
   SELECT product_id, max(updated_at) AS updated_at
   FROM   Purchases 
   GROUP  BY 1
   ) pu ON pu.product_id = pr.id
ORDER  BY pu.updated_at DESC NULLS LAST;

который может быть записан в рельсах как

join_query = Purchase.select("product_id, max(updated_at) as date")
  .group(1) #This selects most recent date for all purchased products

result = Product.joins("INNER JOIN (#{join_query.to_sql}) as unique_purchases ON products.id = unique_purchases.product_id")
  .order("unique_purchases.date")
person aandis    schedule 25.09.2015
comment
Что касается 1-го запроса: The order_by isn't needed inside the subquery. Хотя обычно это работает, это всего лишь деталь реализации, которая может сломаться в любой момент. Вам нужно ORDER BY в подзапросе, чтобы гарантировать последнюю строку. В противном случае Postgres может возвращать любую строку для каждого pr.id. - person Erwin Brandstetter; 29.09.2015

Чтобы основываться на ответе erwin-brandstetter, вот как вы можете сделать это с ActiveRecord (должно быть близко к наименее):

Product
  .select('*')
  .joins('INNER JOIN (SELECT product_id, max(updated_at) AS updated_at FROM Purchases GROUP  BY 1) pu ON pu.product_id = pr.id')
  .order('pu.updated_at DESC NULLS LAST')
person Tom Fast    schedule 25.09.2015
comment
должно быть .joins('INNER JOIN (SELECT product_id ... ) проверить мой ответ. - person aandis; 25.09.2015
comment
Спасибо Зак. Я обновил свой ответ. Это сработало, как вы задумали? - person Tom Fast; 26.09.2015

Попробуйте сделать это:

Product.joins(:purchases)
.select("DISTINCT ON (products_id) purchases.product_id, purchases.updated_at, products.*")
.order("product_id, purchases.updated_at") #postgres requires that DISTINCT ON must match the leftmost order by clause
person akbarbin    schedule 25.09.2015
comment
Это приведет к дубликату. Идентификатор продукта может появляться в таблице покупок несколько раз с разными значениями updated_at. Я хочу выбрать продукт только один раз, - person aandis; 25.09.2015
comment
Чем группировка по products.id отличается от purchases.product_id? - person aandis; 25.09.2015
comment
Извини за это. Оба равны. Я снова обновил на основе вашего кода. - person akbarbin; 25.09.2015
comment
Это то же самое, что и в моем вопросе. - person aandis; 25.09.2015
comment
Product.joins(:purchases) .select(DISTINCT ON (products.id) Purchases.product_id, Purchases.updated_at, products.*) .order(product_id, Purchases.updated_at) - person Kiry Meas; 14.01.2021

Я закончил с этим -

Product.joins(:purchases)
.select("DISTINCT ON (products.id) products.*, purchases.updated_at as date")
.sort_by(&:date)
.reverse

Все еще ищете лучший способ сделать это.

person aandis    schedule 25.09.2015
comment
Половина работы здесь выполняется Ruby, что далеко не идеально. - person D-side; 25.09.2015
comment
@ D-сторона другого ответа. stackoverflow.com/questions/ 32775220/ - person aandis; 25.09.2015
comment
Что бы ни. Я сказал, что вам придется использовать строки SQL, и вы действительно это делаете =) - person D-side; 25.09.2015
comment
@D-side Ну, я не вижу лучшего способа, так как они не поддерживаются рельсами. - person aandis; 25.09.2015