SQL LEFT JOIN 值不在任一连接列中

SQL LEFT JOIN value NOT in either join column

我怀疑这是一个相当普遍的场景,并且可能显示出我作为数据库开发人员的无能,但无论如何……

我有两个 table:ProfilesHiddenProfiles 以及 HiddenProfiles table 有两个相关的外键: profile_idhidden_​​profile_id 存储 ids 来自 个人资料 table.

如您所想,一个用户可以隐藏另一个用户(其中​​他的个人资料 ID 将是 profile_id HiddenProfiles table)或者他可以被另一个用户隐藏(其中他的个人资料 ID 将放在 hidden_​​profile_id 列)。同样,一个很常见的场景。

期望的结果:
我想在 Profiles 和 HiddenProfiles table 上进行连接(或者老实说,任何最有效的查询)以查找给定配置文件既没有隐藏也没有隐藏的所有配置文件。

在我的脑海里,我认为这会非常简单,但我提出的迭代似乎总是遗漏了一半的问题。最后,我得到了如下所示的内容:

SELECT "profiles".* FROM "profiles"
LEFT JOIN hidden_profiles hp1 on hp1.profile_id = profiles.id and (hp1.hidden_profile_id = 1)
LEFT JOIN hidden_profiles hp2 on hp2.hidden_profile_id = profiles.id and (hp2.profile_id = 1)
WHERE (hp1.hidden_profile_id is null) AND (hp2.profile_id is null)

不要误会我的意思,这 "works" 但在我内心深处,我觉得应该有更好的方法。如果实际上没有,我很乐意接受在这件事上比我更有智慧的人的回答。 :)

就其价值而言,这两个 RoR 模型位于 Postgres 数据库上,因此非常感谢针对这些限制量身定制的解决方案。


模型是这样的:

class Profile < ActiveRecord::Base
    ...
    has_many :hidden_profiles, dependent: :delete_all

    scope :not_hidden_to_me, -> (profile) { joins("LEFT JOIN hidden_profiles hp1 on hp1.profile_id = profiles.id and (hp1.hidden_profile_id = #{profile.id})").where("hp1.hidden_profile_id is null") }
    scope :not_hidden_by_me, -> (profile) { joins("LEFT JOIN hidden_profiles hp2 on hp2.hidden_profile_id = profiles.id and (hp2.profile_id = #{profile.id})").where("hp2.profile_id is null") }
    scope :not_hidden, -> (profile) { self.not_hidden_to_me(profile).not_hidden_by_me(profile) }
    ...
end

class HiddenProfile < ActiveRecord::Base
    belongs_to :profile
    belongs_to :hidden_profile, class_name: "Profile"
end

因此,为了获得我想要的配置文件,我正在执行以下操作:

Profile.not_hidden(given_profile)

再说一次,也许这很好,但如果有更好的方法,我会很乐意接受。

如果您只想为单个配置文件获取此列表,我将实现一个实例方法以在 ActiveRecord 中有效地执行相同的查询。我所做的唯一修改是对子查询的并集执行单个连接并将条件应用于子查询。这应该减少需要加载到内存中的列,并希望更快(您需要针对您的数据进行基准测试以确保):

class Profile < ActiveRecord::Base
  def visible_profiles
    Profile.joins("LEFT OUTER JOIN (
      SELECT profile_id p_id FROM hidden_profiles WHERE hidden_profile_id = #{id}
      UNION ALL
      SELECT hidden_profile_id p_id FROM hidden_profiles WHERE profile_id = #{id}
    ) hp ON hp.p_id = profiles.id").where("hp.p_id IS NULL")
  end
end

由于此方法 returns 一个 ActiveRecord 范围,如果需要,您可以链接其他条件:

Profile.find(1).visible_profiles.where("created_at > ?", Time.new(2015,1,1)).order(:name)

就我个人而言,我从来不喜欢 join = null 方法。我发现它违反直觉。您要求加入,然后将结果限制为不匹配的记录。

我会更接近它

SELECT id FROM profiles p
WHERE 
NOT EXISTS 
 (SELECT * FROM hidden_profiles hp1 
  WHERE hp1.hidden_profile_id = 1 and hp1.profile_id = p.profile_id)
AND
  NOT EXISTS (SELECT * FROM hidden_profiles hp2 
  WHERE hp2.hidden_profile_id = p.profile_id and hp2.profile_id = 1)

但是您将需要 运行 一些具有实际体积的 EXPLAIN 来确定哪个最有效。