【问题标题】:Rails + ActiveRecord - fighting with "N+1" issueRails + ActiveRecord - 解决“N+1”问题
【发布时间】:2015-02-12 10:29:49
【问题描述】:

我有这些模型:

汽车:

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

汽车服务

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

卡车服务

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 表中缺少索引,但没有使用任何索引。

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

如果您能提供解决此问题的建议,我将不胜感激。

非常感谢你们。

【问题讨论】:

  • 汽车版不会出现这些查询,还是只是更快?
  • 汽车和卡车的 SELECT ...(隐藏,但可能与问题最相关的一段代码)是否等效?
  • @FrederickCheung 它们不会发生,而且速度更快。
  • @numbers1311407 是的。逻辑是一样的。
  • 什么是shipper_id,为什么在 WHERE 子句中使用它?

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


【解决方案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 %>

希望这会对您有所帮助。

【讨论】:

    猜你喜欢
    • 2011-09-24
    • 1970-01-01
    • 1970-01-01
    • 2011-02-05
    • 2021-08-10
    • 2021-01-22
    • 2021-08-27
    • 1970-01-01
    • 2019-01-18
    相关资源
    最近更新 更多