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

Optimice la consulta GROUP BY para recuperar la última fila por usuario

Para obtener el mejor rendimiento de lectura, necesita un índice de varias columnas:

CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);

Para hacer escaneos de solo índice posible, agregue la columna payload que de otro modo no sería necesaria en un índice de cobertura con INCLUDE cláusula (Postgres 11 o posterior):

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);

Ver:

  • ¿Cubrir índices en PostgreSQL ayuda a JOIN columnas?

Alternativa para versiones anteriores:

CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);

Por qué DESC NULLS LAST ?

  • Consulta de índice no utilizado en el rango de fechas

Para pocos filas por user_id o mesas pequeñas DISTINCT ON suele ser el más rápido y sencillo:

  • ¿Seleccionar la primera fila en cada grupo GROUP BY?

Para muchos filas por user_id un análisis de omisión de índice (o escaneo de índice suelto ) es (mucho) más eficiente. Eso no está implementado hasta Postgres 12:el trabajo está en curso para Postgres 14. Pero hay formas de emularlo de manera eficiente.

Las expresiones de tabla comunes requieren Postgres 8.4+ .
LATERAL requiere Postgres 9.3+ .
Las siguientes soluciones van más allá de lo que se cubre en Postgres Wiki .

1. Sin tabla separada con usuarios únicos

Con users separados tabla, soluciones en 2. a continuación son típicamente más simples y rápidos. Saltar adelante.

1a. CTE recursivo con LATERAL unirse

WITH RECURSIVE cte AS (
   (                                -- parentheses required
   SELECT user_id, log_date, payload
   FROM   log
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT l.*
   FROM   cte c
   CROSS  JOIN LATERAL (
      SELECT l.user_id, l.log_date, l.payload
      FROM   log l
      WHERE  l.user_id > c.user_id  -- lateral reference
      AND    log_date <= :mydate    -- repeat condition
      ORDER  BY l.user_id, l.log_date DESC NULLS LAST
      LIMIT  1
      ) l
   )
TABLE  cte
ORDER  BY user_id;

Esto es fácil de recuperar columnas arbitrarias y probablemente sea mejor en Postgres actual. Más explicación en el capítulo 2a. a continuación.

1b. CTE recursivo con subconsulta correlacionada

WITH RECURSIVE cte AS (
   (                                           -- parentheses required
   SELECT l AS my_row                          -- whole row
   FROM   log l
   WHERE  log_date <= :mydate
   ORDER  BY user_id, log_date DESC NULLS LAST
   LIMIT  1
   )
   UNION ALL
   SELECT (SELECT l                            -- whole row
           FROM   log l
           WHERE  l.user_id > (c.my_row).user_id
           AND    l.log_date <= :mydate        -- repeat condition
           ORDER  BY l.user_id, l.log_date DESC NULLS LAST
           LIMIT  1)
   FROM   cte c
   WHERE  (c.my_row).user_id IS NOT NULL       -- note parentheses
   )
SELECT (my_row).*                              -- decompose row
FROM   cte
WHERE  (my_row).user_id IS NOT NULL
ORDER  BY (my_row).user_id;

Conveniente para recuperar una columna única o la fila completa . El ejemplo utiliza el tipo de fila completa de la tabla. Son posibles otras variantes.

Para afirmar que se encontró una fila en la iteración anterior, pruebe una sola columna NOT NULL (como la clave principal).

Más explicación para esta consulta en el capítulo 2b. a continuación.

Relacionado:

  • Consulta las últimas N filas relacionadas por fila
  • GRUPO POR una columna, mientras se ordena por otra en PostgreSQL

2. Con users separados mesa

El diseño de la tabla apenas importa siempre que haya exactamente una fila por user_id relevante Está garantizado. Ejemplo:

CREATE TABLE users (
   user_id  serial PRIMARY KEY
 , username text NOT NULL
);

Idealmente, la tabla se ordena físicamente en sincronización con el log mesa. Ver:

  • Optimizar el rango de consulta de marca de tiempo de Postgres

O es lo suficientemente pequeño (baja cardinalidad) que apenas importa. De lo contrario, ordenar filas en la consulta puede ayudar a optimizar aún más el rendimiento. Ver la adición de Gang Liang. Si el orden de clasificación físico de los users la tabla coincide con el índice en log , esto puede ser irrelevante.

2a. LATERAL unirse

SELECT u.user_id, l.log_date, l.payload
FROM   users u
CROSS  JOIN LATERAL (
   SELECT l.log_date, l.payload
   FROM   log l
   WHERE  l.user_id = u.user_id         -- lateral reference
   AND    l.log_date <= :mydate
   ORDER  BY l.log_date DESC NULLS LAST
   LIMIT  1
   ) l;

JOIN LATERAL permite hacer referencia al anterior FROM elementos en el mismo nivel de consulta. Ver:

  • ¿Cuál es la diferencia entre LATERAL JOIN y una subconsulta en PostgreSQL?

Da como resultado una búsqueda de índice (solo) por usuario.

No devuelve ninguna fila para los usuarios que faltan en users mesa. Normalmente, una clave externa la restricción que impone la integridad referencial lo descartaría.

Además, no hay fila para usuarios sin entrada coincidente en log - conforme a la pregunta original. Para mantener a esos usuarios en el resultado, use LEFT JOIN LATERAL ... ON true en lugar de CROSS JOIN LATERAL :

  • Llamar a una función de devolución de conjuntos con un argumento de matriz varias veces

Usar LIMIT n en lugar de LIMIT 1 para recuperar más de una fila (pero no todos) por usuario.

Efectivamente, todos estos hacen lo mismo:

JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...

Sin embargo, el último tiene menor prioridad. JOIN explícito se une antes de la coma. Esa diferencia sutil puede ser importante con más tablas de unión. Ver:

  • "referencia no válida a la entrada de la cláusula FROM para la tabla" en la consulta de Postgres

2b. Subconsulta correlacionada

Buena elección para recuperar una única columna de una fila única . Ejemplo de código:

  • Optimizar consulta máxima grupal

Lo mismo es posible para columnas múltiples , pero necesitas más inteligencia:

CREATE TEMP TABLE combo (log_date date, payload int);

SELECT user_id, (combo1).*              -- note parentheses
FROM (
   SELECT u.user_id
        , (SELECT (l.log_date, l.payload)::combo
           FROM   log l
           WHERE  l.user_id = u.user_id
           AND    l.log_date <= :mydate
           ORDER  BY l.log_date DESC NULLS LAST
           LIMIT  1) AS combo1
   FROM   users u
   ) sub;

Me gusta LEFT JOIN LATERAL anterior, esta variante incluye todas usuarios, incluso sin entradas en log . Obtienes NULL para combo1 , que puede filtrar fácilmente con WHERE cláusula en la consulta externa si es necesario.
Nitpick:en la consulta externa no puede distinguir si la subconsulta no encontró una fila o si todos los valores de columna son NULL - mismo resultado. Necesitas un NOT NULL columna en la subconsulta para evitar esta ambigüedad.

Una subconsulta correlacionada solo puede devolver un valor único . Puede envolver varias columnas en un tipo compuesto. Pero para descomponerlo más tarde, Postgres exige un tipo compuesto bien conocido. Los registros anónimos solo se pueden descomponer proporcionando una lista de definición de columna.
Utilice un tipo registrado como el tipo de fila de una tabla existente. O registre un tipo compuesto de forma explícita (y permanente) con CREATE TYPE . O cree una tabla temporal (eliminada automáticamente al final de la sesión) para registrar su tipo de fila temporalmente. Sintaxis de conversión:(log_date, payload)::combo

Finalmente, no queremos descomponer combo1 en el mismo nivel de consulta. Debido a una debilidad en el planificador de consultas, esto evaluaría la subconsulta una vez para cada columna (todavía es cierto en Postgres 12). En su lugar, conviértalo en una subconsulta y descompóngalo en la consulta externa.

Relacionado:

  • Obtener valores de la primera y última fila por grupo

Demostración de las 4 consultas con 100.000 entradas de registro y 1.000 usuarios:
db<>fiddle here - página 11
Sqlfiddle antiguo