Rails SQL COUNT N + 1 неэффективность

У меня есть блог. На моей индексной странице я вытягиваю все сообщения в блоге. Для каждого сообщения в блоге я подсчитываю количество комментариев к этому сообщению. Это приводит к проблеме N + 1. Мои запросы выглядят следующим образом:

SELECT "blog_posts".* FROM "blog_posts" WHERE ("blog_posts"."published" = 't') ORDER BY published_at DESC
SELECT "users".* FROM "users" WHERE ("users"."id" IN (1, 2, 3)) 
SELECT COUNT(*) FROM "blog_comments" WHERE ("blog_comments".blog_post_id = 10)
SELECT COUNT(*) FROM "blog_comments" WHERE ("blog_comments".blog_post_id = 9)
SELECT COUNT(*) FROM "blog_comments" WHERE ("blog_comments".blog_post_id = 8)
SELECT COUNT(*) FROM "blog_comments" WHERE ("blog_comments".blog_post_id = 2)
SELECT COUNT(*) FROM "blog_comments" WHERE ("blog_comments".blog_post_id = 7) 

Есть ли способ в Rails включить COUNT таким же образом, что и пользователи (строка SQL 2)?

Ответ 1

Вы можете использовать счетный кеш: http://guides.rubyonrails.org/association_basics.html#counter_cache

"В этом объявлении Rails сохранит значение кеша в актуальном состоянии, а затем вернет это значение в ответ на метод размера."

class BlogPost < ActiveRecord::Base
  has_many :blog_comments
end

class BlogComment < ActiveRecord::Base
  belongs_to :blog_post, :counter_cache => true
end

Сообщение в блоге будет иметь столбец с именем blog_comments_count.

Ответ 2

В общем, вам нужен SQL-запрос вроде:

  SELECT COUNT(*), blog_post_id
    FROM blog_comments
GROUP BY blog_post_id;

Вы можете использовать это, чтобы создать хэш из blog_post_id в количество комментариев.

Ответ 3

Вы также можете увидеть что-то вроде этого:

BlogComment.group('blog_post_id').count

В чисто Rails Way.:)

Ответ 4

Это запрос ActiveRecord, который выполняет поиск в моей таблице "site_access_log", состоящей из веб-доступа к сайту.

Он выбирает поле "remote_addr" из первых 15 записей вместе с подсчетом для этого IP-адреса, отсортированным в порядке убывания по счету, за которым следует возрастающий порядок номеров IP-адресов, имеющих один и тот же номер счета.

Я использую Postgres, который понимает номера IPv4, поэтому я передал поле типу inet, чтобы разрешить правильную сортировку по значению, а не по значению ASCII. Если ваша база данных не поддерживает значения inet, вы всегда можете конвертировать из IP в inet с помощью Ruby Socket или библиотеки IPSocket, а затем сортировать полученные результаты.

@remote_addr_results = SiteAccessLog.all(
  :select     => 'remote_addr, count(remote_addr) as remote_addr_count',
  :group      => :remote_addr,
  :order      => 'remote_addr_count desc, cast(remote_addr as inet)',
  :limit      => 15
)
puts @remote_addr_results.map{ |r| r.remote_addr_count << ' : ' << r.remote_addr }

>> 985 : 68.228.61.183
>> 572 : 205.203.134.197
>> 500 : 68.32.220.153
>> 460 : 72.200.64.128
>> 281 : 24.121.196.194
>> 262 : 99.91.9.155
>> 241 : 68.99.237.178
>> 213 : 68.99.119.137
>> 208 : 70.167.157.162
>> 204 : 201.165.6.2
>> 164 : 72.201.233.147
>> 155 : 75.245.177.106
>> 150 : 97.123.246.154
>> 149 : 201.165.190.98
>> 145 : 74.37.165.220

Сгенерированный SQL выглядит так:

SELECT remote_addr, count(remote_addr) as remote_addr_count                                                                       
FROM "site_access_logs"                                                                                                           
GROUP BY remote_addr                                                                                                              
ORDER BY remote_addr_count desc, cast(remote_addr as inet)                                                                        
LIMIT 15 

Ответ 5

Вы можете использовать dase gem или один из методов, описанных в это видео.

Пример с Dase:

Author.includes_count_of(:articles).each do |author| puts "#{author.name} has #{author.articles_count} articles" end