PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Otimize a consulta GROUP BY para recuperar a última linha por usuário


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