Запрос для возврата одной записи, когда сводная таблица содержит ровно одну запись для каждого идентификатора в предложении IN

#sql #postgresql

#sql #postgresql

Вопрос:

У меня есть три таблицы:

Пользователи:

 id       username
-----------------
1        user1                    
2        user2                    
3        user3
  

чаты:

 id      created_on
---------------------------
1       2020-11-07 00:00:00
2       2020-11-08 00:00:00
  

chat_users (сводная таблица):

 id      chat_id      user_id
----------------------------
1       1            1
2       1            2
3       2            1
4       2            2
5       2            3
  

Учитывая массив идентификаторов пользователей, я хочу вернуть одну chat запись или пустой результат.
Примеры:

  1. Учитывая идентификаторы пользователей (1, 2), верните строку чата с идентификатором 1.
  2. Учитывая идентификаторы пользователей (1, 3), верните пустой результирующий набор.
  3. Учитывая идентификаторы пользователей (1, 2, 3), верните строку чата с идентификатором 2.

Я пробовал что-то вроде:

 select distinct c.*
from chats as c
  join chat_users as cu on cu.user_id in (...)
;
  

но я знаю, что это неправильно, и это не работает для точного сопоставления сводных записей.

Заранее благодарю вас — любая помощь очень ценится, я рисую пробел.

Обновить

Кажется, это работает, но у меня такое чувство, что это не идеальное решение:

 with filtered_chat_users as (
    select cu.chat_id, string_agg(cu.user_id::text, ',' order by cu.user_id asc) as user_ids
    from chat_users as cu 
    where cu.user_id in (1, 2, 3)
    group by cu.chat_id
)
select c.*
from filtered_chat_users as fcu
    join chats as c on c.id = fcu.chat_id
where fcu.user_ids = '1,2,3';
  

UDPATE 2:
Вышеприведенное работает не так, как я думал / надеялся. Передача подмножества пользовательских идентификаторов неправильно совпадает с записями чата: передача (1, 2) возвращает оба чата 1 и 2.

ОБНОВЛЕНИЕ 3:

Это работает, но снова интересно, есть ли более эффективное решение:

 with filtered_chats as (
    select distinct c.id
    from chats as c
        join chat_users as cu on cu.chat_id = c.id
            and cu.user_id in (1, 2)
), filtered_chat_users as (
    select cu.chat_id, string_agg(cu.user_id::text, ',' order by cu.user_id asc) as user_ids
    from chat_users as cu
        join filtered_chats as fc on fc.id = cu.chat_id
    group by cu.chat_id
)
select c.*
from filtered_chat_users as fcu
    join chats as c on c.id = fcu.chat_id
where fcu.user_ids = '1,2'
  

Ответ №1:

Вы можете использовать условную агрегацию:

 select chat_id
from chat_users cu
group by chat_id
having count(*) filter (user_id in (1, 2, 3)) = count(*) and
       count(*) = 3;
  

Список и «3» необходимо изменить в зависимости от того, что вы ищете.

Это проверяет, что все указанные пользователи находятся в чате. = 3 Проверяет, что там есть только эти пользователи.