活动记录查询,返回在餐厅具有超过一定数量的check_ins的用户

时间:2014-10-20 18:05:52

标签: sql activerecord ruby-on-rails-4

在我的数据库中,我Userscheck_inscheck_inrestaurant_id的一家餐馆相关联。什么是最有效的方式让所有在特定餐厅办理登机手续的用户超过X次?

2 个答案:

答案 0 :(得分:3)

要编写效果Active Record查询,首先必须知道如何编写有效的SQL查询。与任何编程问题一样,第一步是将其分解为较小的任务。

TL; DR

当你只需要一个查询时,不要做两个查询。

users_with_check_in_counts = User.select('users.*, COUNT(*) AS check_in_count')
                               .joins('LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id')
                               .where(check_ins: { restaurant_id: 1 })
                               .group(:id)
                               .having('check_in_count > ?', 3)
                               .all
# => [ #<User id=2, name="Nick", ..., check_in_count=4>,
#      #<User id=4, name="Jordan", ..., check_in_count=4> ]

nick = users_with_check_in_counts.first
puts nick.check_in_count
# => 4

前奏

您的check_ins表可能看起来像这样:

  id | restaurant_id | user_id | ...
-----+---------------+---------+-----
   1 |             1 |       1 | ... 
   2 |             1 |       2 |
   3 |             1 |       2 |
   4 |             1 |       2 |
   5 |             1 |       2 |
   6 |             1 |       3 |
   7 |             1 |       3 |
   8 |             1 |       3 |
   9 |             1 |       4 |
  10 |             1 |       4 |
  11 |             1 |       4 |
  12 |             1 |       4 |
  13 |             2 |       1 |
 ... |           ... |     ... | ...

在上表中,我们在restaurant_id = 1餐厅办理了12次办理登机手续。签入user_id = 1的用户,2签入四次,3签到两次,4签到四次。

天真的方式

这种天真的方法是将其分解为以下任务:

  1. 获取餐厅的check_ins条记录:

    SELECT * FROM check_ins WHERE restaurant_id = 1;
    
  2. 通过user_id分组并计算每组中的记录数量,获取餐馆每个用户的签到次数:

      SELECT check_ins.*, COUNT(user_id) AS check_in_count
        FROM check_ins
       WHERE restaurant_id = 1
    GROUP BY user_id
    
  3. 将结果限制为至少包含 N 记录的组,例如N = 3

      SELECT check_ins.*, COUNT(user_id) AS check_in_count
        FROM check_ins
       WHERE restaurant_id = 1
    GROUP BY user_id
      HAVING check_in_count >= 3
    
  4. 将其转换为Active Record查询:

    check_in_counts = CheckIn.where(restaurant_id: 1).group(:user_id)
                        .having("user_count > ?", 3).count
    # => { 2 => 4, 4 => 4 }
    
  5. 编写第二个查询以获取关联用户:

    User.find(check_in_counts.keys)
    # => [ #<User id=2, ...>, #<User id=4, ...> ]
    
  6. 这很有效,但有一些有点臭 - 哦,我们正在使用关系数据库。如果我们有一个从check_ins获取记录的查询,我们应该在同一个查询中获取相关的users

    更好的方法

    现在,我们可以从上面的(3)中获取我们的SQL查询并添加JOIN users ON check_ins.user_id = users.id来获取关联的用户记录,这是相对明显的,但这使我们处于绑定状态,因为我们仍然希望Active Record能够提供us用户对象,而不是CheckIn对象。为此,我们需要一个不同的查询,一个以users开头并加入check_ins的查询。

    为此,我们使用LEFT OUTER JOIN

             SELECT *
               FROM users
    LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id
              WHERE restaurant_id = 1;
    

    以上查询会给我们这样的结果:

     id | name   | ... | restaurant_id | user_id
    ----+--------+-----+---------------+---------
      1 | Sarah  |   1 |             1 |       1
      2 | Nick   |   1 |             1 |       2
      2 | Nick   |   1 |             1 |       2
      2 | Nick   |   1 |             1 |       2
      2 | Nick   |   1 |             1 |       2
      3 | Carmen |   1 |             1 |       3
      3 | Carmen |   1 |             1 |       3
      3 | Carmen |   1 |             1 |       3
      4 | Jordan |   1 |             1 |       4
      4 | Jordan |   1 |             1 |       4
      4 | Jordan |   1 |             1 |       4
      4 | Jordan |   1 |             1 |       4
    

    这看起来很熟悉:它包含来自check_ins的所有数据,其中users的数据添加到每一行。这就是LEFT OUTER JOIN的作用。现在,就像之前一样,我们可以使用GROUP BY按用户ID分组,COUNT使用HAVING计算每个组中的记录,以便将结果限制为具有特定数量的用户签到:

             SELECT users.*, COUNT(*) AS check_in_count
               FROM users
    LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id
              WHERE restaurant_id = 1
           GROUP BY users.id
             HAVING check_in_count >= 3;
    

    这给了我们:

     id | name   | ... | check_in_count
    ----+--------+-----+----------------
      2 | Nick   | ... |             4
      4 | Jordan |     |             4
    

    完美!

    最后...

    现在我们所要做的就是将其转换为Active Record查询。这很简单:

    users_with_check_in_counts = User.select('users.*, COUNT(*) AS check_in_count')
                                   .joins('LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id')
                                   .where(check_ins: { restaurant_id: 1 })
                                   .group(:id)
                                   .having('check_in_count > ?', 3)
                                   .all
    # => [ #<User id=2, name="Nick", ..., check_in_count=4>,
    #      #<User id=4, name="Jordan", ..., check_in_count=4> ]
    
    nick = users_with_check_in_counts.first
    puts nick.check_in_count
    # => 4
    

    最重要的是,它只执行一次查询。

    奖金:范围

    这是一个非常长的Active Record查询。如果你的应用中只有一个地方你会有这样的查询,那么就可以这样使用它。如果我是你,我会把它变成一个范围:

    class User < ActiveRecord::Base
      scope :with_check_in_count, ->(opts) {
        opts[:at_least] ||= 1
    
        select('users.*, COUNT(*) AS check_in_count')
          .joins('LEFT OUTER JOIN check_ins ON users.id = check_ins.user_id')
          .where(check_ins: { restaurant_id: opts[:restaurant_id] })
          .group(:id)
          .having('check_in_count >= ?', opts[:at_least])
      }
    
      # ...
    end
    

    然后:

    User.with_check_in_count(at_least: 3, restaurant_id: 1)
    # ...or just...
    User.with_check_in_count(restaurant_id: 1)
    

答案 1 :(得分:0)

我无法使用您的确切模型架构进行检查,但这样的事情应该有效:

check_in_counts = CheckIn.group(:user_id).having(restaurant_id: 3).having('COUNT(id) > 10').count

这将返回Hash user_id =&gt; check_in_count值,可用于获取所有User个对象:

users = User.find(check_in_counts.keys)