优化 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 行。
问题:
- 根据 Rails 使用 ActiveRecord 的连接值从 一个或多个表 中删除记录的最佳方法是什么?
- 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 的推荐对解决这个问题非常有帮助。
本质上,我们需要删除 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 行。
问题:
- 根据 Rails 使用 ActiveRecord 的连接值从 一个或多个表 中删除记录的最佳方法是什么?
- 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 的推荐对解决这个问题非常有帮助。