sqlruby-on-railspostgresqlaggregate-functionsaggregate-filter

Rails Query - Group By with 2 groups


In my website my users have an attribute localidade. This specifies where they live.

I'm trying to do a query where I group the results the following way:

localidade   | Number of Users
-------------+--------------
New York     | 6
Not New York | 8

I want the number of users from New York and the number of users from anywhere else but New York.

I tried this:

User.group("lower(localidade) = 'new york'").count

but since I don't have any users from new york and only 1 not from New York it returns:

{false => 1}

Am I able to give aliases to groups? Is there any way of grouping this way the results?

I'm gonna use the results for a Pie Graph from Graphkick.


Solution

  • You could write your query :

    User.group("lower(localidade)")
        .select("CASE WHEN lower(localidade) = 'new york' THEN COUNT(id) END AS NewYork, 
                 CASE WHEN lower(localidade) != 'new york' THEN COUNT(id) END AS Non-NewYork")
    

    Since 9.4, you can use FILTER with aggregate expression :

    User.group("lower(localidade)")
        .select("COUNT(id) FILTER (WHERE lower(localidade) != 'new york') AS NonNewyork, 
                 COUNT(id) FILTER (WHERE lower(localidade) = 'new york') AS Newyork")
    

    I created a Table to explain and test the above sql, and they worked as expected :

    [shreyas@rails_app_test (master)]$ rails db
    psql (9.4.1)
    Type "help" for help.
    
    app_development=# select id, location, name from people;
     id | location | name
    ----+----------+------
      2 | X        | foo
      3 | X        | foo
      4 | Y        | foo
    (3 rows)
    
    app_development=# SELECT COUNT(id) FILTER(WHERE lower(location) != 'x') AS Non_X_loc, COUNT(id) FILTER (WHERE lower(location) = 'x') AS X_loc FROM "people";
     non_x_loc | x_loc
    -----------+-------
             1 |     2
    (1 row)
    

    Let me now, jump to the rails console, and test the equivalent Rails code :

    [2] pry(main)> p = Person.select("COUNT(id) FILTER(WHERE lower(location) != 'x') AS Non_X_loc, COUNT(id) FILTER (WHERE lower(location) = 'x') AS X_loc ")
      Person Load (0.5ms)  SELECT COUNT(id) FILTER(WHERE lower(location) != 'x') AS Non_X_loc, COUNT(id) FILTER (WHERE lower(location) = 'x') AS X_loc  FROM "people"
    => [#<Person:0x007fd85ed71980 id: nil>]
    [3] pry(main)> p.first.attributes
    => {"id"=>nil, "non_x_loc"=>1, "x_loc"=>2}
    [6] pry(main)> Person.group("lower(location)").select("CASE WHEN lower(location) = 'x' THEN COUNT(id) END AS X_loc, CASE WHEN lower(location) != 'x' THEN COUNT(id) END AS Non_X_loc")
      Person Load (0.6ms)  SELECT CASE WHEN lower(location) = 'x' THEN COUNT(id) END AS X_loc, CASE WHEN lower(location) != 'x' THEN COUNT(id) END AS Non_X_loc FROM "people" GROUP BY lower(location)
    => [#<Person:0x007fd8608281e8 id: nil>, #<Person:0x007fd860828008 id: nil>]
    [7] pry(main)> p = _
    => [#<Person:0x007fd8608281e8 id: nil>, #<Person:0x007fd860828008 id: nil>]
    [8] pry(main)> p.map { |rec| rec.attributes }
    => [{"id"=>nil, "x_loc"=>nil, "non_x_loc"=>1}, {"id"=>nil, "x_loc"=>2, "non_x_loc"=>nil}]
    [9] pry(main)> p.map { |rec| rec.attributes.except('id') }
    => [{"x_loc"=>nil, "non_x_loc"=>1}, {"x_loc"=>2, "non_x_loc"=>nil}]
    

    Update

    You can remove those nil from DB level only :

    Rails code :

    [shreyas@rails_app_test (master)]$ rails c
    Loading development environment (Rails 4.2.0)
    [1] pry(main)> Person.group("lower(location)").select("CASE WHEN lower(location) = 'x' THEN COUNT(id) ELSE 0 END AS X_loc, CASE WHEN lower(location) != 'x' THEN COUNT(id) ELSE 0 END AS Non_X_loc")
      Person Load (0.9ms)  SELECT CASE WHEN lower(location) = 'x' THEN COUNT(id) ELSE 0 END AS X_loc, CASE WHEN lower(location) != 'x' THEN COUNT(id) ELSE 0 END AS Non_X_loc FROM "people" GROUP BY lower(location)
    => [#<Person:0x007fd858c100b0 id: nil>, #<Person:0x007fd860853e88 id: nil>]
    [2] pry(main)> p = _
    => [#<Person:0x007fd858c100b0 id: nil>, #<Person:0x007fd860853e88 id: nil>]
    [3] pry(main)> p.map { |rec| rec.attributes }
    => [{"id"=>nil, "x_loc"=>0, "non_x_loc"=>1}, {"id"=>nil, "x_loc"=>2, "non_x_loc"=>0}]
    [4] pry(main)> p.map { |rec| rec.attributes.except('id') }
    => [{"x_loc"=>0, "non_x_loc"=>1}, {"x_loc"=>2, "non_x_loc"=>0}]
    [5] pry(main)> p = Person.select("count(CASE WHEN lower(location) = 'x' THEN 1 END) AS X_loc, count(CASE WHEN lower(location) != 'x' THEN 1 END) AS Non_X_loc").group("lower(location)")
      Person Load (0.9ms)  SELECT count(CASE WHEN lower(location) = 'x' THEN 1 END) AS X_loc, count(CASE WHEN lower(location) != 'x' THEN 1 END) AS Non_X_loc FROM "people" GROUP BY lower(location)
    => [#<Person:0x007fd85b150f78 id: nil>, #<Person:0x007fd85b150230 id: nil>]
    [6] pry(main)> p.map { |rec| rec.attributes.except('id') }
    => [{"x_loc"=>0, "non_x_loc"=>1}, {"x_loc"=>2, "non_x_loc"=>0}]
    

    SQL

    app_development=# select CASE WHEN lower(location) = 'x' THEN COUNT(id) ELSE 0 END AS X_loc, CASE WHEN lower(location) != 'x' THEN COUNT(id) ELSE 0 END AS Non_X_loc from people group by lower(location);
     x_loc | non_x_loc
    -------+-----------
         0 |         1
         2 |         0
    (2 rows)
    app_development=# select count(CASE WHEN lower(location) = 'x' THEN 1 END) AS X_loc, count(CASE WHEN lower(location) != 'x' THEN 1 END) AS Non_X_loc from people group by lower(location);
     x_loc | non_x_loc
    -------+-----------
         0 |         1
         2 |         0
    (2 rows)
    

    Update- II

    The classical approach to get the output same as FILTER :

    app_development=# select count(CASE WHEN lower(location) = 'x' THEN 1 END) AS X_loc, sum(CASE WHEN lower(location) != 'x' THEN 1 END) AS Non_X_loc from people;
     x_loc | non_x_loc
    -------+-----------
         2 |         1
    (1 row)
    
    app_development=# select sum(CASE WHEN lower(location) = 'x' THEN 1 END) AS X_loc, sum(CASE WHEN lower(location) != 'x' THEN 1 END) AS Non_X_loc from people;
     x_loc | non_x_loc
    -------+-----------
         2 |         1
    (1 row)
    
    app_development=# select id, location, name from people;
     id | location | name
    ----+----------+------
      2 | X        | foo
      3 | X        | foo
      4 | Y        | foo
    (3 rows)
    
    app_development=#
    

    And In Rails way :-

    Loading development environment (Rails 4.2.0)
    [1] pry(main)> p = Person.select("sum(CASE WHEN lower(location) = 'x' THEN 1 END) AS X_loc, sum(CASE WHEN lower(location) != 'x' THEN 1 END) AS Non_X_loc")
      Person Load (0.6ms)  SELECT sum(CASE WHEN lower(location) = 'x' THEN 1 END) AS X_loc, sum(CASE WHEN lower(location) != 'x' THEN 1 END) AS Non_X_loc FROM "people"
    => [#<Person:0x007fd85b6e6a78 id: nil>]
    [2] pry(main)> p.first.attributes.except("id")
    => {"x_loc"=>2, "non_x_loc"=>1}
    [3] pry(main)> p = Person.select("count(CASE WHEN lower(location) = 'x' THEN 1 END) AS X_loc, count(CASE WHEN lower(location) != 'x' THEN 1 END) AS Non_X_loc")
      Person Load (0.5ms)  SELECT count(CASE WHEN lower(location) = 'x' THEN 1 END) AS X_loc, count(CASE WHEN lower(location) != 'x' THEN 1 END) AS Non_X_loc FROM "people"
    => [#<Person:0x007fd85b77f098 id: nil>]
    [4] pry(main)> p.first.attributes.except("id")
    => {"x_loc"=>2, "non_x_loc"=>1}
    [5] pry(main)>