【问题标题】:Rails group a table by :created_at, returning a count of the :status column, and then sub-group the :status column with count of each unique valueRails 通过 :created_at 对表进行分组,返回 :status 列的计数,然后使用每个唯一值的计数对 :status 列进行分组
【发布时间】:2015-12-10 15:33:18
【问题描述】:

我有一个模型通知,我想知道每天创建多少个通知以及每个状态有多少个。 status 是模型 Notification 的字符串键。

Notification.where(user: users).group('DATE(created_at)').count('created_at')

那给我:

{Mon, 05 Oct 2015=>2572,
 Tue, 06 Oct 2015=>555,
 Wed, 07 Oct 2015=>2124,
 Thu, 08 Oct 2015=>220,
 Fri, 09 Oct 2015=>36}

但我想要类似的东西:

{Mon, 05 Oct 2015=>{total=>2572, error=>500, pending=>12},
 Tue, 06 Oct 2015=>{total=>555, sent=>50, pending=>12}}

或类似的。活动记录可以吗?也许group_by 对我有好处。但它已被弃用。

目前我已经尝试过:

Notification.where(user: users).group('DATE(created_at), status').count('created_at') 
# => {"error"=>2, "sent"=>42}

这不是我想要的结果。

【问题讨论】:

    标签: sql ruby-on-rails ruby activerecord


    【解决方案1】:

    这对我来说很难测试可能的答案,因为日期有些独特并且查询有些复杂。但这里是如何在哈希中获取您的计数:

    my_hash = {} #create your empty hash
    notifications = Notification.where(user: users).select(:created_at).uniq
      #get a hash of Notification objects with unique created_at dates
    notifications.each do |n|
      my_hash[n.created_at] = Notification.where(created_at: n.created_at).select(:status).group(:status).count
      #load your hash with <created_at date> => {"error" => 20, "pending" => 30}
      my_hash[n.created_at]["total"] = Notification.where(created_at: n.created_at).count
      #add the total count to your hash
    end
    

    编辑

    正如 OP 发现的那样,随着查询的日期数量增加,这变得非常低效。那是因为它正在为第一个查询中返回的每个日期调用一个查询。我确信有一种方法可以将其作为一个长 SQL 查询并将其用作数据库中的视图。

    【讨论】:

    • 顺便说一句,我从没想过.select(:status).group(:status).count会做它做的事情。大声笑,当我尝试时,我永远无法让 ActiveRecord 做符合逻辑的事情。
    • 感谢您的回答。我试过没有成功。 created_at 的两个错误ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column notifications.user does not exist LINE 1: ...ications"."created_at" FROM "notifications" WHERE "notificat...
    • 尝试删除.where(user: users)
    • 是的,抱歉。是否正确获取通知。它似乎不喜欢created_at。有点奇怪。 SELECT COUNT(*) FROM "notifications" WHERE "notifications"."created_at" = $1 [["created_at", "2015-10-12 16:19:28.306376"]] NoMethodError: undefined method []=' for nil:NilClass`
    • my_hash['key']['key2'] = 'test' 不能作为测试的原因是 my_hash["key1"] 在您的测试中还不存在。哦!呃,为了清楚起见,我交换了这些行,但正在做我说你不能做的事情。大声笑检查我的编辑。
    【解决方案2】:

    我正在使用另一种方法添加第二个答案,我认为这种方法会更好,因为它很有效并且可以转换为数据库视图。

    每当我在 DB 上遇到大量重复命中或无法很好翻译的大型复杂查询时,我希望使用纯 SQL,因为它可以用作 DB 中的视图。我问了这个question,因为我的 SQL 很差。我认为这可以适应您的需求,特别是如果“状态”字段是一组已知的可能值。这是我最初尝试的方式:

    构造一个有效的 SQL 查询。您可以在 psql 中进行测试。

    SELECT created_at, count(status) AS total,
    sum(case when status = 'error' then 1 end) AS errors,
    sum(case when status = 'pending' then 1 end) AS pending,
    sum(case when status = 'sent' then 1 end) AS sent
    FROM notifications
    GROUP BY created_at;
    

    这应该返回一个像这样的数据透视表:

    | created_at       |total|errors|pending|sent|
    ----------------------------------------------
    | Mon, 05 Oct 2015 |2572 |500   |12     |null|
    | Tue, 06 Oct 2015 |555  |null  |12     |50  |
    

    太好了,在 Rails 中,任何单个表都是一个简单的查询,可以将其加载为对象数组。这些对象中的每一个都有一个对应于每一列的方法。如果该行的列为空,Rails 将传递 nil 作为值。

    在 Rails 中测试

    @stats = Notification.where(user: users).find_by_sql("SELECT created_at, count(status) 
      AS total,
      sum(case when status = 'error' then 1 end) AS errors,
      sum(case when status = 'pending' then 1 end) AS pending,
      sum(case when status = 'sent' then 1 end) AS sent
      FROM notifications
      GROUP BY created_at;")
    

    这将返回一个 Notification 对象数组...

    => [#< Notification id: nil, created_at: "2014-02-07 22:36:30">
    #< Notification id: nil, created_at: "2014-06-26 02:07:51">,
    #< Notification id: nil, created_at: "2015-04-26 21:37:09">,
    #< Notification id: nil, created_at: "2014-02-07 22:48:29">,
    #< Notification id: nil, created_at: "2014-11-04 23:39:07">,
    #< Notification id: nil, created_at: "2015-01-27 17:46:50">,...]
    

    请注意,Notification id: 为零。这是因为这些对象并不代表数据库中的实际对象,而是您的查询生成的表中的一行。但是现在您可以执行以下操作:

    @stats.each do |daily_stats|
      puts daily_stats.attributes
    end
    
    #{"created_at" => "Mon, 05 Oct 2015", "total" = 2572, "errors" => 500, "pending" => 12, "sent" => nil}
    #{"created_at" => "Tue, 06 Oct 2015", "total" = 555, "errors" => nil, "pending" => 12, "sent" => 50}
    

    等等.. 您的@stats 变量很容易传递给一个视图,在该视图中它很容易打印为.html.erb 文件中的表格。您可以访问数组中任何 Notification 对象的属性,例如:

    @stats[0].created_at
      #=> "Mon, 05 Oct 2015"
    
    @stats[1].pending
      #=> 12
    

    总体而言,您使用了一个查询来获取整个数据集。

    将其存储为视图 登录到数据库上的 SQL 控制台并执行

    CREATE VIEW daily_stats AS
    SELECT user_id, created_at, count(status) AS total,
       sum(case when status = 'error' then 1 end) AS errors,
       sum(case when status = 'pending' then 1 end) AS pending,
       sum(case when status = 'sent' then 1 end) AS sent
    FROM notifications
    GROUP BY user_id, created_at;
    

    现在您可以使用

    获得结果
    Select * FROM daily_stats;
    

    请注意,我故意没有像您在原始问题中那样对用户进行限制,并将 user_id 添加到 SELECT。我们直接在数据库中工作,它应该可以轻松地从该视图生成一个表,其中包含每个日期的所有用户统计信息。对于您正在做的事情,这是一个非常强大的数据集。现在,您可以在 Rails 中设置一个虚拟模型,并且无需扭曲的 Rails 查询即可轻松获取所有数据。

    添加虚拟模型 应用程序/模型/daily_stat.rb:

    class DailyStat < ActiveRecord::Base
      belongs_to :user
      #this is a model for a view in the DB called dash_views
      #class name is singular and will automatically look for the table "daily_stats" which his snake_case and plural.
    end
    

    将对应关系添加到您的User 模型中:

    class User < ActiveRecord::Base
      has_many :daily_stats
    end
    

    现在您可以通过用户以一种非常正常的方式访问您的统计信息。

    users = [2]
    DailyStat.where(user: users)
       => AllStat Load (2.8ms)  SELECT "all_stats".* FROM "all_stats" WHERE "all_stats"."category_id" = 2
       => [ #<AllStat user_id: 2, created_at: "2014-02-14 00:30:24", total: 300, errors: 23, pending: nil, sent: 3>,
            #<AllStat user_id: 2, created_at: "2014-11-29 00:18:28", total: 2454, errors: 3, pending: 45, sent: 323>,
            #<AllStat user_id: 2, created_at: "2014-02-07 22:46:59", total: 589, errors: 33, pending: 240, sent: 68>...]
    

    在另一个方向:

    user = User.first
    user.daily_stats
     #returns array of that users DailyStat objects.
    

    关键是“在最底层解决问题”。解决一个数据库中的数据查询问题,然后用Rails来操作呈现。

    【讨论】:

    • 只是owao。非常感谢。我只想每天对通知进行分组。我试图将GROUP BY created_at 更改为GROUP BY date(created_at),但它不起作用。但对于其余的,它是完美的。谢谢!
    • 您提到group_by被弃用,但请注意所引用的网页显示“已弃用或移动”。它仍然存在于 Ruby 的 Enumerable 中。所以你可以只提取你想要的统计数据,因为你得到了一个返回的数组,你可以像这样在数组上调用它:DailyStat.all.group_by {|stat| stat.created_at} 现在它们被分组在一个哈希中,其中键是一个 DateTime。它不使用任何额外的查询,只是使用 ruby​​ 对它们进行排序。
    • 因为我现在没有做大量的 Ruby/Rails 编码,所以我经常回去阅读 Enumerable 类方法。由于您使用的是散列或数组,因此大多数 Rails 查询返回都可以使用它们。 ruby-doc.org/core-2.2.3/Enumerable.html
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-30
    • 2019-10-11
    • 2017-05-17
    • 1970-01-01
    • 2021-01-31
    • 2015-10-06
    • 2021-11-26
    相关资源
    最近更新 更多