优化 Rails 中的删除操作 4 / ActiveRecord / MySQL

Optimizing delete operations in Rails 4 / ActiveRecord / MySQL

本质上,我们需要删除 T1 条记录,这些记录在给定 @user 时没有关联 t3 条记录。虽然不需要 ,但最好也删除 T2 没有 T3 连接的记录。

这是被推送到生产环境的代码。显然,它很棒,因为它通过了单元测试(哈!)...除了它在生产中造成数百万行的锁定,导致服务器 500 死锁 (Mysql2::Error: Deadlock found when trying to get lock; try restarting transaction) 当多个用户同时点击DELETE查询时。 是的,索引就位:

T1.where(user_id: @user.id, enabled: true)
    .joins('LEFT JOIN t2 ON t2.t1_id = t1.id')
    .joins('LEFT JOIN t3 ON t3.id = t2.t3_id')
    .where('t3.id IS NULL').delete_all

结果SQL:

DELETE FROM `t1`
WHERE `t1`.`id` IN
(SELECT id FROM
     (SELECT `t1`.`id` FROM `t1`
      LEFT JOIN t2 ON t2.t1_id = t1.id
      LEFT JOIN t3 ON t3.id = t2.t3_id
      WHERE `t1`.`user_id` = 65987
      AND `t1`.`enabled` = 1
      AND (t2.id IS NULL)
     ) __active_record_temp
    );

我知道这是结果 SQL 的唯一原因是它包含在 Server 500 死锁错误中。 测试时我似乎无法在控制台中显示 delete_all 查询。 我能够获取查询输出并将其转换为 SELECT解释一下,这表明最外层的 select 扫描了数百万行(我相信这转化为 DELETE 操作的相同数量的行锁。)最内层的查询仅扫描 27 行。

问题:

  1. 根据 Rails 使用 ActiveRecord 的连接值从 一个或多个表 中删除记录的最佳方法是什么?
  2. review/test SQL 在 Rails 中的输出有哪些选项可以确保我不会面临性能不佳和死锁的风险?

更新:情节变厚了……添加当前关联

class User < ActiveRecord::Base
has_many :T1s
has_many :T2s

class T1 < ActiveRecord::Base
belongs_to :user

class T2Custom < ActiveRecord::Base
self.table_name = "t2"
has_many :T3s, :foreign_key => :t2_id

class T3 < ActiveRecord::Base
belongs_to :T2, foreign_key: "t2_id"
belongs_to :T1

1) 可以使用Active Record(https://apidock.com/rails/ActiveRecord/Batches/find_in_batches)find_in_batches方法删除记录,可配置

2) 您可以在测试中使用 Bullet Gem 来验证您没有 n+1 个查询(https://github.com/flyerhzm/bullet),但我不确定这是您的问题。

您可以使用 Active Record (https://apidock.com/rails/ActiveRecord/Relation/to_sql) .to_sql 函数来解释任何查询。

3) 如果您仍然需要帮助解决您的问题,您可以 post 一些更多的代码,也许是生成查询、关联等的代码。 https://whosebug.com/help/how-to-ask

使用 PRIMARY KEY 遍历主要 table。对于每一块,比如 1000 行,在单独的事务中执行 UPDATE

Details(这是为 DELETE 编写的,但可以针对 UPDATE 进行改编。)

如果不需要,请不要使用 LEFT

确保你有 suitable 索引以避免 table 扫描 JOINs.

如果其中任何 table 是 many:many,请遵循效率提示 here

作为快速(可能是永久性)修复,我决定将 .delete_all 替换为 .destroy_all,这基本上只会 运行 内部查询,扫描 27 行,然后实例化并删除一个接一个地记录,如果需要,还有 运行ning destroy 回调的额外优势。

LEFT JOIN 是为了查找此时由于其他表中的子记录从未创建而无效的记录。范围内的记录数不应超过 30。代码试图查找和删除不应存在且通常不存在的记录。这意味着,95% 的时间,片段将在(快速)初始查询返回空后退出。

运行 .to_sql ruby_newbie 的推荐对解决这个问题非常有帮助。