用于识别孤立子记录的 ActiveRecord 查询

ActiveRecord Query to identify orphan child records

我有 Post 和用户模型。每个 post 属于一个用户。但是,在数据库导入期间,在某些 post 上输入了一些错误的 user_ids。获取 post 的 user_ids 不引用任何用户的查询是什么?谢谢。

我不认为你可以用直接的 AR 来做到这一点,但它很容易修复 Ruby:

Post.find_each { |p| p.delete if p.user.nil? }

编辑:忘了 .all 不 return ActiveRecord::Relation

我会执行以下操作,这将导致一个 SELECT 和一个 DELETE 语句(因此总共有 2 个查询)

Post.where('`posts`.`user_id` NOT IN (?)', User.pluck(:id)).delete_all

谢谢你们。我的解决方案类似于 Manuel 的

all_user_ids = User.all.pluck(:id)
unwanted_posts = Post.where.not(:user_id => all_user_ids)

然后我可以摧毁所有unwanted_posts。当然,其他解决方案也可以。

桑杰,

所有建议的解决方案都适用于小表,但根据所涉及表的大小、内存量和可用处理能力,出于性能原因,您可能需要使用 LEFT OUTER JOIN,例如这个:

Post.joins("LEFT OUTER JOIN users ON posts.user_id = user.id")
    .where("user.id IS NULL")

在Rails5中,有support for LEFT OUTER JOIN in ActiveRecord.

此致

正如@user2553863 已经提到的,Rails 5 添加了对 left_outer_joins 的支持,这意味着您现在可以高效地执行此操作,而无需像这样编写任何 SQL:

Post.left_outer_joins(:user).where(users: {id: nil}).delete_all

这将找到所有孤立的帖子(没有用户的帖子)并将其删除。这里,user是关联名,users是加入的table的名字。您不必触发额外的 SELECT 来查询所有用户 ID,当您有很多用户时,这可能会中断。

注意:以下答案适用于 Rails 5.0

其中许多答案适用于少数记录或小型表格,但根本无法很好地扩展到拥有大量孤立记录或处理大型表格时。

例如,处理两个较大的表,其中 ModelOne 有 707,891 条孤立记录:

irb(main):032:0> ModelOne.count
=> 2,265,216
irb(main):033:0> ModelTwo.count
=> 5,109,186

尝试使用 NOT IN 执行查询会失败,因为它太大了:

irb(main):029:0> ModelOne.where.not(model_two_id: ModelTwo.pluck(:id))
ActiveRecord::StatementInvalid (Mysql2::Error: MySQL server has gone away: SELECT `model_ones`.* FROM `model_ones` WHERE (`model_ones`.`model_two_id` NOT IN (12068663, 12076647, 12076648, 12082392, 12082393, 12082394, <repeat for the other 5 million ModelTwo records>))

此外,尝试在使用 left_outer_joins 的查询上调用 .delete_all 并不像预期的那样有效。

这是 SQL rails 为 ModelOne.left_outer_joins(:model_two).where(model_twos: {id: nil}) 生成的:

SELECT `model_ones`.* FROM `model_ones`
LEFT OUTER JOIN `model_twos` ON `model_twos`.`id` = `model_ones`.`model_two_id`
WHERE `model_twos`.`id` IS NULL

但是将 .delete_all 链接到末尾 (ModelOne.left_outer_joins(:model_two).where(model_twos: {id: nil}).delete_all) 会生成:

DELETE FROM `model_ones` WHERE `model_twos`.`id` IS NULL

这会引发错误。

我发现删除孤立记录的最高效方法来自 并使用 SQL EXISTS 和嵌套查询来有效地查找和删除孤立记录。

ModelOne.where.not(
  ModelTwo.where('model_twos.id = model_ones.model_two_id').exists
)

生成:

SELECT `model_ones`.* FROM `model_ones`
WHERE (
  NOT (
    EXISTS (
      SELECT `model_twos`.* FROM `model_twos` WHERE (model_twos.id = model_ones.model_two_id)
    )
  )
)

使用此查询加载 707,891 条孤立记录只需不到一分钟:

irb(main):040:0> Benchmark.measure { ModelOne.where.not(ModelTwo.where('model_twos.id = model_ones.model_two_id').exists).load }
=> #<Benchmark::Tms:0x0000563cfa227580 @label="", @real=59.61208474007435, @cstime=0.0, @cutime=0.0, @stime=0.23068100000000014, @utime=49.025859000000025, @total=49.25654000000002>

链接 .delete_all 到此查询将按预期工作并删除所有孤立记录

ModelOne.where.not(ModelTwo.where('model_twos.id = model_ones.model_two_id').exists).delete_all

生成 SQL:

  DELETE FROM `model_ones` WHERE (NOT (EXISTS (SELECT `model_twos`.* FROM `model_twos` WHERE (model_twos.id = model_ones.model_two_id))))

RAILS 6.1+

您可以使用 'missing' 方法来获取孤立的记录。例如

Class User
end

Class Post
  belongs_to :user
end

这里是使用 missing 方法的时候

Post.where.missing(:user)

这将获取具有 user_id 但相应用户被删除的所有 Post 记录。