为什么ActiveRecord destroy_all需要这么长时间?

时间:2013-05-21 22:37:01

标签: mysql performance activerecord ruby-on-rails-3.2

我有一个简单的rails应用程序,在MySQL 5.5,Ruby 1.9.3和rails 3.2.12上运行文章和评论:

class Article < ActiveRecord::Base                                                                                   
  attr_accessible :body, :title
  has_many :comments
end   

class Comment < ActiveRecord::Base
  attr_accessible :content
  belongs_to :article
end

我为一篇文章制作了很多评论,现在我试图在rails控制台中删除它们:

$ rails c 
Loading development environment (Rails 3.2.12)
[1] pry(main)> a = Article.find(1)
   (2.0ms)  SET SQL_AUTO_IS_NULL=0
  Article Load (8.0ms)  SELECT `articles`.* FROM `articles` WHERE `articles`.`id` = 1 LIMIT 1
=> #<Article id: 1, title: "Test", body: "---\n- Est vel provident. Laboriosam dolor asperiore...", created_at: "2013-05-17 09:54:54", updated_at: "2013-05-21 14:52:18">
[2] pry(main)> require 'benchmark'
[3] pry(main)> puts Benchmark.measure { a.comments.destroy_all }
  Comment Load (896.0ms)  SELECT `comments`.* FROM `comments` WHERE `comments`.`article_id` = 1
  EXPLAIN (2.0ms)  EXPLAIN SELECT `comments`.* FROM `comments` WHERE `comments`.`article_id` = 1
EXPLAIN for: SELECT `comments`.* FROM `comments`  WHERE `comments`.`article_id` = 1
+----+-------------+----------+------+---------------+------------+---------+-------+-------+-------------+
| id | select_type | table    | type | possible_keys | key        | key_len | ref   | rows  | Extra       |
+----+-------------+----------+------+---------------+------------+---------+-------+-------+-------------+
|  1 | SIMPLE      | comments | ref  | article_id    | article_id | 5       | const | 48186 | Using where |
+----+-------------+----------+------+---------------+------------+---------+-------+-------+-------------+
1 row in set (0.00 sec)

  SQL (1.0ms)  DELETE FROM `comments` WHERE `comments`.`id` = 2
  SQL (2.0ms)  DELETE FROM `comments` WHERE `comments`.`id` = 3
  SQL (1.0ms)  DELETE FROM `comments` WHERE `comments`.`id` = 4
  SQL (1.0ms)  DELETE FROM `comments` WHERE `comments`.`id` = 5
  SQL (1.0ms)  DELETE FROM `comments` WHERE `comments`.`id` = 6
  SQL (5.0ms)  DELETE FROM `comments` WHERE `comments`.`id` = 7
  SQL (2.0ms)  DELETE FROM `comments` WHERE `comments`.`id` = 8
  SQL (2.0ms)  DELETE FROM `comments` WHERE `comments`.`id` = 9

. . .
  SQL (0.0ms)  DELETE FROM `comments` WHERE `comments`.`id` = 37360
  SQL (0.0ms)  DELETE FROM `comments` WHERE `comments`.`id` = 37361

最后一个查询是删除最后一条评论,然后在最终返回并提交之前,该过程会暂停非常很长时间:

   (1.9ms)  COMMIT
690.380000   1.390000 691.770000 (693.885877)

SHOW PROCESSLIST确认没有锁定:

mysql> show processlist;
+----+----------+-----------+------------------+---------+------+-------+------------------+
| Id | User     | Host      | db               | Command | Time | State | Info             |
+----+----------+-----------+------------------+---------+------+-------+------------------+
|  6 | bloguser | localhost | blog_development | Query   |    0 | NULL  | show processlist |
|  7 | bloguser | localhost | blog_development | Sleep   |  459 |       | NULL             |
+----+----------+-----------+------------------+---------+------+-------+------------------+
2 rows in set (0.00 sec)
带有delete_alldependent: :destroy

dependent: :delete_all表现出非常相似的行为。

流行的看法似乎是destroy_all的问题是它实例化所有对象并逐个删除它们,但它看起来不像这里的问题。在执行了所有DELETE之后,在最终调用COMMIT之前需要花费这么长的时间来处理什么?

3 个答案:

答案 0 :(得分:1)

深入研究这一点似乎是comments数组的删除需要很长时间。然后,从阵列here中删除已删除的记录。

使用大型数组进行模拟,我们会得到相同的缓慢行为:

1.9.3-p194 :001 > require 'benchmark'; require 'ostruct'
 => true 
1.9.3-p194 :002 > i = 0; a = []
 => [] 
1.9.3-p194 :003 > 35_000.times { i+=1; a << OpenStruct.new(value: i) }
 => 35000 
1.9.3-p194 :004 > puts Benchmark.measure { a.each { |i| a.delete(i) } }
623.560000   0.820000 624.380000 (625.244664)

如果Array#clear ...

,可能会优化ActiveRecord以执行destroy_all

答案 1 :(得分:0)

请注意#destroy_all实例化对象的每个实例,然后运行并删除它。这可能需要一段时间,这就是为什么你得到所有那些不同的DELETE语句而不是单个语句。你可能想要的是delete_all

Comment.delete_all("article_id = 1")

我知道你已经提到了实例化问题,但是并排尝试两种不同的方法 - 我认为你会看到差异。

上面重要的部分,虽然你没有通过关联这样做,但请注意我提供的代码不会这样做:

Article.find(1).comments.delete_all

直接从评论中调用。这确保您没有实例化对象。通过关联代理调用delete_all可以导致事物被实例化。如果它们被实例化,你通常会在删除/销毁它们时获得回调 - 更不用说ruby必须在内存中集合中的对象进行随机播放。

时间的原因是ruby处理一个包含35k复杂关联对象的数组。同时,请注意35k删除语句。包含在交易中的35,000条删除语句仍然需要很长时间。

答案 2 :(得分:0)

除了destroy_all首先实例化所有行之外,这听起来像提交回调后的activerecord。

当您更新/删除事务中的行时,activerecord会跟踪您修改过的所有行,以便它可以调用任何定义的提交挂钩(即使没有)。在过去,我发现当涉及大量记录时(几千个),这种簿记可能会非常缓慢。这个命中就像rails提交事务一样。

如果我的内存是正确的,那么缓慢的罪魁祸首是rails在更改的对象数组上调用uniq==hash如何实施的详细信息似乎在某些情况下会变慢

在过去,我通过

蹒跚而行
class Foo  < ActiveRecord::Base
  #hobble commit hooks
  def add_to_transaction
  end
end

当然会破坏提交回调(你可能还没有使用)