2010-08-18 15 views
11

Provare a recuperare una matrice di oggetti ActiveRecord raggruppati per data con PostgreSQL.Elegante gruppo PostgreSQL per Ruby on Rails/ActiveRecord

In particolare sto cercando di tradurre la seguente query MySQL:

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

Sono consapevole che PostgreSQL interpretazione dello standard SQL è più rigoroso di MySQL e che di conseguenza questo tipo di query non funzionerà. ..e ho letto un numero di post su StackOverflow e altrove sull'argomento - ma nessuno di essi sembra essere la risposta definitiva su questo argomento

Ho provato varie combinazioni di query con clausole group by e distinte senza molto gioia - e per il momento ho un trucco piuttosto inelegante che sebbene i lavori mi faccia arrossire it Lo guardo.

Qual è il modo corretto di eseguire una query di questo tipo con Rails e PostgreSQL? (Ignorando il fatto che sicuramente questo dovrebbe essere estratto a livello ActiveRecord)

+0

un "allineamento ... raggruppati per data" - questo non ha senso. Cosa stai cercando di ottenere? Puoi semplicemente ordinare per data (data)? – DanSingerman

+1

Qualsiasi database, ad eccezione di MySQL, rifiuterà l'SQL illegale. I database non indovinano quali risultati vorresti oggi, db's dovrebbe ottenere tutti i risultati corretti in tutte le situazioni. Usa ONLY_FULL_GROUP_BY in MySQL e anche la query sopra verrà respinta da MySQL. –

+0

Ciao Dan - Sto cercando di ottenere una serie di oggetti Post ma voglio solo recuperare un Post per un dato giorno (l'ultimo Post per quel giorno). – digitalfrost

risposta

13

La funzione PostgreSQL che si desidera utilizzare qui è DISTINCT ON. Ci sono due modi fondamentali per fare questa query tramite ActiveRecord.

Il primo metodo è solo specificare le opzioni :select e :order. Funziona alla grande quando hai una query abbastanza semplice senza :joins o :include.

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

Se si dispone di una query più complessa in cui ActiveRecord genera il proprio SELECT clausola, è possibile utilizzare una sottoquery per selezionare i record di destinazione.

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

Si noti che questo potrebbe essere un po 'più lento del primo metodo a seconda dei dati. Vorrei usare questo metodo solo se necessario. Assicurati di fare un benchmark con dati simili alla produzione.

1

La mia soluzione:

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

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

semplice e ripetibile.

0

Mentre SQL è abbastanza semplice quando si tratta di rispondere a domande come "quando era il post più recente per ogni giorno?" NON è molto semplice quando chiedi "quale era il post più recente per ogni giorno?"

Non è possibile recuperare l'ultimo post per ogni giorno senza utilizzare un sub SELECT (o più istruzioni SQL). Questo potrebbe funzionare per voi (uso Post.find_by_sql o simile):

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 

L'istruzione SQL di cui sopra dovrebbe essere abbastanza se si può essere certi che due posti non avranno lo stesso valore nella colonna created_at. Se non è possibile garantire univocità nella colonna creata in, allora è necessario filtrare i duplicati in Ruby (che non dovrebbe essere troppo inefficiente perché presumibilmente si andrà a scorrere sull'elenco in ogni caso) o sarà necessario fare N +1 istruzioni SQL. (In realtà si potrebbe fare selezioni per riga, ma AFAIK è altrettanto inefficiente delle istruzioni SQL N + 1.)

Ecco come si potrebbe rimuovere i duplicati mentre loop:

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

Detto questo, si potrebbe scrivere bene con solo Rubino/ActiveRecord, se avete qualche giorno a sufficienza che un prescelto per ogni isn giorno' t troppo male:

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

Se si utilizza l'impaginazione (dicono 10 articoli per pagina), allora questo richiederà 11 istruzioni SQL per ogni pagina. Non idee, ma la semplicità potrebbe valere l'inefficienza.

Onestamente, se si prevede che questa query sia eseguita frequentemente e con un set di dati ragionevolmente grande, suggerisco di aggiungere una colonna booleana chiamata most_recent. L'ultimo post dei giorni passati non cambierà. Devi solo preoccuparti dei post di oggi. Basta impostare un processo cron per eseguire alcuni minuti dopo la fine della giornata per aggiornare il valore per l'ultimo giorno. Se si desidera qualcosa di più aggiornato, è possibile che il processo cron venga eseguito ogni 5 minuti. Oppure se hai bisogno di tempo reale, quindi aggiungi un callback after_save per impostare most_recent su false per tutti i post di oggi che non sono il post corrente.

Questa domanda è simile: MySQL: Getting highest score for a user