Estou tentando acelerar a seguinte consulta no postgres:
select MAX(msg."timestamp") AS latestDate, msg.channel_id from message msg group by msg.channel_id
o explain
é como tal:
Finalize GroupAggregate (cost=1000.63..2442779.42 rows=305 width=24)
Group Key: channel_id
-> Gather Merge (cost=1000.63..2442770.27 rows=1220 width=24)
Workers Planned: 4
-> Partial GroupAggregate (cost=0.57..2441624.90 rows=305 width=24)
Group Key: channel_id
-> Parallel Index Only Scan using message_channel_id_timestamp on message msg (cost=0.57..2243767.89 rows=39570792 width=24)
JIT:
Functions: 6
Options: Inlining true, Optimization true, Expressions true, Deforming true
O DDL da tabela é assim:
CREATE TABLE public.message (
message_pgid bigserial NOT NULL,
id uuid NOT NULL,
"timestamp" timestamptz NOT NULL,
"content" text NOT NULL,
channel_id uuid NOT NULL,
CONSTRAINT message_pk PRIMARY KEY (message_pgid),
CONSTRAINT message_un UNIQUE (channel_id, id)
);
CREATE INDEX message_channel_id_idx ON public.message USING btree (channel_id);
CREATE INDEX message_channel_id_timestamp ON public.message USING btree (channel_id, "timestamp");
CREATE INDEX message_id ON public.message USING btree (id);
CREATE INDEX message_timestamp_idx ON public.message USING btree ("timestamp");
-- public.message foreign keys
ALTER TABLE public.message ADD CONSTRAINT channel_fk FOREIGN KEY (channel_id) REFERENCES public.channel(id) DEFERRABLE;
ALTER TABLE public.message ADD CONSTRAINT message_fk FOREIGN KEY (user_id) REFERENCES public."user"(id);
e por fim, o explain analyze
:
Finalize GroupAggregate (cost=1000.63..2442779.42 rows=305 width=24) (actual time=7631.501..7673.692 rows=597 loops=1)
Group Key: channel_id
-> Gather Merge (cost=1000.63..2442770.27 rows=1220 width=24) (actual time=7631.383..7673.511 rows=1667 loops=1)
Workers Planned: 4
Workers Launched: 4
-> Partial GroupAggregate (cost=0.57..2441624.90 rows=305 width=24) (actual time=305.736..6125.479 rows=333 loops=5)
Group Key: channel_id
-> Parallel Index Only Scan using message_channel_id_timestamp on message msg (cost=0.57..2243767.89 rows=39570792 width=24) (actual time=0.557..4938.221 rows=31656633 loops=5)
Heap Fetches: 32082
Planning Time: 4.032 ms
JIT:
Functions: 18
Options: Inlining true, Optimization true, Expressions true, Deforming true
Timing: Generation 12.315 ms, Inlining 193.685 ms, Optimization 122.739 ms, Emission 100.570 ms, Total 429.309 ms
Execution Time: 7684.655 ms
Como você pode ver, mesmo com um índice btree, a operação ainda leva 7,6 segundos, a maior parte dos quais é gasta apenas na varredura paralela do índice. Não sei como acelerar isso ainda mais. o índice tem um tamanho relativo de 5,7 G e dou à minha instância 6 GB de RAM, o que deve ser mais que suficiente para uma pesquisa btree max. Eu defini minhas configurações de acordo com pgtune ( https://pgtune.leopard.in.ua/ ).
Há alguma coisa que esteja faltando em face das coisas?
Infelizmente, o postgres não implementa (ainda) o tipo de verificação de índice necessário para otimizar esta consulta automaticamente, portanto, ele verificará todo o índice.
É capaz de usar um índice em (a,b) para otimizar "max(b) WHERE a=..." e também "WHERE a=... ORDER BY b DESC LIMIT 1" que retorna a linha inteira com maior valor de b (isso pode ser mais útil do que apenas max() se você realmente quiser outras colunas). Mas isso é apenas para um valor de a, ou vários em um loop aninhado, e não para toda a tabela, como você está fazendo.
Supondo que você tenha uma tabela separada "canais" com a chave primária "channel_id" que é referenciada pelas mensagens da sua tabela, é fácil emular isso manualmente.
O Postgres sabe como encontrar a linha desejada usando um índice se você solicitar apenas um valor de channel_id. Portanto, o truque é fazer isso para cada valor de channel_id, usando uma subconsulta dependente (se você quiser apenas a coluna max()) ou uma junção LATERAL (se você também quiser outras colunas, como o conteúdo da última mensagem).
Isso resulta em uma varredura de índice nas mensagens por valor de channel_id. Portanto, o tempo está em O(número de canais * log(número de mensagens)) que deve ser muito mais rápido do que verificar toda a tabela de mensagens. Além disso, ele acessa apenas as páginas com as mensagens mais recentes, portanto não destrói seu cache.
Crie dados de teste:
Consultas lentas que leem a tabela inteira (ou índice):
Consulta muito mais rápida que encontra a linha mais recente para cada channel_id imediatamente usando o índice, usando uma subconsulta dependente que seleciona max(ts):
Variante usando LATERAL que tem as seguintes vantagens: pode retornar mais colunas da mensagem caso você precise delas, e pode retornar as últimas ou N últimas mensagens (basta alterar o LIMITE) por canal.
A sintaxe LATERAL JOIN é um pouco estranha. Se você deseja uma linha para um channel_id que não possui mensagens, você precisa usar LEFT JOIN, e isso requer uma condição de junção (USING(channel_id)). Mas por ser um LATERAL JOIN, a tabela direita na junção depende da linha esquerda, portanto esta condição já está especificada nela. Portanto, há um pouco de duplicação.