Este es un caso de relational-division - con el requisito especial añadido de que la misma conversación no tendrá adicionales usuarios.
Suponiendo es el PK de la tabla "conversationUsers"
que impone la unicidad de las combinaciones, NOT NULL
y también proporciona implícitamente el índice esencial para el rendimiento. Columnas del PK multicolumna en this ¡ordenar! De lo contrario, tendrá que hacer más.
Sobre el orden de las columnas de índice:
Para la consulta básica, existe la "fuerza bruta" enfoque para contar el número de usuarios coincidentes para todos conversaciones de todos los usuarios dados y luego filtre las que coincidan con todos los usuarios dados. Está bien para tablas pequeñas y/o solo matrices de entrada cortas y/o pocas conversaciones por usuario, pero no escala bien :
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[])
);
Eliminar conversaciones con usuarios adicionales con un NOT EXISTS
anti-semi-unión. Más:
Técnicas alternativas:
Hay varios otros, (mucho) más rápidos relational-division técnicas de consulta. Pero los más rápidos no se adaptan bien a una dinámica número de identificaciones de usuario.
Para una consulta rápida que también puede manejar una cantidad dinámica de ID de usuario, considere un CTE recursivo :
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[])
);
Para facilitar su uso, envuelva esto en una función o declaración preparada . Me gusta:
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);
Llamar:
EXECUTE conversations('{1,4,6}');
db<>fiddle aquí (también demostrando una función )
Todavía hay margen de mejora:llegar a top rendimiento, tiene que poner a los usuarios con la menor cantidad de conversaciones primero en su matriz de entrada para eliminar la mayor cantidad de filas posible antes. Para obtener el máximo rendimiento, puede generar una consulta no recursiva y no dinámica de forma dinámica (usando uno de los rápidos técnicas del primer enlace) y ejecutar eso a su vez. Incluso podría envolverlo en una sola función plpgsql con SQL dinámico...
Más explicación:
Alternativa:MV para tabla escasamente escrita
Si la tabla "conversationUsers"
es principalmente de solo lectura (es poco probable que cambien las conversaciones antiguas), puede usar un MATERIALIZED VIEW
con usuarios agregados previamente en matrices ordenadas y cree un índice btree simple en esa columna de matriz.
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");
El índice de cobertura demostrado requiere Postgres 11. Consulte:
Acerca de ordenar filas en una subconsulta:
En versiones anteriores, use un índice simple de varias columnas en (users, "conversationId")
. Con matrices muy largas, un índice hash podría tener sentido en Postgres 10 o posterior.
Entonces la consulta mucho más rápida sería simplemente:
SELECT "conversationId"
FROM mv_conversation_users c
WHERE users = '{1,4,6}'::int[]; -- sorted array!
db<>fiddle aquí
Debe sopesar los costos adicionales de almacenamiento, escritura y mantenimiento con los beneficios del rendimiento de lectura.
Aparte:considere los identificadores legales sin comillas dobles. conversation_id
en lugar de "conversationId"
etc.: