activerecord和慢速DB连接:(n + 1)个查询:如何有效地命中DB?

时间:2015-02-25 20:12:13

标签: ruby-on-rails ruby performance activerecord

我有一个RoR应用程序,可以经常通过个人查询来访问数据库。问题是每个查询花费100毫秒,并且由于ActiveRecord正在生成的单个查询的数量,一些控制器操作需要很长时间才能完成。

另一个问题是我不能使用AR includes()(它只生成一个查询),因为我遇到了AR-sqlserver-adapter和unicode字符串忽略索引的问题,我正在从sql查询中删除unicode前缀(这是一个长篇故事......)

示例:

#model
class Company < ActiveRecord::Base
   has_one: city
end
#controller
sql_string = where(:code => ['12B1', '34C8', '87DD', ...]).to_sql
sql_string = sql_string.gsub("N'","'")  #remove unicode prefix
companies = find_by_sql(sql_string)
#companies = Company.where(:code => ['12B1', '34C8', '87DD', ...]).includes(:city) #I wish I could use this line

当我访问公司阵列中的城市时,我会在控制台中看到每个城市的几个单独查询,这会减慢很多事情。

我希望仅 2个查询:一个用于使用SQL IN的公司,一个用于城市。

我已经手动完成了(使用虚拟属性),但是否有“Rails方式”呢?

到目前为止我所做的(谨慎,未来的丑陋代码)

    def self.get_companies_by_code(code_array)
       comps = Company.where(company_code: ['12B1', '34C8', '87DD', ...])
       cities_id_array = comps.map {|c| c.city_id}.compact.uniq
       cities = City.find(cities_id_array)
       comps.each {|co| 
           co.virtual_city = cities.select{|ct| co.city_id==ct.id}.first }
       comps
   end

2 个答案:

答案 0 :(得分:0)

comps = Company.where(company_code: ['12B1', '34C8', '87DD', ...]).select(:city_id).distinct(:city_id)
cities = City.where(id: comps.collect { |c| c.city_id })

那就是说,rails方式真的是用“包括”。我强烈建议在核心修复unicode问题,这样你就不会在你的代码中永久分散很多变通方法(比如这样)。

答案 1 :(得分:0)

比2个查询(一个用于公司,一个用于城市)更好,只有一个查询使用连接:

def self.get_companies_by_code(code_array)
  sql_string = Company.where(company_code: code_array).joins(:city).select("*").to_sql #['12B1', '34C8', '87DD', ...]
  sql_string = sql_string.gsub("N'","'")  #remove unicode prefix
  companies = find_by_sql(sql_string)  
end

我在控制台上只获得了这个查询:

Company Load (60.6ms)  EXEC sp_executesql N'SELECT * FROM
  [companies] INNER JOIN [cities] ON [cities].[id] = [companies].[city_id] 
  WHERE [companies].[code] IN (''12B1'', ''34C8'', ''87DD'', ''9AA2'')'

现在,奇怪的部分:在控制台上,它返回的公司对象数组不会显示已加入的城市,但它们就在那里!我的意思是,如果我做result.first.attributes,它会列出公司和加入城市的所有属性

然后,我可以访问返回数组中的“不可见”属性:

> result = Company.get_companies_by_code( ['12B1', '34C8', '87DD'] )
> result.first.city_name
> "Atlanta"

我只能想象如果碰撞碰撞名称会发生​​什么......

更多“Rails”方法是手动设置关联:

def self.get_companies_by_code(code_array)
  sql_string = Company.where(company_code: code_array).to_sql 
  sql_string = sql_string.gsub("N'","'")  #remove unicode prefix
  companies = find_by_sql(sql_string)  

  cities_ids_array = companies.map(&:city_id)
  cities_array = City.where(:id => cities_ids_array)
  cities_hash = cities_array.group_by {|c| c.id}

  companies.each {|comp|
       association = comp.association(:city)
       association.target = cities_hash[comp.city_id]
  }
  companies
end

一些参考文献:

http://blog.bigbinary.com/2013/07/01/preload-vs-eager-load-vs-joins-vs-includes.html

https://mrbrdo.wordpress.com/2013/09/25/manually-preloading-associations-in-rails-using-custom-scopessql

http://blog.arkency.com/2013/12/rails4-preloading/