【问题标题】:Getting a rails model with statistics获取带有统计信息的 Rails 模型
【发布时间】:2010-02-26 00:14:43
【问题描述】:

我在 Rails 应用程序中有几个模型。假设它是产品和销售。

一个产品有很多销售,一个销售属于一个产品。

我要做的是获取所有产品的列表(可能带有条件),并在每个产品旁边提供销售数量的计数,类似于:

  • 产品 1 (10)
  • 产品 2 (22)

虽然输出相当简单,但我无法找到一种不使用 SQL 来收集数据的好方法。

我还应该补充一点,我可能也想对销售应用条件。

【问题讨论】:

    标签: ruby-on-rails activerecord


    【解决方案1】:

    这不是一个答案,而是这个问题的三个答案的比较。我已经给出了我的答案。 :joins:includeActiveRecord.find 中的工作存在混淆。所以我花了一些时间分析了三个解决方案的 SQL 日志。

    方法一:使用count获取销售计数

      #To return the sales count for each product
      ps_count_hash = Product.count(:joins => [:sales], :group => "products.id") # sql 1.1
    
      # To print the product id and sales count
      ps_count_hash.each do | product_id, sales_count|
       p "Product#{product_id} - (#{sales_count})"
      end
    
      # To print the product details and sales count
      # get all the products associated 
      Product.find_all_by_id(ps_count_hash.keys).each |product|  #sql 1.2
       p "Product[id = #{product.id}, name = #{product.name}] - (#{ps_count_hash[product.id]})"
      end
    

    方法二:通过join获取产品

      Product.find(:all, :joins=>[:sales]).each do |product| #sql 2.1
       p "Product[id = #{product.id}, name = #{product.name}] - (#{product.sales.size})" # sql 2.2 - 2.(2+N)
      end
    

    方法三:通过include获取产品

      Product.find(:all, :include=>[:sales]).each do |product| #sql 3.1 and 3.2
       p "Product[id = #{product.id}, name = #{product.name}] - (#{product.sales.size})" 
      end
    

    现在让我们看看这三种方法生成的 SQL 语句

    方法 1 - 2 SQL 的 SQL 语句

    SELECT count(*) AS count_all, products.id AS products_id FROM `products` INNER JOIN `sales` ON sales.product_id = products.id GROUP BY products.id 
    SELECT `products`.* FROM `products` WHERE (`products`.`id` IN (1,2))
    

    方法 2 - 2 SQL 的 SQL 语句

    SELECT * FROM `products` 
    SELECT `sales`.* FROM `sales` WHERE (`sales`.product_id IN (1,2)) 
    

    方法 3 - N + 1 SQL 的 SQL 语句

    SELECT `products`.* FROM `products` INNER JOIN `sales` ON sales.product_id = products.id 
    SELECT * FROM `sales` WHERE (`sales`.product_id = 1) 
    SELECT * FROM `sales` WHERE (`sales`.product_id = 2) 
    

    按销售额计算产品的最佳方法(不含销售详情):方法 1

    按销售额计算产品的最佳方法(包含销售详情):方法 2

    方法 3 存在 N+1 问题。所以它是没有争议的。

    【讨论】:

      【解决方案2】:

      如果您正确地声明了associations,则如下所示:

      Product.find(:all, :include => :sales).each do |product|
        puts product.name + " (#{product.sales.size})"
      end
      

      编辑:
      关联是丰富的集合,如果您需要应用条件,可以对其进行搜索:

      @products = Product.find(:all, :include => :sales)
      @products.each do |product|
        # Find product sales only from yesterday
        yesterdays_sales = product.sales.find(:all, :conditions => { :created_at => Date.yesterday }
      end
      

      【讨论】:

      • 这里有N+1 问题。
      • 没有理由过早优化。关联是 Rails 的方式,任何查看他的代码的 Rails 开发人员都会熟悉。如果这成为瓶颈,他总是可以在以后进行优化。
      • 这不是一个过早的优化。只需使用:include:join。你不会为此花时间。如果我们现在可以用完全相同的努力编写更好的代码,为什么还要提前写出糟糕的代码。
      • 你说得对,我应该在这些中使用:include。我已经编辑了帖子以说明这一点。
      • 我相信它不能解决第二个示例中的问题。它将在第一行加载产品和销售。但是,当您迭代产品时,您会再次对每个产品执行 1 个查询。因此,当您首先提出所有产品+销售时,情况会更糟。然后每次查询每个产品的销售额。条件应包含在第一个查询中以解决该问题。
      【解决方案3】:

      如果您最感兴趣的是计算每个产品的销售额,您应该使用counter_cache 功能。这将确保您始终从 products 表中获取计数,而不是 JIT 计数计算。

      # products table should have a column called sales_count
      class Product < ActiveRecord:Base
        has_many :sales
      end
      
      class Sales < ActiveRecord:Base
        belongs_to :product, :counter_cache => true
      end
      

      rails 将在创建/删除销售时增加/减少 sales_count 列。 现在,您可以通过执行以下操作来获取销售额:

      Product.first.sales_count
      

      要对单个 product 对象有条件地计算 sales,请执行以下操作:

      Product.first.sales.count(:conditions => ["amount > ? ", 200])
      

      要有条件地计算一批products,请执行以下操作:

      #returns a ordered hash
      product_sales_count = Product.count(:joins => [:sales], :conditions => 
                  ["amount > ? ", 200], :group =>"products.id").each do |product_id, sales_count|
         p "Product #{product_id} = #{sales_count}"
      end
      
      # If you need the product objects get all of them in one call and match the count
      products = Product.find(:all, :joins => [:sales], :conditions => 
                   ["amount > ? ", 200], :group =>"products.id").each do |product|
      
         p "Product #{product.name} = #{product_sales_count[product.id]}"
      
      end
      

      这种方法将节省大量访问数据库的次数。

      【讨论】:

      • Product.first.sales.count(:conditions =&gt; ["amount &gt; ? ", 200]) 将对每个产品执行一个查询。那么如果需要条件计数,那么拥有 :counter_cache 有什么意义呢?
      • 正如我在回答中提到的,只有当他主要无条件计算销售额时,他才应该选择 counter_cache。我已经修改了我的答案以解决条件场景的 N+1 问题。同样,这取决于他的用例。
      【解决方案4】:

      如果您不介意将数据库中的相关销售对象引入应用程序,那么 like this 应该可以完成这项工作:

      但请留意。

      # Preload all the Sales for our Products, sow we won't end up with `N+1` issue.
      selected_products = Product.all :joins => :sales, :conditions => {'orders.successful' => true}
      
      # Then use it like a normal association:
      selected_products.each { |p| puts p.sales.size }
      

      问候,
      德米特里。

      【讨论】:

      • 您的解决方案存在 N+1 问题。 Product.all 调用在WHERE 条件中使用join 表。在您对 p.sales 的后续调用中,将向数据库提交一个新查询以获取与产品关联的销售额。我在这里给出了详细的回复:(stackoverflow.com/questions/2338605/…)。
      • @KandadaBoggu,非常感谢。我相信sales.size 不会执行查询,因为它应该加载:join。需要去看看。
      • 不用担心。因为你,我能够证实我一直对joins 的怀疑。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-02-14
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多