Элегантная группа PostgreSQL для Ruby on Rails / ActiveRecord - PullRequest
11 голосов
/ 18 августа 2010

Попытка получить массив объектов ActiveRecord, сгруппированных по дате, с PostgreSQL.

Более конкретно, я пытаюсь перевести следующий запрос MySQL:

@posts = Post.all(:group => "date(date)", 
   :conditions => ["location_id = ? and published = ?", @location.id, true], 
   :order => "created_at DESC")

Мне известно, что PostgreSQLИнтерпретация стандарта SQL является более строгой, чем MySQL, и, следовательно, этот тип запроса не будет работать ... и прочитал ряд сообщений в StackOverflow и в других местах по этой теме - но ни один из них, кажется, не является окончательным ответом на этот вопросsubject

Я пробовал различные комбинации запросов с группировкой и различными предложениями без особой радости - и на данный момент у меня довольно неуклюжий хак, который, хотя работает, заставляет меня краснеть, когда я на него смотрю.

Как правильно сделать такой запрос с помощью Rails и PostgreSQL?(Не обращая внимания на тот факт, что это обязательно должно быть удалено на уровне ActiveRecord)

Ответы [ 3 ]

14 голосов
/ 08 января 2011

Функция PostgreSQL, которую вы хотите использовать здесь: DISTINCT ON.Есть два основных способа сделать этот запрос через ActiveRecord.

Первый способ - просто указать опции :select и :order.Это прекрасно работает, когда у вас есть довольно простой запрос без :joins или :include.

Post.all(
  :select => 'DISTINCT ON (date::date) *',
  :order => 'date::date DESC, created_at DESC'
)

Если у вас более сложный запрос, где ActiveRecord генерирует свое собственное предложение SELECT, вы можете использоватьподзапрос для выбора целевых записей.

Post.all(
  :joins => 'INNER JOIN (SELECT DISTINCT ON (date::date) id FROM posts ORDER BY date::date DESC, created_at DESC) x ON x.id = posts.id'
)

Обратите внимание, что это может быть немного медленнее, чем первый метод, в зависимости от ваших данных.Я бы использовал этот метод только в случае необходимости.Обязательно сравните с данными, подобными производственным.

1 голос
/ 31 марта 2011

Мое решение:

def self.columns_list
   column_names.collect { |c| "#{table_name}.#{c}" }.join(",")
 end

 scope :selling, joins(:products).group(columns_list)

Простой и повторяемый.

0 голосов
/ 12 апреля 2011

Хотя SQL довольно прост, когда речь идет об ответах на вопросы типа «когда была самая последняя публикация за каждый день?» это не очень прямолинейно, когда вы спрашиваете «какая была самая последняя запись за каждый день?»

Вы не можете получить последнее сообщение за каждый день, не используя вложенный SELECT (или несколько операторов SQL). Это может работать для вас (используйте Post.find_by_sql или подобное):

SELECT P.*, M.just_day, M.max_created_at
FROM posts P
JOIN (
  SELECT date(P2.date) AS just_day, MAX(P2.created_at) AS max_created_at
  FROM posts P2
  P.location_id='12345' AND P.published=true
  GROUP BY date(P2.date)
) AS M  
   ON AND M.max_created_at = P.created_at
WHERE P.location_id='12345' AND P.published=true

Приведенного выше оператора SQL должно быть достаточно , если , вы можете быть уверены, что два столбца не будут иметь одинаковое значение в столбце create_at. Если вы не можете гарантировать уникальность в созданном столбце at, то вам нужно либо отфильтровать дубликаты в Ruby (это не должно быть слишком неэффективно, потому что, по-видимому, вы все равно будете перебирать список), либо вам нужно будет выполнить N +1 SQL операторов. (На самом деле вы можете делать выборки для каждой строки, но AFAIK так же неэффективен, как N + 1 SQL-операторы.)

Вот как вы можете удалить дубликаты во время цикла:

last_post = nil
posts.each do |post|
  unless post.just_day == last_past.try(:just_day)
    # Do stuff
    last_post = post
  end
end

Тем не менее, вы могли бы написать это просто с помощью Ruby / ActiveRecord, если у вас есть достаточно дней, чтобы SELECT для каждого дня не был слишком плохим:

days = Post.group("date(date)")
posts = days.each { |day| Post.order('created DESC').where("date(day) = ?", day) }

Если вы используете нумерацию страниц (скажем, 10 элементов на страницу), то для каждой страницы потребуется 11 операторов SQL. Не идеи, а простота может стоить неэффективности.

Честно говоря, если вы ожидаете, что этот запрос будет выполняться часто и с достаточно большим набором данных, тогда я предлагаю вам добавить логический столбец с именем most_recent. Последнее сообщение из прошлых дней не изменится. Вам нужно только беспокоиться о сообщениях с сегодняшнего дня. Просто настройте задачу cron для запуска через несколько минут после окончания дня, чтобы обновить значение за последний день. Если вы хотите что-то более современное, вы можете запускать задание cron каждые 5 минут. Или, если вам нужно в режиме реального времени, добавьте обратный вызов after_save, чтобы установить для Most_recent значение false для всех сегодняшних сообщений, которые не являются текущими.

Этот вопрос похож на: MySQL: получение наибольшего количества очков для пользователя

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...