如何在Rails中选择聚合子查询的平均值



我有以下返回预期结果的数据和SQL查询。我现在正试图在Rails中实现与Groupdate(和ActiveMedian)相同的事情。

<表类> user_id 点 created_at tbody> <<tr> 1 5 2020-10-01 1 15 2020-11-01 1 20 2020-11-02 2 33 2020-11-01

如果我们放下Rails的一个层,并使用支持活动记录的关系代数(Arel),这是可能的。

在这种方法中,我们将自己教Arel关于date_trunc函数*,然后为内部求和构建一个嵌套的聚合查询,不会立即执行,而是合并到外部平均聚合中:
class UserPoints
  def self.averages
    period = Arel::Nodes::NamedFunction.new('date_trunc', [
      Arel::Nodes::Quoted.new('month'),
      arel_table[:created_at]
    ])
    points = arel_table[:points].sum
    
    # The per-user aggregate sum subquery, as an abstract relational structure
    subquery = select(points.as("points"), period.as("period")).group(:user_id, :period)
    # Execute
    from(subquery, quoted_table_name).group(:period).average(:points)
  end
end

这种方法是通用的。它适用于具有范围关系的组合;例如,如果您想写UserPoints.where(created_at: Time.current.all_year).averages,那么在最后一行插入适当的unscope,变成:

from(subquery, quoted_table_name).unscope(:where).group(:period).average(:points)
类似地,要与Groupdate库结合使用,至少对于外部查询*,请尝试:
from(subquery, quoted_table_name).group_by_month(:period).average(:points)

甚至可能有机会将其重构为scope声明,通过省略最终的聚合表达式,从而获得使用其他表达式的灵活性。

现在警告:Arel是Rails内部API,这意味着如果你想要文档,你需要阅读它的源代码,即使在小版本中也可能有破坏性的变化。这实际上是非常罕见的,如果你的代码戴上了适当的安全装置,使用Arel是可以的(很多人都这样做),这当然是一个合适的测试用例。


*我没有在内部聚合查询中使用Groupdate gem,因为它缺乏命名结果列的方法。

解决方案#2,这不是一个解决方案

这个额外的答案也使用了Arel和Groupdate gem,但它实际上比我推荐的方法更危险。我把它作为一个单独的答案,因为a)它有效,b)它看起来所以很优雅:

class UserPoints < ApplicationRecord
  def self.averages_by_month
    # average of points = total points / # of distinct users
    points_avg = arel_table[:points].sum / arel_table[:user_id].count(true)
    # execute, grouped by month
    group_by_month(:created_at).calculate(:itself, points_avg)
  end
end

,这给出了正确的结果!至少在编写Rails 6时是这样的。

不幸的是,骗局正在上演;这种方法依赖于更多关于活动记录内部的知识,而不是简单地使用Arel API。

讨论,或者为什么这是不好的

#calculate method's documented parameters为:

relation.calculate(operation, column_name)

,尽管有意添加了对在聚合计算中使用Arel表达式的支持,但它没有在公共API中记录。单独来看,这可能没有那么糟糕,但是这个方法依赖于一个实现细节:Rails在调用列参数的Arel表示时,通过使用operation参数作为方法名来构造完整的聚合表达式,从而返回聚合表达式。通过传递:itself,我劫持了框架层之间的内部通信,并导致points_avg在通过Kernel#itself的内部来回期间返回自己。

这是一个元编程技巧,就像所有的特技一样,演示它很有趣,但不应该成为任何人的产品代码的一部分,至少除非有一天#calculate方法被文档化以接受一个简单的Arel表达式,因为我们依赖于非常深入的Rails内部知识,也就是说,这是一个维护的禁止。

还有一些更相关的假设隐藏在那里,除此之外,关于分组聚合表达式求值的核心中的其他元素,例如期望列混叠只处理它所给出的任何内容。这也是可行的,所以有些人可能会说这也证明了Rails是多么健壮和通用,但它绝对是在测试合理假设的边界。

推荐

总的来说,尽管这个解决方案看起来简洁而优雅,但它隐藏了比我准备推荐的用于生产的更多的魔法。相反,我把它作为一种有趣的新奇事物来呈现和解释。

谁知道呢,也许有一天这甚至会得到明确的支持。

相关内容

  • 没有找到相关文章

最新更新