本质上,我们需要删除给定
T1
没有与之关联的 t3
记录的 @user
记录。虽然不是 必需,但最好也删除没有 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
,这显示最外面的选择扫描数百万行(我相信这会转化为 DELETE
操作的相同数量的行锁。)最里面的查询仅扫描 27 行。
问题:
更新:情节变得更加丰富...添加当前关联
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
您可以使用Active Record(https://apidock.com/rails/ActiveRecord/Batches/find_in_batches)
find_in_batches
方法来删除记录,并且它是可配置的。
您可以在测试中使用 Bullet Gem 来验证您没有 n+1 查询(https://github.com/flyerhzm/bullet),但我不确定这是您的问题。
您可以使用 Active Record (https://apidock.com/rails/ActiveRecord/Relation/to_sql)
.to_sql
函数来解释任何查询。
作为一个快速(可能是永久性的)修复,我决定将
.delete_all
替换为 .destroy_all
,这实际上只会运行内部查询,扫描 27 行,然后逐条实例化并删除记录,还有运行的额外优势destroy
如果需要回调。
LEFT JOIN
是查找此时由于其他表中的子记录从未创建而无效的记录。范围内的记录数不应超过 30 条。代码尝试查找并删除不应存在且通常不存在的记录。这意味着,95% 的情况下,代码片段将在(快速)初始查询返回空后退出。
按照 ruby_newbie 的建议运行
.to_sql
对于解决这个问题非常有帮助。