【问题标题】:Rails has_and_belongs_to_many find unique objects in commonRails has_and_belongs_to_many 找到共同的独特对象
【发布时间】:2013-08-10 15:12:38
【问题描述】:

我有两个模型,Conversation 和 Phones,它们都有_and_belongs_to_many。电话可以有很多对话,对话可以有很多电话(两个或更多)。

class Conversation < ActiveRecord::Base
  has_and_belongs_to_many :phones
end

class Phone < ActiveRecord::Base
  has_and_belongs_to_many :conversations
end

当然,还有一个 conversations_phones 连接表。

如果我有两个或更多电话对象,我如何找到它们共享的所有对话的列表?问题:对话不能包含任何其他电话(即电话 ID 的数量等于我们搜索的号码)。

我已经能够使用纯 Rails 做到这一点,但它涉及循环每个对话并依赖数据库。不好。

我不介意做纯 SQL;使用模型 ID 应该有助于阻止注入攻击。

我最接近的是:

SELECT conversations.* FROM conversations 
INNER JOIN conversations_phones AS t0_r0 ON conversations.id = t0_r0.conversation_id 
INNER JOIN conversations_phones AS t0_r1 ON conversations.id = t0_r1.conversation_id 
WHERE (t0_r0.phone_id = ? AND t0_r1.phone_id = ?), @phone_from.id, @phone_to.id

但它包括与外部电话的对话。我觉得 GROUP BY 和 HAVING COUNT 会有所帮助,我对 SQL 太陌生了。

【问题讨论】:

    标签: sql ruby-on-rails postgresql relational-division


    【解决方案1】:

    我想你快到了。只需通过额外的NOT EXISTS anti-semi-join 排除与外人的对话:

    SELECT c.*
    FROM   conversations c
    JOIN   conversations_phones AS cp1 ON cp1.conversation_id = c.id
                                      AND cp1.phone_id = ?
    JOIN   conversations_phones AS cp2 ON cp2.conversation_id = c.id
                                      AND cp2.phone_id = ?
    ...
    WHERE NOT EXISTS (
       SELECT 1
       FROM   conversations_phones cp
       WHERE  cp.conversation_id = c.id
       AND    cp.phone_id NOT IN (cp1.phone_id, cp2.phone_id, ...) -- or repeat param
       )
    , @phone1.id, @phone2.id, ...
    

    为简单起见,我将条件放入 JOIN 子句中,不会更改查询计划。
    不用说您需要conversations(id)conversations_phones(conversation_id, phone_id) 上的索引

    替代方案(慢得多):

    非常简单,但是很慢:

    SELECT cp.conversation_id
    FROM  (
       SELECT conversation_id, phone_id
       FROM   conversations_phones
       ORDER  BY 1,2
       ) cp
    GROUP  BY 1
    HAVING array_agg(phone_id) = ?
    

    .. 其中? 是一个排序后的 id 数组,例如 '{559,12801}'::int[]

    慢 30 倍在快速测试中。

    为了完整起见,(简化的)提议的 alternative by @BroiSatse in the comments 在类似的快速测试中执行大约 20 倍

    ...
    JOIN (
       SELECT conversation_id, COUNT(*) AS phone_count
       FROM   conversations_phones
       GROUP  BY prod_id
       ) AS pc ON pc.conversation_id = c.id AND phone_count = 2
    

    或者,更简单更快:

    ...
    JOIN (
       SELECT conversation_id
       FROM   conversations_phones
       GROUP  BY prod_id
       HAVING COUNT(*) = 2
       ) AS pc ON pc.conversation_id = c.id
    

    【讨论】:

    • 小心 N+1,它可能会非常慢。
    • @BroiSatse:我不太确定我是否遵循。对于满手的手机,这应该非常好。比较this related question下的关系除法算法
    • EXISTS 将导致每次对话的额外查询。然而,这可以通过以下方式完成:JOIN (SELECT dual.conversation_id conversation_Id, COUNT(*) phone_count FROM conversations_phones dual GROUP BY dual.id) AS phone_counter ON conversation.conversation_id = phone_counter.id AND phone_count = 2
    • @BroiSatse:这看起来像 Oracle 代码。也许您的替代方案在 Oracle 中更快。你用 PostgreSQL 测试过吗?我做到了,我的答案比您提出的替代方案快约 20 倍。所有相关指标均已到位。正如您可以在此处找到的许多相关答案(有些带有基准)所示,我对相关案例进行了相当多的测试。
    • 嘘!这很疯狂,但它有效。我怀疑我的模型是否有任何“快速”的方法。我也愿意接受 db 建议。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-05-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-01-14
    • 1970-01-01
    相关资源
    最近更新 更多