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