sql >> Base de Datos >  >> RDS >> PostgreSQL

Consulta SQL para encontrar una fila con un número específico de asociaciones

Este es un caso de - 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 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.: