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