Rails + ActiveRecord - 与“N + 1”问题作斗争

时间:2014-12-14 00:47:02

标签: ruby-on-rails ruby join activerecord has-many

我有这些模特:

class Car < ActiveRecord::Base
  has_many :car_services, dependent: :destroy
  has_many :services, through: :car_services
end

car_service

class CarService < ActiveRecord::Base
  belongs_to :car
  belongs_to :service
  validates_uniqueness_of :service_id, scope: :car_id
end

卡车

class Truck < ActiveRecord::Base 
  has_many :truck_services, dependent: :destroy
  has_many :services, through: :truck_services
end

truck_service

class TruckService < ActiveRecord::Base
  belongs_to :truck
  belongs_to :service
end

服务

class Service < ActiveRecord::Base
  has_many :car_services
  has_many :cars, through: :car_services

  has_many :truck_services
  has_many :trucks, through: :truck_services
end

在控制器的动作中(汽车和卡车有不同的动作)我查询模型如下:

@cars = Car.find_by_sql("SELECT ...")
...
@trucks = Truck.find_by_sql("SELECT ...")

然后在各自的观点中:

<% @cars.each do |result| %>
  <li data-services="<% result.services.each do |service| %><%= service.name %>,<% end %>">...</li>
  ...
<% end %>

此程序适用于cars,速度非常快。

trucks的不同操作,但程序相同:

<% @trucks.each do |result| %>
  <li data-services="<% result.services.each do |service| %><%= service.name %>,<% end %>">...</li>
  ...
<% end %>

现在,这个程序非常慢。当我试图调试原因时(因为正如我所提到的,两个模型的程序相同,但对于汽车而言,即使在car模型中的记录是600k记录和truck中,它的工作也非常快)只有“300k”,我在控制台日志中发现以下信息:

...
Service Load (159.1ms)  SELECT `services`.* FROM `services` INNER JOIN `truck_services` ON `services`.`id` = `truck_services`.`service_id` WHERE `truck_services`.`truck_id` = 35769
Service Load (166.8ms)  SELECT `services`.* FROM `services` INNER JOIN `truck_services` ON `services`.`id` = `truck_services`.`service_id` WHERE `truck_services`.`truck_id` = 45681
Service Load (151.4ms)  SELECT `services`.* FROM `services` INNER JOIN `truck_services` ON `services`.`id` = `truck_services`.`service_id` WHERE `truck_services`.`truck_id` = 50974
...

这怎么可能?为什么我还没有看到cars的这些行,我看到truck的这些行?我错过了什么吗?我在考虑MySQL表中缺少索引,但没有使用任何索引。

我整个星期六都在和这个问题作斗争,但我没有解决这个问题......

我非常感谢有关如何解决此问题的建议。

非常感谢你们。

1 个答案:

答案 0 :(得分:1)

我将从建议开始:发布您的sql查询以便其他人了解您正在做什么并能够帮助您

要消除N + 1,您应该使用includespreload

在您的控制器中执行以下操作:

@cars = Car.includes(:services).all
分别

@trucks = Truck.includes(:services).all

你可以看到我更喜欢find_by_sql更清晰的语法。

当你跑步时:

@cars = Car.includes(:services).all

这将触发3个查询:

SELECT "cars".* FROM "cars";
SELECT "car_services".* FROM "car_services" WHERE "car_services".car_id IN (?, ?, ? ...);
SELECT "services".* FROM "services" WHERE "services".car_service_id IN (?, ?, ? ...);

如果您删除N + 1,您的页面加载速度会更快,但您应该检查所有密钥是否也已定义索引。

以上只是一个例子。您应该为我们发布完整的查询,以便准确了解您在做什么。而那些型号Car和Truck,它们看起来非常相似,也许它将是使用Single Table Inheritance的更好的解决方案。

你可以改写这个:

<% @trucks.each do |result| %>
  <li data-services="<% result.services.each do |service| %><%= service.name %>,<% end %>">...</li>
<% end %>

这个

<% @trucks.each do |truck| %>
  <%= content_tag :li, '...', data: {services: truck.services.map(&:name).join(', ')} %>
<% end %>

希望这会以某种方式帮助你。