Resumo
Tenho uma tabela com fontes de eventos e uma tabela com eventos. Preciso de uma consulta que me forneça os N
eventos mais recentes de cada fonte ( N
está entre 1 e 100). Atualmente, estou fazendo isso com uma subconsulta que executa uma ROW_NUMBER() OVER (PARTITION BY "EventSourceId" ORDER BY ...) as rankRecent
consulta externa que filtra WHERE rankRecent <= @N
.
Os EXPLAIN ANALYZE
resultados dizem que ele está usando meu índice para as cláusulas partition
e order by
, mas ainda está classificando a tabela inteira e aparentemente esperando encontrar 6 milhões de resultados, mas existem apenas 22 mil. Estou tentando descobrir se existe: (1) uma maneira melhor de obter os N
eventos mais recentes para cada fonte de evento ou (2) uma maneira de sugerir ao planejador de consultas que ele não precisa classificar estritamente a maioria dos tabela, pois apenas as primeiras entradas serão usadas.
Além disso, há um segundo caso de uso para a consulta que nem sei como começar a indexar. Esse não é o cerne desta questão; Menciono-o apenas no interesse de incluir tudo o que possa ser relevante.
Detalhes
Configuração de dados
CREATE TABLE "EventSources"
(
"Id" uuid NOT NULL,
"Name" character varying(100),
CONSTRAINT "PK_EventSources" PRIMARY KEY ("Id")
);
CREATE TABLE "Events"
(
"Id" uuid NOT NULL,
"EventSourceId" uuid NOT NULL,
"Time" timestamp with time zone,
"AltKey" character varying(100),
CONSTRAINT "PK_Events" PRIMARY KEY ("Id"),
CONSTRAINT "FK_Events_EventSources_EventSourceId" FOREIGN KEY ("EventSourceId") REFERENCES "EventSources" ("Id") MATCH SIMPLE ON UPDATE NO ACTION ON DELETE RESTRICT
);
CREATE INDEX "IX_Events_EventSourceId_Time_Desc_AltKey_Desc" ON "Events" USING btree
(
"EventSourceId" ASC,
"Time" DESC NULLS LAST,
"AltKey" DESC NULLS LAST
);
Algumas informações adicionais possivelmente relevantes:
SELECT version(); --PostgreSQL 13.12, compiled by Visual C++ build 1914, 64-bit
SELECT COUNT(*) FROM "EventSources"; --29,000ish
SELECT COUNT(*) FROM "Events"; --20,000,000ish
SELECT COUNT(*) FROM (SELECT DISTINCT "EventSourceId" FROM "Events") sub; --5,000ish. Most of the "EventSources" don't have "Events" but are used for other things in the db
A pergunta
Aqui está a consulta que estou tentando otimizar:
SELECT
*
FROM
(
SELECT
"Events".*,
ROW_NUMBER() OVER (
PARTITION BY "Events"."EventSourceId"
ORDER BY
"Events"."Time" DESC NULLS LAST,
"Events"."AltKey" DESC NULLS LAST
) as rankRecent
FROM
"Events"
--WHERE "Events"."Time" < @LimitTime
) sub
WHERE
rankRecent <= @N; -- @N is in the range 1 to 100.
Casos de uso
Aqui estão os casos de uso para a consulta:
- Estou carregando um painel que exibe cálculos agregados de dados recentes para cada fonte de evento e, com base nesses cálculos, escolhe quais fontes de eventos serão exibidas e em qual ordem.
- Estou investigando um problema que aconteceu ontem às 3h14min15s da manhã e, para uma determinada coleção de fontes de eventos relevantes, preciso ver os 100 eventos que levaram a esse horário para poder ver o que pode ter acontecido. estava dando errado. Neste caso, a
WHERE
cláusula comentada na consulta será descomentada, podendo também se unir a outra tabela para filtrar fontes de eventos relacionadas a um determinado contexto. Não tenho ideia de como indexar isso, mas esse não é o cerne da questão.
Explicar
Aqui está o resultado de EXPLAIN (ANALYZE, BUFFERS, SETTINGS)
(neste caso, @N
foi definido como 5):
- Verificação de subconsulta em sub (custo = 0,56..2637123,43 linhas = 6664000 largura = 66) (tempo real = 0,156..90245,642 linhas = 22613 loops = 1)
- Filtro: (sub.rankrecent <= 5)
- Linhas removidas pelo filtro: 19963368
- Buffers: hit compartilhado=6738934 lido=13332278
- -> WindowAgg (custo = 0,56..2387223,43 linhas = 19992000 largura = 66) (tempo real = 0,155..89355,268 linhas = 19985981 loops = 1)
- Buffers: hit compartilhado=6738934 lido=13332278
- -> Varredura de índice usando "IX_Events_EventSourceId_Time_Desc_AltKey_Desc" em "Eventos" (custo = 0,56..1987383,43 linhas = 19992000 largura = 58) (tempo real = 0,100..82274,745 linhas = 19985981 loops = 1)
- Buffers: hit compartilhado=6738934 lido=13332278
- -> Varredura de índice usando "IX_Events_EventSourceId_Time_Desc_AltKey_Desc" em "Eventos" (custo = 0,56..1987383,43 linhas = 19992000 largura = 58) (tempo real = 0,100..82274,745 linhas = 19985981 loops = 1)
- Buffers: hit compartilhado=6738934 lido=13332278
- Tempo de planejamento: 0,111 ms
- Tempo de execução: 90247,357ms
Abordagens alternativas que considerei
- Considerei usar uma visão materializada para fazer o trabalho pesado de antemão. No entanto, a
"Events"
tabela terá dados inseridos cerca de uma dúzia de vezes por segundo, e cada transação pode conter de zero a algumas dezenas de linhas. Portanto, se a visualização usar o mesmo plano de consulta que a consulta existente, os dados na visualização ficarão totalmente desatualizados antes que um comando para atualizar a visualização possa ser concluído. Então concluí que não fazia sentido essa estratégia. - Também considerei tentar adicionar uma nova
sequence
coluna inteira na"Events"
tabela e depois filtrar nessa sequência. Mas isso realmente não resolve o problema. Cada transação que insere eventos pode ou não ter eventos para uma determinada fonte de eventos, portanto, os 5 eventos mais recentes para uma determinada fonte de eventos podem incluir dados de, por exemplo, uma transação de 5 segundos atrás e uma transação de uma hora atrás. , e um de ontem e um da semana passada e um do ano passado. Portanto, não há uma maneira fácil de sincronizar issosequence
entre milhares de fontes de eventos diferentes. Além disso, é possível que uma fonte de eventos envie manualmente dados que foram perdidos em um momento arbitrário no passado, portanto, a classificação"Time"
parece estar correta. - Considerei ter uma tabela que monitorasse apenas, digamos, os 50 eventos mais recentes para cada fonte de evento e descartasse qualquer coisa mais antiga. Dessa forma, a consulta poderia prosseguir e classificar a tabela inteira, pois não haveria tantos dados para classificar. No entanto, a consulta não satisfaria mais o Caso de Uso nº 2; e, embora o caso de uso nº 2 não seja o objetivo desta questão, não posso simplesmente removê-lo da consulta sem fornecer um substituto.
Esclarecimento
A row_number over partition
questão é a questão principal aqui. O material sobre o Caso de Uso nº 2 e não saber como indexá-lo está incluído aqui, caso inspire alguém a realizar um plano, consulta ou esquema melhor; Não estou esperando uma solução para isso, apenas quero incluir qualquer informação que possa ser relevante.
Uma nova otimização foi introduzida na v15 que permite parar de calcular as classificações depois de encontrar o número necessário de linhas para cada partição. Isso não é mágico, ele ainda precisa ler todas as linhas porque não sabe onde a próxima partição começa, a não ser lendo as linhas até que a chave da partição mude. Mas deveria pelo menos economizar algum tempo. Quando esta otimização está em vigor, ela aparece no plano com uma linha como:
Você provavelmente poderia melhorar isso implementando uma varredura de salto de índice. O PostgreSQL não os implementa automaticamente, mas você pode fazê-lo com um CTE recursivo, conforme mostrado no wiki do projeto (embora pareça que alguém tenha exagerado um pouco desde a última vez que o visitei).
Como você já tem uma tabela de fontes de eventos distintas, você poderia fazer uso dela com uma junção lateral, em vez de usar o CTE, assim: (não vou ficar pulando no teclado com letras maiúsculas e aspas, eu deixe isso para você)
As linhas não utilizadas em EventSources irão desacelerar um pouco, mas provavelmente ainda será muito mais rápido do que você está fazendo atualmente.
Para sua pergunta relacionada, não sei, não li com atenção. Faça outra pergunta se tiver outra pergunta, depois de verificar que essa abordagem funciona para você.