【发布时间】:2015-09-01 00:52:35
【问题描述】:
我的 Rails 4 应用有一个User 模型、一个Link 模型和一个Hit 模型。每个User 有很多Links,每个Link 有很多Hits。有时,我想显示User 的Links 的列表以及它拥有的Hits 的数量。
执行此操作的明显方法是遍历链接并在每个链接上调用link.hits.count,但这会产生 N+1 个查询。所以相反,我写了一个连接hits 表的范围:
scope :with_hit_counts, -> {
joins("LEFT OUTER JOIN hits ON hits.link_id = links.id").select('links.*', 'count(hits.link_id) AS hit_count').group("links.id")
}
这有效地为每个Link 添加了一个虚拟hit_count 属性,该属性在单个查询中计算。奇怪的是,它似乎是与加载链接分开的查询,而不是实际在同一个查询中完成:
SELECT COUNT(*) AS count_all, links.id AS links_id
FROM "links" LEFT OUTER JOIN hits ON hits.link_id = links.id
WHERE "links"."user_id" = $1
GROUP BY links.id
ORDER BY "links"."domain_id" ASC, "links"."custom_slug" ASC, "links"."id" ASC ;
不幸的是,随着hits 表的增长,这已成为一个缓慢的查询。 EXPLAIN 表示查询正在使用索引将 所有 匹配链接与其匹配的链接连接起来,然后通过顺序扫描将链接缩小到具有正确 user_id 的链接;这似乎是它很慢的原因。但是,如果我们已经单独加载了链接列表——而且我们是——根本不需要加入链接表。我们可以获取用户的链接 ID 列表,然后使用hits.link_id IN (list of IDs) 纯粹对命中表进行查询。
将其编写为单独的查询很容易,而且运行速度极快:
Hit.where(link_id: @user.links.ids).group(:link_id).count
问题是,我不知道如何让 ActiveRecord 作为 Link 模型上的范围来执行此操作,以便每个 Link 都有一个我可以使用的 hit_count 属性,所以我可以使用生成的返回值作为关系,并能够将其他查询链接到它上面。有什么想法吗?
(我知道 ActiveRecord 的 counter_cache 功能,但我不想在这里使用它——hits 由单独的非 Ruby 系统插入,修改该系统以更新计数器缓存将是中度疼痛。)
【问题讨论】:
-
您是否考虑过使用数据库视图并为其添加 hit_count 属性?在数据库级别上的处理速度比在 activerecord 级别上快得多。
标签: ruby-on-rails join scope rails-activerecord