【问题标题】:How to (massively) reduce the number of SQL queries in Rails app?如何(大量)减少 Rails 应用程序中的 SQL 查询数量?
【发布时间】:2013-10-01 13:42:36
【问题描述】:

在我的 Rails 应用程序中,我有 users,它可以有很多 invoices,而invoices 又可以有很多 payments

现在在dashboard 视图中,我想总结一下user 收到的所有payments,按年、季度或月排序。 payments 还细分为 grossnettax

user.rb

class User < ActiveRecord::Base

  has_many  :invoices
  has_many  :payments

  def years
    (first_year..current_year).to_a.reverse
  end

  def year_ranges
    years.map { |y| Date.new(y,1,1)..Date.new(y,-1,-1) }
  end

  def quarter_ranges
    ...
  end

  def month_ranges
    ...
  end

  def revenue_between(range, kind)
    payments_with_invoice ||= payments.includes(:invoice => :items).all
    payments_with_invoice.select { |x| range.cover? x.date }.sum(&:"#{kind}_amount") 
  end

end

invoice.rb

class Invoice < ActiveRecord::Base

  belongs_to :user
  has_many :items
  has_many :payments

  def total
    items.sum(&:total)
  end

  def subtotal
    items.sum(&:subtotal)
  end

  def total_tax
    items.sum(&:total_tax)
  end

end

payment.rb

class Payment < ActiveRecord::Base

  belongs_to :user
  belongs_to :invoice  

  def percent_of_invoice_total
    (100 / (invoice.total / amount.to_d)).abs.round(2)
  end

  def net_amount
    invoice.subtotal * percent_of_invoice_total / 100
  end  

  def taxable_amount
    invoice.total_tax * percent_of_invoice_total / 100
  end

  def gross_amount
    invoice.total * percent_of_invoice_total / 100
  end

end

dashboards_controller

class DashboardsController < ApplicationController

  def index    
    if %w[year quarter month].include?(params[:by])   
      range = params[:by]
    else
      range = "year"
    end
    @ranges = @user.send("#{range}_ranges")
  end

end

index.html.erb

<% @ranges.each do |range| %>

  <%= render :partial => 'range', :object => range %>

<% end %>

_range.html.erb

<%= @user.revenue_between(range, :gross) %>
<%= @user.revenue_between(range, :taxable) %>
<%= @user.revenue_between(range, :net) %>

现在的问题是这种方法有效,但也会产生大量的 SQL 查询。在典型的 dashboard 视图中,我得到 100+ 个 SQL 查询。在添加.includes(:invoice) 之前,还有更多的查询。

我认为主要问题之一是每张发票的subtotaltotal_taxtotal 并未存储在数据库中的任何位置,而是根据每个请求进行计算。

谁能告诉我如何在这里加快速度?我不太熟悉 SQL 和 ActiveRecord 的内部工作原理,所以这可能是这里的问题。

感谢您的帮助。

【问题讨论】:

  • 您应该查看 SQL 请求以查找需要哪些信息,并使用includes(就像您为发票所做的那样)将它们包含在第一个请求中。如果找不到,请将 SQL 请求添加到您的问题中。
  • 查看bullet gem,它会检测您可以通过使用急切加载减少查询的位置。

标签: ruby-on-rails ruby ruby-on-rails-3 activerecord


【解决方案1】:

每当调用revenue_between 时,它都会从数据库中获取给定时间范围内的payments 以及关联的invoicesitems。由于时间范围有很多重叠(月、季度、年),因此会一遍又一遍地获取相同的记录。

我认为最好一次获取用户的所有付款,然后在 Ruby 中对其进行过滤和汇总。

要实现,将revenue_between方法更改如下:

def revenue_between(range, kind) 
   #store the all the payments as instance variable to avoid duplicate queries
   @payments_with_invoice ||= payments.includes(:invoice => :items).all
   @payments_with_invoice.select{|x| range.cover? x.created_at}.sum(&:"#{kind}_amount") 
end

这会急切地加载所有付款以及相关的发票和项目。

同时更改invoice 求和方法,使其使用预先加载的items

class Invoice < ActiveRecord::Base

   def total
     items.map(&:total).sum
   end

   def subtotal
     items.map(&:subtotal).sum
   end

   def total_tax
     items.map(&:total_tax).sum
   end

end

【讨论】:

  • 应该绝对可以不添加列,发布控制器和视图......这里有一些小东西。
  • 更新了答案,这有点矫枉过正,但想法是使用 mappayment 模型本身中执行所有发票操作,并在关联本身中包括 items。这个方案在我的应用中对我有用。
  • 一个问题是在invoice 模型中使用sum。它忽略了急切加载的项目并再次查询数据库。像 sum maximum count 这样的 Arel 计算不会急于加载。
  • 你也可以在 Invoice 模型中做到这一点
  • 用更紧凑的语法更新了答案。
【解决方案2】:

除了@tihom 提出的memoizing 策略,我建议你看看Bullet gem,正如他们在描述中所说,它将帮助你杀死N+1 个查询和未使用的急切加载。

【讨论】:

    【解决方案3】:

    您的大部分数据不需要是实时的。你可以有一个服务来计算统计数据并将它们存储在你想要的任何地方(Redis,缓存......)。然后每 10 分钟或根据用户要求刷新它们。

    首先,在没有统计信息的情况下呈现您的页面并使用 ajax 加载它们。

    【讨论】:

    • 好的,谢谢。对于以前没有做过大量 Rails 工作的人来说,创建 service 听起来很复杂。 caching 会是一个更简单的替代方案吗?当用户创建新的invoicepayment 时,我可以简单地清除缓存。
    • 小心缓存:它不是持久化的。 Redis 可能是一个不错的选择。或者创建一个包含统计历史的模型
    猜你喜欢
    • 2023-03-09
    • 2023-01-07
    • 2019-05-30
    • 2019-08-22
    • 1970-01-01
    • 2011-01-10
    • 2016-10-24
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多