Estou usando Postgre 9.5. Eu tenho uma tabela que registra acessos de página de vários sites. Esta tabela contém cerca de 32 milhões de linhas de 1º de janeiro de 2016 a 30 de junho de 2016.
CREATE TABLE event_pg (
timestamp_ timestamp without time zone NOT NULL,
person_id character(24),
location_host varchar(256),
location_path varchar(256),
location_query varchar(256),
location_fragment varchar(256)
);
Estou tentando ajustar uma consulta que conta o número de pessoas que realizaram uma determinada sequência de acessos à página. A consulta destina-se a responder a perguntas como "quantas pessoas visualizaram a página inicial, acessaram o site de ajuda e visualizaram a página de agradecimento"? O resultado se parece com isso
╔════════════╦════════════╦═════════════╗
║ home-page ║ help site ║ thankyou ║
╠════════════╬════════════╬═════════════╣
║ 10000 ║ 9800 ║1500 ║
╚════════════╩════════════╩═════════════╝
Observe que os números estão diminuindo, o que faz sentido, porque dos 10.000 que visualizaram a página inicial, 9.800 foram para o site de ajuda e desses 1.500 foram para a página de agradecimento.
O SQL para uma sequência de 3 etapas usa junções laterais da seguinte maneira:
SELECT
sum(view_homepage) AS view_homepage,
sum(use_help) AS use_help,
sum(thank_you) AS thank_you
FROM (
-- Get the first time each user viewed the homepage.
SELECT X.person_id,
1 AS view_homepage,
min(timestamp_) AS view_homepage_time
FROM event_pg X
WHERE X.timestamp_ between '2016-04-23 00:00:00.0' and timestamp '2016-04-30 23:59:59.999'
AND X.location_host like '2015.testonline.ca'
GROUP BY X.person_id
) e1
LEFT JOIN LATERAL (
SELECT
Y.person_id,
1 AS use_help,
timestamp_ AS use_help_time
FROM event_pg Y
WHERE
Y.person_id = e1.person_id AND
location_host = 'helpcentre.testonline.ca' AND
timestamp_ BETWEEN view_homepage_time AND timestamp '2016-04-30 23:59:59.999'
ORDER BY timestamp_
LIMIT 1
) e2 ON true
LEFT JOIN LATERAL (
SELECT
1 AS thank_you,
timestamp_ AS thank_you_time
FROM event_pg Z
WHERE Z.person_id = e2.person_id AND
location_fragment = '/file/thank-you' AND
timestamp_ BETWEEN use_help_time AND timestamp '2016-04-30 23:59:59.999'
ORDER BY timestamp_
LIMIT 1
) e3 ON true;
Eu tenho um índice em timestamp_
, person_id
e as location
colunas. As consultas em intervalos de alguns dias ou semanas são muito rápidas (1s a 10s). Onde fica lento é quando tento executar a consulta para tudo entre 1º de janeiro e 30 de julho. Demora mais de um minuto. Se você comparar as duas explicações abaixo, poderá ver que ele não usa mais o índice timestamp_ e, em vez disso, faz um Seq Scan porque o índice não compraria nada, pois estamos consultando "o tempo todo", portanto, praticamente todos os registros na tabela .
- Explique por um período de alguns dias (usa o índice): https://explain.depesz.com/s/2tOi
- Explique por 6 meses (seq scan): https://explain.depesz.com/s/c0yq
Agora percebo que a natureza do loop aninhado da junção lateral vai desacelerar quanto mais registros ele tiver que percorrer, mas existe alguma maneira de acelerar essa consulta para grandes intervalos de datas para que ela seja dimensionada melhor?
Notas preliminares
Você está usando tipos de dados ímpares.
character(24)
?char(n)
é um tipo desatualizado e quase sempre a escolha errada. Você tem índices ativadosperson_id
e se junta a eles repetidamente.integer
seria muito mais eficiente por vários motivos. (Oubigint
, se você planeja gravar mais de 2 bilhões de linhas durante o tempo de vida da tabela.) Relacionado:LIKE
é inútil sem curingas. Use=
em vez disso. Mais rápido.x.location_host LIKE '2015.testonline.ca'
x.location_host = '2015.testonline.ca'
Use
count(e1.*)
oucount(*)
em vez de adicionar uma coluna fictícia com o valor1
para cada subconsulta. (Exceto para o último (e3
), onde você não precisa de nenhum dado real.)Você é inconsistente em às vezes converter a string literal para
timestamp
e às vezes não (timestamp '2016-04-30 23:59:59.999'
). Ou faz sentido, então faça o tempo todo , ou não faz, então não faça.Não. Quando comparado a uma
timestamp
coluna, uma string literal é coagida detimestamp
qualquer maneira. Portanto, você não precisa de um elenco explícito.O tipo de dados Postgres
timestamp
tem até 6 dígitos fracionários. SuasBETWEEN
expressões deixam casos de canto. Eu os substituí por expressões menos propensas a erros.Índices
Importante: para otimizar o desempenho, crie índices multicolunas .
Para a primeira subconsulta
hp
:Ou, se você puder obter varreduras somente de índice, anexe
person_id
ao índice:Para intervalos de tempo muito grandes abrangendo a maior parte ou toda a tabela, esse índice deve ser preferível - ele também oferece suporte à
hlp
subconsulta, portanto, crie-o de qualquer maneira:Para
tnk
:Otimizado com índices parciais
Se seus predicados forem constantes
location_host
,location_fragment
podemos usar índices parciais muito mais baratos , especialmente porque suaslocation_*
colunas parecem grandes:Considerar:
Novamente, todos esses índices são substancialmente menores e mais rápidos com
integer
oubigint
paraperson_id
.Geralmente, você precisa acessar
ANALYZE
a tabela depois de criar um novo índice - ou esperar até que o autovacuum entre em ação para fazer isso por você.Para obter varreduras somente de índice , sua tabela deve ser
VACUUM
'editada o suficiente. Teste imediatamente depoisVACUUM
como prova de conceito. Leia a página vinculada do Postgres Wiki para obter detalhes se você não estiver familiarizado com varreduras somente de índice .Consulta básica
Implementando o que eu discuti. Consulta para intervalos pequenos ( poucas linhas por
person_id
):DISTINCT ON
geralmente é mais barato para algumas linhas porperson_id
. Explicação detalhada:Se você tiver muitas linhas por
person_id
(mais provável para intervalos de tempo maiores), o CTE recursivo discutido nesta resposta no capítulo 1a pode ser (muito) mais rápido:Veja integrado abaixo.
Otimize e automatize a melhor consulta
É o velho enigma: uma técnica de consulta é melhor para um conjunto menor, outra para um conjunto maior. No seu caso particular, temos um indicador muito bom desde o início - a duração de um determinado período de tempo - que podemos usar para decidir.
Envolvemos tudo em uma função PL/pgSQL. Minha implementação muda
DISTINCT ON
para rCTE quando o período de tempo determinado é maior que um limite definido:Ligar:
O rCTE trabalha com um CTE por definição. Também coloquei CTEs para a
DISTINCT ON
consulta (como discuti com @Lennart nos comentários ), o que nos permite usarCROSS JOIN
em vez deLEFT JOIN
reduzir o conjunto a cada etapa, pois podemos contar cada CTE separadamente. Isso tem efeitos trabalhando em direções opostas:Você terá que testar qual supera o outro.