sqlpostgresqlrelational-divisionsql-match-all

Joined elements should *all* match some criteria


I have a setup like:

conversations(id) 

notifications(id, conversation_id, user_id)

users(id)

If a conversation concerns someone, the user is linked to the conversation through one notification.

GOAL: I'm looking for conversations which concern exclusively users with given ids.


The obvious:

SELECT DISTINCT conversations.* 
FROM "conversations" 
INNER JOIN "notifications" ON "notifications"."conversation_id" = "conversations"."id"
WHERE notifications.user_id IN (1, 2)

doesn't work because it would also retrieve a conversation concerning:

and this is not desired.


Solution

  • This is assuming that each user can only be joined once to a conversation:

    SELECT c.*
    FROM   conversations c
    JOIN   notifications n1 ON n1.conversation_id = c.id
    JOIN   notifications n2 ON n2.conversation_id = c.id
    WHERE  n1.user_id = 1
    AND    n2.user_id = 2
    AND    NOT EXISTS (
       SELECT 1
       FROM   notifications n3
       WHERE  n3.conversation_id = c.id
       AND    n3.user_id <> ALL ('{1,2}')
      )
    

    This is a special case of relational division. We have assembled a whole arsenal of techniques under this this related question:
    How to filter SQL results in a has-many-through relation

    The special requirement is to exclude additional matches. I use NOT EXISTS for this end. You could also use LEFT JOIN / IS NULL or NOT IN. More details here:
    Select rows which are not present in other table

    There could be several notifications for the same user

    To avoid duplicate conversations in the result you can use DISTINCT or GROUP BY in addition to the above. Or you suppress duplicates to begin with (probably faster):

    SELECT c.*
    FROM   conversations c
    WHERE  EXISTS (
       SELECT 1
       FROM   notifications n1
       JOIN   notifications n2 USING (conversation_id)
       WHERE  n1.conversation_id = c.id
       AND    n1.user_id = 1
       AND    n2.user_id = 2
      )
    AND    NOT EXISTS (
       SELECT 1
       FROM   notifications n3
       WHERE  n3.conversation_id = c.id
       AND    n3.user_id <> ALL ('{1,2}')
      )