Para melhor desempenho de leitura, você precisa de um índice de várias colunas:
CREATE INDEX log_combo_idx
ON log (user_id, log_date DESC NULLS LAST);
Para fazer varreduras somente de índice possível, adicione a coluna não necessária
payload
em um índice de cobertura com o INCLUDE
cláusula (Postgres 11 ou posterior):CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST) INCLUDE (payload);
Ver:
- A cobertura de índices no PostgreSQL ajuda nas colunas JOIN?
Fallback para versões mais antigas:
CREATE INDEX log_combo_covering_idx
ON log (user_id, log_date DESC NULLS LAST, payload);
Por que
DESC NULLS LAST
? - Índice não utilizado na consulta de intervalo de datas
Para poucos linhas por
user_id
ou pequenas tabelas DISTINCT ON
é normalmente mais rápido e simples:- Selecionar a primeira linha em cada grupo GROUP BY?
Para muitos linhas por
user_id
uma verificação de salto de índice (ou verificação de índice solto ) é (muito) mais eficiente. Isso não é implementado até o Postgres 12 - o trabalho está em andamento para o Postgres 14. Mas existem maneiras de emular isso de forma eficiente. Expressões de tabela comuns requerem Postgres 8.4+ .
LATERAL
requer Postgres 9.3+ .As soluções a seguir vão além do que é abordado no Postgres Wiki .
1. Nenhuma tabela separada com usuários únicos
Com
users
separados tabela, soluções em 2. abaixo são normalmente mais simples e rápidos. Avance. 1a. CTE recursivo com LATERAL
junte-se
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;
Isso é simples para recuperar colunas arbitrárias e provavelmente melhor no Postgres atual. Mais explicações no capítulo 2a. abaixo de.
1b. CTE recursiva com 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 uma coluna única ou a linha inteira . O exemplo usa todo o tipo de linha da tabela. Outras variantes são possíveis.
Para afirmar que uma linha foi encontrada na iteração anterior, teste uma única coluna NOT NULL (como a chave primária).
Mais explicações para esta pergunta no capítulo 2b. abaixo.
Relacionado:
- Consulte as últimas N linhas relacionadas por linha
- GROUP BY uma coluna, enquanto ordena por outra no PostgreSQL
2. Com users
separados mesa
O layout da tabela pouco importa, desde que seja exatamente uma linha por
user_id
relevante é garantido. Exemplo:CREATE TABLE users (
user_id serial PRIMARY KEY
, username text NOT NULL
);
Idealmente, a tabela é classificada fisicamente em sincronia com o
log
tabela. Ver:- Otimizar o intervalo de consulta de carimbo de data/hora do Postgres
Ou é pequeno o suficiente (baixa cardinalidade) que pouco importa. Caso contrário, classificar as linhas na consulta pode ajudar a otimizar ainda mais o desempenho. Veja a adição de Gang Liang. Se a ordem de classificação física dos
users
tabela coincide com o índice em log
, isso pode ser irrelevante. 2a. LATERAL
junte-se
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 fazer referência ao FROM
anterior itens no mesmo nível de consulta. Ver:- Qual é a diferença entre LATERAL JOIN e uma subconsulta no PostgreSQL?
Resultados em uma pesquisa de índice (-somente) por usuário.
Não retorna nenhuma linha para usuários ausentes em
users
tabela. Normalmente, uma chave estrangeira restrição que impõe integridade referencial descartaria isso. Além disso, nenhuma linha para usuários sem entrada correspondente em
log
- em conformidade com a pergunta original. Para manter esses usuários no resultado, use LEFT JOIN LATERAL ... ON true
em vez de CROSS JOIN LATERAL
:- Chame uma função de retorno de conjunto com um argumento de matriz várias vezes
Use
LIMIT n
em vez de LIMIT 1
para recuperar mais de uma linha (mas não todos) por usuário. Efetivamente, todos eles fazem o mesmo:
JOIN LATERAL ... ON true
CROSS JOIN LATERAL ...
, LATERAL ...
O último tem prioridade mais baixa, no entanto.
JOIN
explícito liga antes da vírgula. Essa diferença sutil pode ser importante com mais tabelas de junção. Ver:- "referência inválida à entrada da cláusula FROM para tabela" na consulta do Postgres
2b. Subconsulta correlacionada
Boa escolha para recuperar uma coluna única de uma linha única . Exemplo de código:
- Otimizar a consulta máxima em grupo
O mesmo é possível para várias colunas , mas você precisa de mais inteligência:
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;
Curta
LEFT JOIN LATERAL
acima, esta variante inclui todos usuários, mesmo sem entradas em log
. Você recebe NULL
para combo1
, que você pode filtrar facilmente com um WHERE
cláusula na consulta externa, se necessário.Nitpick:na consulta externa você não pode distinguir se a subconsulta não encontrou uma linha ou todos os valores da coluna são NULL - mesmo resultado. Você precisa de um
NOT NULL
coluna na subconsulta para evitar essa ambiguidade. Uma subconsulta correlacionada só pode retornar um valor único . Você pode agrupar várias colunas em um tipo composto. Mas para decompô-lo mais tarde, o Postgres exige um tipo composto bem conhecido. Registros anônimos só podem ser decompostos fornecendo uma lista de definição de coluna.
Use um tipo registrado como o tipo de linha de uma tabela existente. Ou registre um tipo composto explicitamente (e permanentemente) com
CREATE TYPE
. Ou crie uma tabela temporária (descartada automaticamente no final da sessão) para registrar seu tipo de linha temporariamente. Sintaxe de conversão:(log_date, payload)::combo
Finalmente, não queremos decompor
combo1
no mesmo nível de consulta. Devido a uma fraqueza no planejador de consulta, isso avaliaria a subconsulta uma vez para cada coluna (ainda é verdade no Postgres 12). Em vez disso, torne-a uma subconsulta e decomponha na consulta externa. Relacionado:
- Receba valores da primeira e da última linha por grupo
Demonstrando todas as 4 consultas com 100 mil entradas de registro e 1 mil usuários:
db<>fiddle here - página 11
antigo sqlfiddle