SQL-запрос для поиска строки с определенным количеством ассоциаций - PullRequest
0 голосов
/ 10 ноября 2018

Использование Postgres У меня есть схема, которая имеет conversations и conversationUsers. Каждый conversation имеет много conversationUsers. Я хочу быть в состоянии найти разговор, который имеет точно указанное число conversationUsers. Другими словами, при условии массива userIds (скажем, [1, 4, 6]) я хочу иметь возможность найти разговор, который содержит только этих пользователей, и не более.

Пока я пробовал это:

SELECT c."conversationId"
FROM "conversationUsers" c
WHERE c."userId" IN (1, 4)
GROUP BY c."conversationId"
HAVING COUNT(c."userId") = 2;

К сожалению, это также, кажется, возвращает разговоры, которые включают этих 2 пользователей среди других. (Например, он возвращает результат, если разговор также включает "userId" 5).

Ответы [ 3 ]

0 голосов
/ 10 ноября 2018

Это может быть легче следовать. Вы хотите идентификатор разговора, группировать по нему. добавьте предложение HAVING, основанное на сумме совпадений идентификаторов пользователей, равных всем возможным в группе. Это будет работать, но будет дольше обрабатываться из-за отсутствия предварительного квалификатора.

select
      cu.ConversationId
   from
      conversationUsers cu
   group by
      cu.ConversationID
   having 
      sum( case when cu.userId IN (1, 4) then 1 else 0 end ) = count( distinct cu.UserID )

Чтобы еще больше упростить список, подготовьте предварительный запрос к разговорам, в которых участвует хотя бы один человек ... Если они не находятся в начале, зачем беспокоиться о рассмотрении таких других разговоров.

select
      cu.ConversationId
   from
      ( select cu2.ConversationID
           from conversationUsers cu2
           where cu2.userID = 4 ) preQual
      JOIN conversationUsers cu
         preQual.ConversationId = cu.ConversationId
   group by
      cu.ConversationID
   having 
      sum( case when cu.userId IN (1, 4) then 1 else 0 end ) = count( distinct cu.UserID )
0 голосов
/ 10 ноября 2018

Это случай - с добавленным специальным требованием, чтобы в этом же разговоре не было дополнительных пользователей.

Предполагая, что - это PK таблицы "conversationUsers", которая обеспечивает уникальность комбинаций NOT NULL, а также обеспечивает индекс, неявно необходимый для производительности. Столбцы многоколоночного ПК в этом порядке! Иначе ты должен сделать больше.
О порядке столбцов индекса:

Для базового запроса используется "грубая сила" , позволяющая подсчитать количество подходящих пользователей для всех разговоров всех заданных пользователей, а затем отфильтровать те, которые соответствуют всем заданным пользователи. Хорошо для небольших таблиц и / или только коротких входных массивов и / или нескольких разговоров на пользователя, но плохо масштабируется :

SELECT "conversationId"
FROM   "conversationUsers" c
WHERE  "userId" = ANY ('{1,4,6}'::int[])
GROUP  BY 1
HAVING count(*) = array_length('{1,4,6}'::int[], 1)
AND    NOT EXISTS (
   SELECT FROM "conversationUsers"
   WHERE  "conversationId" = c."conversationId"
   AND    "userId" <> ALL('{1,4,6}'::int[])
   );

Устранение разговоров с дополнительными пользователями с помощью NOT EXISTS anti-semi-join. Подробнее:

Альтернативные методы:

Существуют и другие (намного) более быстрые запросов. Но самые быстрые из них плохо подходят для динамического количества идентификаторов пользователей.

Для быстрого запроса , который также может работать с динамическим числом идентификаторов пользователей, рассмотрим рекурсивный CTE :

WITH RECURSIVE rcte AS (
   SELECT "conversationId", 1 AS idx
   FROM   "conversationUsers"
   WHERE  "userId" = ('{1,4,6}'::int[])[1]

   UNION ALL
   SELECT c."conversationId", r.idx + 1
   FROM   rcte                r
   JOIN   "conversationUsers" c USING ("conversationId")
   WHERE  c."userId" = ('{1,4,6}'::int[])[idx + 1]
   )
SELECT "conversationId"
FROM   rcte r
WHERE  idx = array_length(('{1,4,6}'::int[]), 1)
AND    NOT EXISTS (
   SELECT FROM "conversationUsers"
   WHERE  "conversationId" = r."conversationId"
   AND    "userId" <> ALL('{1,4,6}'::int[])
   );

Для простоты использования оберните это в функцию или подготовленный оператор . Как:

PREPARE conversations(int[]) AS
WITH RECURSIVE rcte AS (
   SELECT "conversationId", 1 AS idx
   FROM   "conversationUsers"
   WHERE  "userId" = $1[1]

   UNION ALL
   SELECT c."conversationId", r.idx + 1
   FROM   rcte                r
   JOIN   "conversationUsers" c USING ("conversationId")
   WHERE  c."userId" = $1[idx + 1]
   )
SELECT "conversationId"
FROM   rcte r
WHERE  idx = array_length($1, 1)
AND    NOT EXISTS (
   SELECT FROM "conversationUsers"
   WHERE  "conversationId" = r."conversationId"
   AND    "userId" <> ALL($1);

Звоните:

EXECUTE conversations('{1,4,6}');

дБ <> скрипка здесь (также демонстрирует функцию )

Есть еще возможности для улучшения: чтобы добиться производительности top , вы должны поставить пользователей с наименьшим количеством разговоров на первом месте во входном массиве, чтобы исключить как можно больше строк на ранней стадии. Для достижения максимальной производительности вы можете динамически генерировать нединамический, нерекурсивный запрос (используя один из методов fast из первой ссылки) и выполнять его по очереди. Вы даже можете обернуть его в одну функцию plpgsql с динамическим SQL ...

Более подробное объяснение:

Альтернатива: MV для редко написанной таблицы

Если таблица "conversationUsers" в основном доступна только для чтения (старые разговоры вряд ли изменятся), вы можете использовать MATERIALIZED VIEW с предварительно агрегированными пользователями в отсортированных массивах и создать простой индекс btree на этот столбец массива.

CREATE MATERIALIZED VIEW mv_conversation_users AS
SELECT "conversationId", array_agg("userId") AS users  -- sorted array
FROM (
   SELECT "conversationId", "userId"
   FROM   "conversationUsers"
   ORDER  BY 1, 2
   ) sub
GROUP  BY 1
ORDER  BY 1;

CREATE INDEX ON mv_conversation_users (users) INCLUDE ("conversationId");

Для демонстрируемого индекса покрытия требуется Postgres 11. См .:

О сортировке строк в подзапросе:

В старых версиях используйте простой многоколонный индекс для (users, "conversationId"). Для очень длинных массивов индекс хеша может иметь смысл в Postgres 10 или более поздних версиях.

Тогда гораздо более быстрый запрос будет просто:

SELECT "conversationId"
FROM   mv_conversation_users c
WHERE  users = '{1,4,6}'::int[];  -- sorted array!

дБ <> скрипка здесь

Вы должны сопоставить дополнительные расходы на хранение, запись и обслуживание с преимуществами для повышения производительности чтения.

В сторону: рассмотрим юридические идентификаторы без двойных кавычек. conversation_id вместо "conversationId" и т. Д.:

0 голосов
/ 10 ноября 2018

Вы можете изменить свой запрос следующим образом, и он должен работать:

SELECT c."conversationId"
FROM "conversationUsers" c
WHERE c."conversationId" IN (
    SELECT DISTINCT c1."conversationId"
    FROM "conversationUsers" c1
    WHERE c1."userId" IN (1, 4)
    )
GROUP BY c."conversationId"
HAVING COUNT(DISTINCT c."userId") = 2;
...