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