O problema
Temos uma consulta como
SELECT COUNT(1)
FROM article
JOIN reservation ON a_id = r_article_id
WHERE r_last_modified < now() - '8 weeks'::interval
AND r_group_id = 1
AND r_status = 'OPEN';
Como ocorre um tempo limite (após 10 minutos) com mais frequência, decidi investigar o problema.
A EXPLAIN (ANALYZE, BUFFERS)
saída é assim:
Aggregate (cost=264775.48..264775.49 rows=1 width=0) (actual time=238960.290..238960.291 rows=1 loops=1)
Buffers: shared hit=200483 read=64361 dirtied=666 written=8, temp read=3631 written=3617
I/O Timings: read=169806.955 write=0.154
-> Hash Join (cost=52413.67..264647.65 rows=51130 width=0) (actual time=1845.483..238957.588 rows=21644 loops=1)
Hash Cond: (reservation.r_article_id = article.a_id)
Buffers: shared hit=200483 read=64361 dirtied=666 written=8, temp read=3631 written=3617
I/O Timings: read=169806.955 write=0.154
-> Index Scan using reservation_r_article_id_idx1 on reservation (cost=0.42..205458.72 rows=51130 width=4) (actual time=34.035..237000.197 rows=21644 loops=1)
Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
Rows Removed by Filter: 151549
Buffers: shared hit=200193 read=48853 dirtied=450 written=8
I/O Timings: read=168614.105 write=0.154
-> Hash (cost=29662.22..29662.22 rows=1386722 width=4) (actual time=1749.392..1749.392 rows=1386814 loops=1)
Buckets: 32768 Batches: 8 Memory Usage: 6109kB
Buffers: shared hit=287 read=15508 dirtied=216, temp written=3551
I/O Timings: read=1192.850
-> Seq Scan on article (cost=0.00..29662.22 rows=1386722 width=4) (actual time=23.822..1439.310 rows=1386814 loops=1)
Buffers: shared hit=287 read=15508 dirtied=216
I/O Timings: read=1192.850
Total runtime: 238961.812 ms
O nó do gargalo é obviamente a varredura do índice. Então, vamos ver a definição do índice:
CREATE INDEX reservation_r_article_id_idx1
ON reservation USING btree (r_article_id)
WHERE (r_status <> ALL (ARRAY['FULFILLED', 'CLOSED', 'CANCELED']));
Tamanhos e números de linhas
O tamanho (relatado por \di+
ou visitando o arquivo físico) é de 36 MB. Como as reservas geralmente passam apenas um tempo relativamente curto em todos os status não listados acima, há muitas atualizações acontecendo, então o índice é bastante inchado (cerca de 24 MB são desperdiçados aqui) - ainda assim, o tamanho é relativamente pequeno.
A reservation
tabela tem cerca de 3,8 GB de tamanho, contendo cerca de 40 milhões de linhas. O número de reservas que ainda não foram fechadas é de cerca de 170.000 (o número exato é relatado no nó da varredura de índice acima).
Agora, a surpresa: os relatórios da varredura de índice buscam uma enorme quantidade de buffers (ou seja, 8 KB de páginas):
Buffers: shared hit=200193 read=48853 dirtied=450 written=8
Os números lidos no cache e no disco (ou no cache do SO) somam 1,9 GB!
Pior cenário
Por outro lado, o pior cenário, quando cada tupla fica em uma página diferente da tabela, seria responsável por visitar (21644 + 151549) + 4608 páginas (total de linhas buscadas na tabela mais o número da página de índice do físico Tamanho). Isso ainda está abaixo de 180.000 - muito abaixo dos quase 250.000 observados.
Interessante (e talvez importante) é que a velocidade de leitura do disco é de cerca de 2,2 MB / s, o que é bastante normal, eu acho.
E daí?
Alguém tem uma idéia de onde essa discrepância pode surgir?
Nota: Para ser claro, temos ideias sobre o que melhorar / alterar aqui, mas eu realmente gostaria de entender os números que obtive - é disso que se trata a questão.
Atualização: verificando o efeito do armazenamento em cache ou do microvacuum
Com base na resposta de jjanes , verifiquei o que acontece quando reexecuto exatamente a mesma consulta imediatamente. O número de buffers afetados não muda realmente. (Para fazer isso, simplifiquei a consulta ao mínimo necessário que ainda mostra o problema.) É o que vejo desde a primeira execução:
Aggregate (cost=240541.52..240541.53 rows=1 width=0) (actual time=97703.589..97703.590 rows=1 loops=1)
Buffers: shared hit=413981 read=46977 dirtied=56
I/O Timings: read=96807.444
-> Index Scan using reservation_r_article_id_idx1 on reservation (cost=0.42..240380.54 rows=64392 width=0) (actual time=13.757..97698.461 rows=19236 loops=1)
Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
Rows Removed by Filter: 232481
Buffers: shared hit=413981 read=46977 dirtied=56
I/O Timings: read=96807.444
Total runtime: 97703.694 ms
e depois do segundo:
Aggregate (cost=240543.26..240543.27 rows=1 width=0) (actual time=388.123..388.124 rows=1 loops=1)
Buffers: shared hit=460990
-> Index Scan using reservation_r_article_id_idx1 on reservation (cost=0.42..240382.28 rows=64392 width=0) (actual time=0.032..385.900 rows=19236 loops=1)
Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
Rows Removed by Filter: 232584
Buffers: shared hit=460990
Total runtime: 388.187 ms
article
? Parece que todas as colunas envolvidas são dareservation
tabela e (assumindo) que existe um FK, o resultado deve ser o mesmo.pg_stat_reset()
-lo e, em seguida, executar a consulta e, em seguida, procurarpg_statio_user_tables
onde ele atribui os blocos.Respostas:
Eu acho que a chave aqui é o monte de atualizações e o inchaço no índice.
O índice contém ponteiros para linhas na tabela que não estão mais 'ativas'. Estas são as versões antigas das linhas atualizadas. As versões antigas das linhas são mantidas por um tempo, para satisfazer as consultas com um instantâneo antigo, e são mantidas por mais algum tempo, porque ninguém deseja fazer o trabalho de removê-las com mais frequência do que o necessário.
Ao varrer o índice, ele deve visitar essas linhas e depois perceber que elas não estão mais visíveis, então as ignora. A
explain (analyze,buffers)
instrução não relata essa atividade explicitamente, exceto pela contagem dos buffers lidos / acessados no processo de inspeção dessas linhas.Há algum código de "microvacuum" para btrees, de modo que, quando a varredura retorna ao índice novamente, ele lembra que o ponteiro que ele perseguiu não estava mais ativo e o marca como morto no índice. Dessa forma, a próxima consulta semelhante executada não precisa persegui-la novamente. Portanto, se você executar exatamente a mesma consulta novamente, provavelmente verá que os acessos ao buffer caem mais perto do que você previu.
Você também pode
VACUUM
tabela com mais freqüência, que limpará as tuplas mortas da própria tabela, não apenas do índice parcial. Em geral, as tabelas com um índice parcial de alta rotatividade provavelmente se beneficiarão de um vácuo mais agressivo do que o nível padrão.fonte