如何(大规模)减少Rails应用程序中的SQL查询数量?

时间:2013-09-29 10:32:07

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

在我的Rails应用中,我有users,其中可以包含许多invoices,而payments可能会有很多dashboard

现在,在payments视图中,我想总结所有user a payments曾收到过的信息,按年,季度或月份排序。 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 也细分为 net tax

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

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

payment.rb

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

dashboards_controller

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

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

<% end %>

index.html.erb

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

_range.html.erb

dashboard

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

我认为其中一个主要问题是每个发票的total_taxtotal和{{1}}都没有存储在数据库的任何位置,而是根据每个请求进行计算。

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

感谢您的帮助。

3 个答案:

答案 0 :(得分:4)

每当调用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

答案 1 :(得分:2)

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

答案 2 :(得分:1)

您的大多数数据不需要是实时的。您可以使用计算统计数据的服务并将其存储在任何位置(Redis,缓存...)。然后每10分钟或根据用户的要求刷新它们。

首先,渲染没有统计数据的页面,然后使用ajax加载它们。