Rails:如何构建每天/每月/每年的统计信息或如何缺少与数据库无关的SQL函数(例如:STRFTIME,DATE_FORMAT,DATE_TRUNC)

lak*_*kim 19 sql sqlite group-by ruby-on-rails count

我一直在网上搜索,我不知道.

  • 假设您必须在Rails应用程序的管理区域中构建一个仪表板,并且您希望每天拥有订阅数量.
  • 假设您使用SQLite3进行开发,使用MySQL进行生产(相当标准的设置)

基本上,有两种选择:

1)使用以下方法在Rails应用程序中使用和按天聚合从数据库中检索所有行:Subscriber.allEnumerable.group_by

@subscribers = Subscriber.all
@subscriptions_per_day = @subscribers.group_by { |s| s.created_at.beginning_of_day }
Run Code Online (Sandbox Code Playgroud)

我认为这是一个非常糟糕的主意.对于小型应用程序,从数据库中检索所有行是可以接受的,但它根本不会扩展.数据库聚合和日期功能拯救!

2)使用聚合和日期函数在数据库中运行SQL查询:

Subscriber.select('STRFTIME("%Y-%m-%d", created_at) AS day, COUNT(*) AS subscriptions').group('day')
Run Code Online (Sandbox Code Playgroud)

哪个将在此SQL查询中运行:

SELECT STRFTIME("%Y-%m-%d", created_at) AS day, COUNT(*) AS subscriptions
FROM subscribers
GROUP BY day
Run Code Online (Sandbox Code Playgroud)

好多了.现在聚合在数据库中完成,该数据库针对此类任务进行了优化,每天只有一行从数据库返回到Rails应用程序.

...但等等......现在应用程序必须在我使用MySQL的生产环境中运行!替换STRFTIME()DATE_FORMAT().如果明天我切换到PostgreSQL怎么办?替换DATE_FORMAT()DATE_TRUNC().

我喜欢用SQLite开发.简单易行.我也喜欢Rails与数据库无关的想法.但是为什么Rails没有提供一种方法来翻译完全相同的SQL函数,但是在每个RDBMS中都有不同的语法(这种差异真的很愚蠢,但是嘿,抱怨它为时已晚)?

我无法相信我在网上找到Rails应用程序的这一基本功能的答案很少:计算每天,每月或每年的订阅量.

告诉我我错过了什么:)

编辑

我发布这个问题已经有几年了.经验表明,我应该使用相同的数据库开发和生产.所以我现在认为数据库不可知的要求无关紧要.

开发/生产平价 FTW.

lak*_*kim 7

我最终写了自己的宝石.查看它并随时贡献:https: //github.com/lakim/sql_funk

它允许您拨打电话,如:

Subscriber.count_by("created_at", :group_by => "day")
Run Code Online (Sandbox Code Playgroud)


ada*_*mar 5

不幸的是,您提到了 Rails 完全忽略的一些非常困难的问题。ActiveRecord::Calculations 文档就像您所需要的那样编​​写,但数据库可以做更高级的事情。正如 Donal Fellows 在他的评论中提到的,这个问题比看起来要棘手得多。

在过去的两年里,我开发了一个大量使用聚合的 Rails 应用程序,我尝试了几种不同的方法来解决这个问题。不幸的是,我没有忽略夏令时之类的东西,因为统计数据“只是趋势”。我生成的计算由我的客户按照准确的规格进行测试。

稍微扩展一下这个问题,我认为您会发现您当前按日期分组的解决方案是不够的。使用 STRFTIME 似乎是一个自然的选择。主要问题是它不允许您按任意时间段进行分组。如果您想按年、月、日、小时和/或分钟进行聚合,STRFTIME 可以正常工作。如果没有,您会发现自己正在寻找另一种解决方案。另一个巨大的问题是聚合后聚合。比如说,你想按月分组,但你想从每个月的 15 号开始做。你会如何使用 STRFTIME 做到这一点?您必须按天分组,然后按月分组,但随后有人会考虑每个月第 15 天的起始偏移量。最后一根稻草是按 STRFTIME 分组需要按字符串值分组,您

我遇到的性能最好、设计最好的解决方案是基于整数时间段的解决方案。这是我的一个 mysql 查询的摘录:

SELECT
  field1, field2, field3,
  CEIL((UNIX_TIMESTAMP(CONVERT_TZ(date, '+0:00', @@session.time_zone)) + :begin_offset) / :time_interval) AS time_period
FROM
  some_table
GROUP BY 
  time_period
Run Code Online (Sandbox Code Playgroud)

在这种情况下,:time_interval 是分组周期中的秒数(例如,每天 86400),而 :begin_offset 是偏移周期开始的秒数。CONVERT_TZ() 业务说明了 mysql 解释日期的方式。Mysql 总是假设日期字段在 mysql 本地时区。但是因为我以 UTC 存储时间,所以如果我希望 UNIX_TIMESTAMP() 函数给我一个正确的响应,我必须将它从 UTC 转换为会话时区。时间段最终是一个整数,它描述了自 unix 时间开始以来的时间间隔数。此解决方案更加灵活,因为它允许您按任意时间段进行分组,并且不需要在聚合时进行聚合。

现在,进入我的真正观点。对于强大的解决方案,我建议您考虑根本不使用 Rails 来生成这些查询。最大的问题是不同数据库的聚合的性能特征和细微差别是不同的。您可能会发现一种设计在您的开发环境中运行良好,但在生产环境中却无法运行,反之亦然。您将跳过许多困难,以使 Rails 在查询构造中与这两个数据库完美配合。

相反,我建议您在您选择的数据库中生成特定于数据库的视图,并将它们带到正确的环境中。尝试像对任何其他 ActiveRecord 表(id​​ 和所有表)一样对视图建模,当然还要使视图中的字段跨数据库相同。因为这些统计信息是只读查询,所以您可以使用模型来支持它们并假装它们是成熟的表。如果有人试图保存、创建、更新或销毁,只需引发异常。

通过以 Rails 方式进行操作,您不仅会获得简化的模型管理,还会发现您可以以纯 SQL 中做梦都想不到的方式为聚合功能编写单元测试。如果您决定切换数据库,您将不得不重写这些视图,但是您的测试会告诉您哪里错了,并使生活变得更加轻松。


pba*_*ann 0

如果您追求的是数据库不可知论,我可以想到几个选择:

为存储格式化日期或时间戳的订阅服务器创建一个新字段(我们将其称为 day_str)并使用 ActiveRecord.count:

daily_subscriber_counts = Subscriber.count(:group => "day_str")
Run Code Online (Sandbox Code Playgroud)

当然,代价是记录大小稍大一些,但这几乎可以消除性能担忧。

您还可以根据可视化数据的粒度,只需多次调用 .count 并根据需要设置日期...

((Date.today - 7)..Date.today).each |d|
    daily_subscriber_counts[d] = Subscriber.count(:conditions => ["created_at >= ? AND created_at < ?", d.to_time, (d+1).to_time)
end
Run Code Online (Sandbox Code Playgroud)

这也可以根据不同的粒度(每月、每年、每天、每小时)进行定制。如果您想按天对所有订阅者进行分组(也没有机会运行它),那么这不是最有效的解决方案,但我想您会想按月、日、小时进行分组如果您分别查看一年、几个月或几天的数据。

如果你愿意致力于 mysql 和 sqlite 你可以使用......

daily_subscriber_counts = Subscriber.count(:group => "date(created_at)")
Run Code Online (Sandbox Code Playgroud)

...因为它们共享相似的 date() 函数。