2010-08-18 16 views
11

Próbuje pobrać tablicę obiektów ActiveRecord pogrupowanych według daty w PostgreSQL.Elegancka grupa PostgreSQL dla Ruby on Rails/ActiveRecord

Dokładniej próbuję tłumaczyć następujące kwerendy MySQL:

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

Jestem świadomy, że PostgreSQL interpretacja standardu SQL jest bardziej restrykcyjne niż MySQL i że w konsekwencji tego typu zapytania nie będą działać. ..i przeczytałem pewną liczbę postów na temat StackOverflow i innych tematów - ale żadna z nich nie wydaje się być ostateczną odpowiedzią na ten temat

Próbowałem różnych kombinacji zapytań z klauzulami grupowymi i odrębnymi bez dużej ilości radość - i na razie mam raczej nieelegancki hack, który choć działa sprawia, że ​​się rumieniam pl Patrzę na to.

Jaki jest właściwy sposób wykonania takiego zapytania z Railsami i PostgreSQL? (Ignorując fakt, że z pewnością powinno to zostać usunięte na poziomie ActiveRecord)

+0

Określenie "array ... pogrupowane według daty" - to nie ma sensu. Co próbujesz osiągnąć? Czy możesz po prostu zamówić według daty (daty)? – DanSingerman

+1

Każda baza danych, z wyjątkiem MySQL, odrzuci nielegalny SQL. Bazy danych nie zgadują, jakie wyniki chcesz dzisiaj, db powinny uzyskać tylko poprawne wyniki we wszystkich sytuacjach. Użyj ONLY_FULL_GROUP_BY w MySQL, a powyższe zapytanie również zostanie odrzucone przez MySQL. –

+0

Witaj Dan - Próbuję uzyskać tablicę obiektów Post, ale chcę pobrać tylko jeden post dla danego dnia (najnowszy post dla tego dnia). – digitalfrost

Odpowiedz

13

Funkcja PostgreSQL, której chcesz użyć, to DISTINCT ON. Istnieją dwa podstawowe sposoby wykonywania tego zapytania za pośrednictwem ActiveRecord.

Pierwsza metoda polega tylko na podaniu opcji :select i :order. Działa to świetnie, gdy masz dość prostą kwerendę bez :joins lub :include.

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

Jeśli masz bardziej złożone kwerendy gdzie ActiveRecord generuje własny SELECT klauzulę można użyć podzapytania, aby wybrać rekordy docelowych.

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

Należy pamiętać, że może to być sprawiedliwe nieco wolniej niż w pierwszym sposobie, w zależności od Twoich danych. W razie potrzeby użyłbym tej metody. Pamiętaj o porównaniu z danymi produkcyjnymi.

1

Moje rozwiązanie:

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

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

prosty i powtarzalny.

0

Chociaż SQL jest dość prosty, jeśli chodzi o udzielanie odpowiedzi na pytania typu "kiedy ostatni post był na każdy dzień?" NIE jest to bardzo proste, gdy pytasz "który był najnowszym postem na każdy dzień?"

Nie można pobrać najnowszego wpisu na każdy dzień bez użycia podrzędnego SELECT (lub wielu instrukcji SQL). To może pracować dla Ciebie (wykorzystanie Post.find_by_sql lub podobny):

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 

Powyższe oświadczenie SQL powinien być wystarczająco jeśli można mieć pewność, że dwa posty nie będą miały taką samą wartość w kolumnie created_at. Jeśli nie możesz zagwarantować unikalności w kolumnie utworzonej w kolumnie, musisz albo odfiltrować duplikaty w Ruby (które nie powinny być zbyt nieefektywne, ponieważ prawdopodobnie i tak będziesz pętlą nad listą) lub będziesz musiał zrobić N +1 instrukcje SQL. (Właściwie można wykonywać wybory w wierszach, ale AFAIK, które są tak samo nieefektywne jak instrukcje SQL N + 1.)

Oto w jaki sposób można usunąć duplikaty podczas zapętlenie:

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

Powiedział, że można ją napisać ładnie tylko z Ruby/ActiveRecord, jeśli masz wystarczająco dużo kilka dni, że SELECT dla każdego dnia ISN” t tak źle:

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

Jeśli używasz paginacji (powiedzmy 10 pozycji na stronie), to będzie wymagało 11 SQL dla każdej strony. Nie pomysły, ale prostota może być warta nieskuteczności.

Szczerze mówiąc, jeśli spodziewasz się, że zapytanie będzie zarówno uruchamiane często, jak i z dość dużym zbiorem danych, proponuję dodać kolumnę boolowską o nazwie most_recent. Ostatni post z ostatnich dni nie zmieni się. Musisz tylko martwić się o posty z dzisiaj. Wystarczy skonfigurować zadanie cron, aby uruchomić kilka minut po zakończeniu dnia, aby zaktualizować wartość ostatniego dnia. Jeśli chcesz czegoś bardziej aktualnego, możesz uruchomić zadanie cron co 5 minut. Lub jeśli potrzebujesz w czasie rzeczywistym, a następnie dodaj callback after_save, aby ustawić most_recent na false dla wszystkich dzisiejszych postów, które nie są bieżącym postem.

To pytanie jest podobne: MySQL: Getting highest score for a user