【问题标题】:Why is ActiveRecord destroy_all taking so long?为什么 ActiveRecord destroy_all 需要这么长时间?
【发布时间】:2013-05-21 22:37:00
【问题描述】:

我有一个简单的 Rails 应用程序,其中包含在 MySQL 5.5、Ruby 1.9.3 和 rails 3.2.12 上运行的文章和 cmets:

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

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

我为一篇文章生成了很多 cmets,现在正试图在 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: :destroydependent: :delete_all 表现出非常相似的行为。

普遍的看法似乎是destroy_all 的问题在于它实例化了所有对象并一个一个地删除它们,但看起来这不是问题所在。在所有DELETEs 都被执行之后,在COMMIT 最终被调用之前,需要这么长时间来处理什么?

【问题讨论】:

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


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

    ActiveRecord 可能会被优化为在 destroy_all 的情况下执行 Array#clear...

    【讨论】:

    • 查看我更新的答案,看看它是否与上述结合起来更清楚。
    【解决方案2】:

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

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

    如果我的记忆是正确的罪魁祸首,那么缓慢的一点是 rails 在更改的对象数组上调用uniq。在某些情况下,==hash 的实现方式的细节似乎让这个速度变慢了

    在过去,我一直在通过

    class Foo  < ActiveRecord::Base
      #hobble commit hooks
      def add_to_transaction
      end
    end
    

    这当然会破坏提交回调(无论如何你可能都不会使用)

    【讨论】:

    • 玩得更久一点,似乎并不是回调导致了延迟,而是记录从巨大的关联数组中删除的方式。
    【解决方案3】:

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

    Comment.delete_all("article_id = 1")
    

    我知道您已经提到了实例化问题,但请同时尝试这两种不同的方法 - 我想您会看到不同之处。

    虽然上面的重要部分是您没有通过关联来执行此操作,但请注意我提供的代码不这样做:

    Article.find(1).comments.delete_all
    

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

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

    【讨论】:

    • 如果您仔细阅读我的问题,我并不是在问多个DELETEs,而是在所有这些DELETEs 之后和COMMIT 之前实际发生了什么。正如我所说,协会上的delete_all 表现出相同的行为。 Comment.delete_all 显然会快得多,因为它不在关联上执行。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-08-31
    • 2011-08-27
    • 2011-12-07
    • 2021-11-27
    • 2015-07-19
    相关资源
    最近更新 更多