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

问题描述:

在我的Rails应用我有users它可以有很多invoices这反过来又可以有很多payments如何(大量)减少Rails应用程序中SQL查询的数量?

现在在dashboard视图我想总结所有payments a user曾经收到,按年,季度或月份排序。 payments也细分为总计,

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的内部工作,所以这可能是这里的问题。

感谢您的任何帮助。

+2

您应该查看SQL请求以查找需要哪些信息,并使用'includes'(与发票相同)将它们包含在第一个请求中。如果您找不到它,请将SQL请求添加到您的问题中。 – Baldrick

+2

查看[bullet gem](https://github.com/flyerhzm/bullet),它可以通过使用急切加载来检测可以减少查询的位置。 – fivedigit

每当调用revenue_between时,它都会从db中获取给定时间范围内的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 
+0

非常好的一点,非常感谢。当我使用'.sum(&:amount_in_cents)'时,你的代码大大减少了SQL查询的数量。这种方式不再有N + 1个查询。我认为这是因为ActiveRecord可以通过这种方式直接从数据库获取金额。但是,当使用'.sum(&:“#{kind} _amount”)时,我仍然会通过'range'得到几十个查询。我想这是因为所有这些方法都必须计算每个“付款”(参见上面的“付款”模型代码)。那么可以通过为'net_amount','taxable_amount'和'gross_amount'添加额外的数据库列来避免这种情况? – Tintin81

+0

如何计算invoice.total invoice.subtotal等。你可以发布这些代码吗? – tihom

+0

我的意思是他们如何计算发票' – tihom

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

摆在首位,使您的页面没有统计和使用Ajax加载它们。

+1

好的,谢谢。对于以前没有做过很多Rails工作的人来说,创建一个'service'听起来很复杂。 “缓存”是否会成为更简单的替代方案?当用户创建新的“发票”或“付款”时,我可以简单地扫描缓存。 – Tintin81

+0

请注意缓存:它不会持久。 Redis可能是一个很好的选择。或者创建一个能够承载统计历史的模型 – apneadiving

从@tihom提出的memoizing战略

除此之外,我建议你看看Bullet gem,是因为他们在说说明,它将帮助你杀死N + 1个查询和未使用的急切加载。