Это случай реляционного деления - с добавленным специальным требованием, чтобы в этом же разговоре не было дополнительных пользователей.
Предполагая, что - это 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"
и т. Д.: