1

I have a model Notification and I would like to know how many notifications are created per days and how many with each status. status is a string key of the model Notification.

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

That give me :

{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}

But I would like something like :

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

Or similar. Is it possible with active record ? Maybe group_by was something good for me. But it's deprecated.

For the moment I've tried :

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

It's not the result I want.

Beartech
  • 6,173
  • 1
  • 18
  • 41
Mio
  • 1,412
  • 2
  • 19
  • 41

2 Answers2

3

This is tough for me to test possible answers since the date is somewhat unique and the query slightly complex. But here is how to get your counts in a hash:

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

Edit

As the OP found out, this becomes very inefficient as the number of dates queried against goes up. That's because it is calling a query for every date returned in the first query. I'm sure there is a way to do this as one long SQL query and use that as a view in the DB.

Beartech
  • 6,173
  • 1
  • 18
  • 41
  • By the way, I never thought `.select(:status).group(:status).count` would do what it does. LOL, I can never get ActiveRecord to do things that logically when I try. – Beartech Dec 11 '15 at 07:16
  • Thanks for your answer. I've tried without success. Two errors with the created_at `ActiveRecord::StatementInvalid: PG::UndefinedColumn: ERROR: column notifications.user does not exist LINE 1: ...ications"."created_at" FROM "notifications" WHERE "notificat...` – Mio Dec 11 '15 at 07:50
  • Try dropping `.where(user: users)`. – Beartech Dec 11 '15 at 07:57
  • Yes sorry. Did fetch the notifications properly. It seems it doesn't like created_at. It's weird. `SELECT COUNT(*) FROM "notifications" WHERE "notifications"."created_at" = $1 [["created_at", "2015-10-12 16:19:28.306376"]] NoMethodError: undefined method `[]=' for nil:NilClass` – Mio Dec 11 '15 at 08:16
  • First we can't do that `my_hash[n.created_at]["total"]` and `Notification.where(created_at: n.created_at).select(:status).group(:status).count` return `PG::WrongObjectType: ERROR: count(*) must be used to call a parameterless aggregate function` – Mio Dec 11 '15 at 08:37
  • I am using a similar table in my own project to test this. I can't tell you exactly what your problem is because I don't have any access to the system you are using. As far as my tables go these queries work. I have to assume the `users` variable is an array? You will need to provide a lot more information if you really want an answer that works. Do you have your code up on github or somewhere similar? – Beartech Dec 11 '15 at 14:53
  • What do you mean by "First we can't do that"? – Beartech Dec 11 '15 at 14:56
  • I get the notifications I want with the user device. Like `notifications = Notification.where(device: Device.where(user: Store.find(12).users.ids)).select(:created_at).uniq` I get proper notifications like ` #,` – Mio Dec 11 '15 at 14:59
  • `my_hash['key']['key2'] = 'test' #NoMethodError: undefined method '[]=' for nil:NilClass` – Mio Dec 11 '15 at 15:02
  • What happens if you drop the entire `my_hash[n.created_at]["total"] = Notification...` line? – Beartech Dec 11 '15 at 15:03
  • 1
    The reason `my_hash['key']['key2'] = 'test'` does not work as a test is that my_hash["key1"] does not exist yet in your test. OH! duh, I swapped those lines for clarity but was doing just what I said you can't do. LOL check my edit. – Beartech Dec 11 '15 at 15:11
  • Swapping the order of the two lines in the `each` statement makes a big difference. – Beartech Dec 11 '15 at 15:12
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/97658/discussion-between-beartech-and-beni-mio). – Beartech Dec 11 '15 at 15:25
1

I'm adding a second answer with a different approach that I believe to be much better in that it is efficient and can be translated into a DB view.

Any time I end up with lots of repeated hits on the DB or large, complex queries that don't translate well, I look to use pure SQL as that can then be used as a view in the DB. I asked this question because my SQL is poor. I think this can be adapted to your needs, especially if the "status" field is a know set of possible values. Here's how I would try it initially:

Construct a SQL query that works. You can test this in 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;

This should return a pivot table like:

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

Great, any single table is an easy query in Rails that will load it up as an array of objects. Each of those objects will have a method that corresponds to each column. If the column is null for that row Rails will pass nil as the value.

Test it in 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;")

Which will return an array of Notification objects...

=> [#< 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">,...]

Note that the Notification id: is nil. That's because these objects do not represent the actual objects in the DB, but a row in the table produced by your query. But now you can do something like:

@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}

and so on.. Your @stats variable is easily passed to a view where it is easily printed as a table in an .html.erb file. You can access the attributes of any Notification object in the array like:

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

@stats[1].pending
  #=> 12

The overall point is you have used one query to get your entire dataset.

Store it as a view Log into the SQL console on your DB and do

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;

Now you can get the results with

Select * FROM daily_stats;

Note that I have purposefully not limited this by user as you are in your original question and added user_id to the SELECT. We are working in the DB directly and it should easily handle generating a table from this view with ALL users stats for every date. This is a very powerful dataset for what you are doing. Now you can set up a dummy model in Rails and easily have all of your data available without contorted Rails queries.

Add a dummy model app/models/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

add the corresponding relation to your User model:

class User < ActiveRecord::Base
  has_many :daily_stats
end

Now you have access to your stats by user in a very rail-ish way.

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>...]

and in the other direction:

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

The key is to "solve things at the lowest level". Solve a data query problem in the database, then use Rails to manipulate and present it.

Community
  • 1
  • 1
Beartech
  • 6,173
  • 1
  • 18
  • 41
  • Just owao. Thanks a lot. I would like just to group notifications per day. I tried to change `GROUP BY created_at` to `GROUP BY date(created_at)` but it doesn't work. But for the rest it's perfect. Thanks ! – Mio Dec 13 '15 at 10:57
  • 1
    You mention `group_by`being deprecated, but note that the web page referenced says "deprecated or moved". It sill exists in Ruby's Enumerable. So you can just pull the stats you want and since you are getting an array returned you can just call it on the array like: `DailyStat.all.group_by {|stat| stat.created_at}` Now they are grouped in a hash where the key is a DateTime. It doesn't use any additional queries, just sorts them using ruby. – Beartech Dec 13 '15 at 18:57
  • 1
    Because I am not doing a ton of Ruby/Rails coding at the moment, I often go back and read through the Enumerable class methods. They are available to you for most Rails query returns since you're working with either a hash or an array. http://ruby-doc.org/core-2.2.3/Enumerable.html – Beartech Dec 13 '15 at 20:50