【问题标题】:Query ActiveRecord for records and relation calculations at once一次查询 ActiveRecord 的记录和关系计算
【发布时间】:2014-10-26 20:28:49
【问题描述】:

TL;博士?请参阅编辑 2

我有一个小的 Rails 应用程序,其中包含人们可以玩的几种不同类型的游戏:它基于体育运动,因此他们可以每周挑选每场比赛的获胜者(模型 PickEm,属性 correct boolean nil 表示未完成的比赛),并预测特定球队比赛的结果(模型Guess,属性score 为整数,nil 表示未完成的比赛)。每个Userhas_manyPickEmsGuesses。而且我正在尝试显示排名(correct/total - 总数都不是nilscore/total 可能)。

我发现我可以收集用户及其相关记录,但在尝试显示排名时,我发现每个用户都在触发另一个查询 - 随着用户群的增加,速度缓慢且不可持续。那是因为@user.pick_em_scorepick_ems.where(correct: true).size@user.guess_Scoreguesses.where.not(score: nil).sum(:score)。所以我打电话给user.pick_em_score,它会运行那个查询。我觉得应该有一种方法可以一次获取每个 User 以及这些特定计数,而不是缓冲一大堆不必要的额外内容。

我需要什么:

  • User记录
  • User.pick_em_score(计算correct记录)
  • User.pick_emsNOT NULL
  • User.guesses_score(由guesses.sum(:score)计算)
  • User.guessesNOT NULL

我在 Rails 的 ActiveRecord 助手中找到的大部分内容,尤其是与计算相关的内容,都是用于检索计算的。看起来我可能需要直接深入研究select() 等。但我无法让它工作。有人能指出我正确的方向吗?

编辑

澄清一下:我知道我可以将此信息写入User 模型,但这过于严格:下个赛季,我需要在User 中添加一个新列以供 那一年的结果等等。此外,这是第三级回调更新相关模型——Match 模型在保存时已经更新了相关的PickEmsGuesses我正在寻找能够处理此信息的最简单的 ActiveRecord 查询或查询,如标题所示。理想情况下,一个查询可以返回上述信息,但如果需要几个,没关系。

我以前使用 PHP 直接在 MySQL 中工作,但这些技能已经生锈了(我想,在原始 MySQL 中,我会有几个子选择语句来帮助提取这些计数),我也喜欢 能够使用 Rails 的 ActiveRecord 助手等,并尽可能避免构建原始 SQL。

第二次编辑:

我似乎将其归结为 开始 工作的一次调用,但我正在编写大量 SQL。 它也很脆弱,IMO,并试图运行它失败了。看起来我只是将 Rails 中的百万单数 SELECT 查询直接推送到 SQL 中,但这可能仍然是一个进步。

User.unscoped.select('users.*',
  '(SELECT COUNT(*) FROM pick_ems WHERE pick_ems.user_id = users.id AND pick_ems.correct) AS correct_pick_ems',
  '(SELECT COUNT(*) FROM pick_ems WHERE pick_ems.user_id = users.id AND pick_ems.correct IS NOT NULL) AS total_pick_ems',
  '(SELECT SUM(guesses.score) FROM guesses WHERE guesses.user_id = users.id AND guesses.score IS NOT NULL) AS guesses_score',
  '(SELECT COUNT(*) FROM guesses WHERE guesses.user_id = users.id AND guesses.score IS NOT NULL) AS guesses_count' )

问题似乎是:有没有办法使用 Rails 而不是原始 SQL 将我们看到的 users.id 与这些子查询联系起来?或者只是……一般来说是一种更好的构建方式?

此外,我正在为WHERE 运行另一组SELECTs,这将取决于total_pick_emsguesses_count> 0,但由于我不能使用那些别名列,我必须再拨打SELECT 一次。

【问题讨论】:

  • 您可能会使用数据库视图进行调查?我从未真正尝试过使用它们,但它们可以用模型在导轨中表示,就好像它们是一个实际的桌子一样。我会考虑制作一个基本上是您的 select 语句的数据库视图,然后将一个与用户相关联......尽管它们可能是 postgres 的东西......

标签: mysql ruby-on-rails ruby activerecord ruby-on-rails-4


【解决方案1】:

欢迎来到 AR。它真的只适用于简单的 CRUD 之类的查询。一旦您真的想愤怒地查询您的数据,它就没有能力执行您想要的查询,而无需求助于批发 SQL 字符串,因此通常会放弃链接的能力。

这正是我转向 Sequel 的原因,因为它确实具有使用更完整的 SQL 功能集组合查询的功能,包括连接条件、窗口函数、递归公用表表达式和高级预加载。与 AR 和 Arel 相比,作者反应迅速,文档也非常出色。

我不希望您会喜欢这个答案,但总有一天,您会开始将目光投向带有导轨的自以为是的组件之外,我不得不说这些组件几乎不是同类产品中最好的。 Sequel 还使我的应用程序速度比我使用 AR 能够获得的速度快很多倍,这不仅是开发人员的快乐,还意味着可以运行的服务器更少。是的,这将是一个学习曲线,但 IMO 最好学习有你支持的工具。

【讨论】:

  • 没听说过续集。我得去看看,谢谢。在这里不能解决我的问题,但至少很有趣。
【解决方案2】:

连接可能会起作用。如下所示

User.unscoped.joins(:guesses).joins(:pick_ems).
where("guesses.score IS NOT NULL").
select("users.*, 
sum(guesses.score) as guesses_score,
count(guesses.id) as guesses_count,
count(case when pick_ems.correct = True then 1 else null end) 
as correct_pick_ems,
count(case when pick_ems.correct != null then 1 else null end)
as total_pick_ems,
").
group("users.id")

如果您一次需要为有限数量的用户提供此信息,请使用类方法进行查询或预先加载 (User.includes(:guesses, :pick_ems)),例如

def correct_pick_ems
   pick_ems.count(&:correct)
end

会起作用的。

但是,如果您大部分时间都需要所有用户的这些信息,那么用户表中的缓存计数器会更理想。

【讨论】:

  • 快到了!这就是我一直在寻找的,但由于有两个连接,它似乎正在成倍增加。我已经为guesses_count(使用DISTINCT)解决了这个问题,但没有解决其他问题。这就是我所拥有的:User.unscoped.joins(:guesses, :pick_ems).select('users.*, sum(guesses.score) as guesses_score, count(DISTINCT guesses.id) as guesses_count, sum(case when pick_ems.correct = true then 1 else 0 end) as correct_pick_ems, sum(case when pick_ems.correct IS NOT NULL then 1 else 0 end) as total_pick_ems').where.not(guesses: { score: nil }, pick_ems: { correct: nil }).group('users.id')
  • 在这里颁发赏金。这是最接近我正在寻找的东西。 FTR 我已经设置了所有用户方法,但是在尝试获取 每个 用户的信息时这是不切实际的(它为每个用户创建至少一个 SQL 调用)。
  • @BenSaufley 如果您使用急切加载,它将总共进行 3 次 sql 调用。一个获取所有用户Users,接下来的两个获取所有关联的guessespick_ems。见here
  • 它没有,因为它需要为每个用户查询guesses和pick_ems。用户可能已经预先查询了他们的关联记录,但计数函数不通过 Ruby,它们运行自己的 SQL 查询。
  • @BenSaufley 是正确的。 count 将进行另一个 SQL 调用,但可以通过使用 length 而不是 count 轻松更正
【解决方案3】:

您需要的是某种自定义(智能)counter_cache,仅在特定条件下计数(例如,正确为真)

您可以使用conditional after_saveafter_destroy 触发器来构建您自己的自定义counter_cache,如下所示:

class PickEm
  belongs_to :user

  after_save :increment_finished_counter_cache, if: Proc.new { |pick_em| pick_em.correct }
  after_destroy :decrement_finished_counter_cache, if: Proc.new { |pick_em| pick_em.correct }

  private

  def increment_finished_counter_cache
    self.user.update_column(:finished_games_counter, self.user.finished_games_counter + 1) #update_column should not trigger any validations or callbacks
  end

  def decrement_finished_counter_cache
    self.user.update_column(:finished_games_counter, self.user.finished_games_counter - 1) #update_column should not trigger any validations or callbacks
  end
end

注意事项:

  • 代码未经测试(仅展示想法)

  • 有些人说最好避免将自定义计数器命名为 rails 命名它们 (foo_counter_cache)

【讨论】:

  • 谢谢。我知道这种可能的解决方案,但我已经在游戏模块上做了类似的事情——当Match 更新时,它会更新PickEmGuess 并进行评分——我想避免保留这么多可以通过编程确定写入数据库的东西。另外,下个赛季,我必须创建finished_games_counter_2015 来保持新的分数。我正在寻找具有最少/最简单 SQL 查询的 ActiveRecord 解决方案。
  • 答案条件太多 :) 我认为您最好更新您的问题以获得更好的答案!
  • 我认为标题和问题非常明确地与 ActiveRecord 查询有关,但我已添加说明,谢谢。
【解决方案4】:

您应该对其进行基准测试,但我的直觉是,将所有这些数据添加到单个 SELECT 不会比将其分解为单独的 SELECTs 快得多(我实际上有过这样的情况后者更快)。通过分解它,您还可以坚持更多的 ActiveRecord 和更少的原始 SQL,例如:

user_ids_to_pick_em_score = User.joins(:pick_ems).where(pick_ems: {correct: true}).group(:user_id).count
user_ids_to_pick_ems_count = User.joins(:pick_ems).where.not(pick_ems: {correct: nil}).group(:user_id).count
user_ids_to_guesses_score = Hash[User.select("users.id, SUM(guesses.score) AS total_score").joins(:guesses).group(:user_id).map{|u| [u.id, u.total_score]}]
user_ids_to_guesses_count = User.joins(:guesses).where.not(guesses: {score: nil}).group(:user_id).count

编辑:要显示它们,您可以这样做:

<%- User.select(:id, :name).find_each do |u| -%>
  Name: <%= u.name %>
  Picks Correct: <%= user_ids_to_pick_em_score[u.id] %>/<%= user_ids_to_pick_ems_count[u.id] %>
  Total Score: <%= user_ids_to_guesses_score[u.id] %>/<%= user_ids_to_guesses_count[u.id] %>
<%- end -%>

【讨论】:

  • 我可以告诉你,Heroku(在免费层)试图为每个用户单独运行每个查询时内存不足,而我当前的解决方案实际上运行得非常快。您的解决方案可能是最好的答案(更少的 SELECT,更少的原始 SQL/更多的 Ruby),但现在它们处于四个独立的关系中,没有以编程方式链接。当我遍历用户以显示排名时,我必须搜索黑白关系以匹配它们……对吗?
  • 上面的每一行都会给你一个用户 ID 的哈希值到所需的值(pick_em 分数、计数等)。如果您希望它们全部在一个查询中,恐怕在 AR 中没有办法做到这一点。我很惊讶这些解决方案会导致任何内存问题。除了第三个之外,所有的都没有实例化任何 AR 对象,这往往是 Rails 中的内存猪。我在包含超过 200,000 条记录的数据库上运行类似的查询,并在大约 50 毫秒内获得结果。
  • 这些查询没有造成内存问题,我的也没有,上面。最初的解决方案(使用 pick_ems 或猜测获取用户,遍历它们并显示信息)会在每次用户显示(以获取用户的分数)时导致一个新的查询,并且显然填满了 Heroku 的内存。
  • 啊,是的,这更有意义。如果您仍然看到这些性能问题,您可以尝试将它们缓存为模型方法,这样您就不必在每次页面加载时计算它们。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2012-01-14
  • 1970-01-01
  • 1970-01-01
  • 2023-02-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多