I have two models, Conversation and Phones, both of which has_and_belongs_to_many each other. Phones can have a lot of conversations, and conversations can have a lot of phones (two or more).

class Conversation < ActiveRecord::Base
  has_and_belongs_to_many :phones
end

class Phone < ActiveRecord::Base
  has_and_belongs_to_many :conversations
end

Of course, there's a conversations_phones join table as well.

If I have two or more phone objects, how do I find a list of all the conversations they share? The catch: the conversations can't include any other phones (IE the number of phone IDs equals the number we search with).

I've been able to do it with pure Rails, but it involves looping every conversation and counting on the db. Not good.

I don't mind doing pure SQL; using the model IDs should help stop injection attacks.

The closest I've come is:

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

But it includes conversations with outside phones. I have a feeling GROUP BY and HAVING COUNT would help, I'm just too new to SQL.

有帮助吗?

解决方案

I think you were almost there. Just exclude conversations with outsiders with an additional 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, ...

I pulled conditions into the JOIN clause for simplicity, doesn't change the query plan.
Goes without saying that you need indices on conversations(id) and conversations_phones(conversation_id, phone_id).

Alternatives (much slower):

Very simple, but slow:

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) = ?

.. where ? is a sorted array of ids like '{559,12801}'::int[]

30x slower in a quick test.

For completeness, the (simplified) proposed alternative by @BroiSatse in the comments performs around 20x slower in a similar quick test:

...
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

Or, slightly simpler and faster:

...
JOIN (
   SELECT conversation_id
   FROM   conversations_phones
   GROUP  BY prod_id
   HAVING COUNT(*) = 2
   ) AS pc ON pc.conversation_id = c.id
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top