Tenho uma tabela com 2.395.015 linhas, onde uma TEXT
coluna tem um de três valores e nunca é NULL
. Tenho problemas intermitentes de desempenho de consulta ao contar o número de linhas em que o valor corresponde à maioria (>99%) das linhas. Quero corrigir esse problema de desempenho. Essas consultas devem retornar contagens exatas, então não posso usar contagens aproximadas.
corpus=# \d metadata
Table "public.metadata"
Column | Type | Collation | Nullable | Default
---------------+-----------------------------+-----------+----------+----------------
id | text | | not null |
priority | integer | | not null | 10
media_type | text | | not null |
modified | timestamp without time zone | | not null | now()
processed | timestamp without time zone | | |
status | text | | not null | 'QUEUED'::text
note | text | | |
content | text | | |
resolved | text | | |
response_time | integer | | |
luid | integer | | not null |
jamo_date | timestamp without time zone | | |
audit_path | text | | |
Indexes:
"metadata_pkey" PRIMARY KEY, btree (id)
"metadata_id_idx" btree (id)
"metadata_luid_idx" btree (luid)
"metadata_modified_idx" btree (modified DESC)
"metadata_processed_idx" btree (processed DESC)
"metadata_status_idx" btree (status)
Check constraints:
"media_type_ck" CHECK (media_type = ANY (ARRAY['text/json'::text, 'text/yaml'::text]))
"status_ck" CHECK (status = ANY (ARRAY['QUEUED'::text, 'PROCESSED'::text, 'ERROR'::text]))
Foreign-key constraints:
"metadata_luid_fkey" FOREIGN KEY (luid) REFERENCES concept(luid) ON DELETE CASCADE
corpus=#
Tenho algumas consultas simples que contam o número de linhas que correspondem a um dos três códigos de status ( QUEUED
, PROCESSED
, ERROR
). Há 0 linhas que correspondem a QUEUED
, 9.794 que correspondem a ERROR
, e 2.385.221 que correspondem a PROCESSED
. Quando executo uma consulta idêntica em cada um desses códigos de status, geralmente obtenho um conjunto de resultados prontamente:
corpus=# EXPLAIN ANALYZE VERBOSE SELECT COUNT(*) FROM metadata WHERE status='QUEUED';
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=1947.17..1947.18 rows=1 width=8) (actual time=2.935..2.936 rows=1 loops=1)
Output: count(*)
-> Index Only Scan using metadata_status_idx on public.metadata (cost=0.43..1915.97 rows=12480 width=0) (actual time=2.932..2.933 rows=0 loops=1)
Output: status
Index Cond: (metadata.status = 'QUEUED'::text)
Heap Fetches: 0
Planning Time: 0.734 ms
Execution Time: 2.988 ms
(8 rows)
corpus=# EXPLAIN ANALYZE VERBOSE SELECT COUNT(*) FROM metadata WHERE status='ERROR';
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------
Aggregate (cost=1184.19..1184.20 rows=1 width=8) (actual time=1484.763..1484.764 rows=1 loops=1)
Output: count(*)
-> Index Only Scan using metadata_status_idx on public.metadata (cost=0.43..1165.26 rows=7569 width=0) (actual time=4.235..1484.029 rows=9794 loops=1)
Output: status
Index Cond: (metadata.status = 'ERROR'::text)
Heap Fetches: 9584
Planning Time: 0.072 ms
Execution Time: 1484.786 ms
(8 rows)
corpus=#
corpus=# EXPLAIN ANALYZE VERBOSE SELECT COUNT(*) FROM metadata WHERE status='PROCESSED';
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=261398.83..261398.84 rows=1 width=8) (actual time=741.319..749.026 rows=1 loops=1)
Output: count(*)
-> Gather (cost=261398.62..261398.83 rows=2 width=8) (actual time=741.309..749.020 rows=3 loops=1)
Output: (PARTIAL count(*))
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=260398.62..260398.63 rows=1 width=8) (actual time=735.099..735.100 rows=1 loops=3)
Output: PARTIAL count(*)
Worker 0: actual time=730.871..730.872 rows=1 loops=1
Worker 1: actual time=733.435..733.436 rows=1 loops=1
-> Parallel Index Only Scan using metadata_status_idx on public.metadata (cost=0.43..257903.37 rows=998100 width=0) (actual time=0.065..700.529 rows=795074 loops=3)
Output: status
Index Cond: (metadata.status = 'PROCESSED'::text)
Heap Fetches: 747048
Worker 0: actual time=0.060..702.980 rows=670975 loops=1
Worker 1: actual time=0.076..686.946 rows=1010099 loops=1
Planning Time: 0.085 ms
Execution Time: 749.068 ms
(18 rows)
corpus=#
Mas, ocasionalmente, a contagem de PROCESSED
linhas leva muito tempo (às vezes, vários minutos):
corpus=# EXPLAIN ANALYZE VERBOSE SELECT COUNT(*) FROM metadata WHERE status='PROCESSED';
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=261398.83..261398.84 rows=1 width=8) (actual time=30019.273..30019.336 rows=1 loops=1)
Output: count(*)
-> Gather (cost=261398.62..261398.83 rows=2 width=8) (actual time=30019.261..30019.326 rows=3 loops=1)
Output: (PARTIAL count(*))
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=260398.62..260398.63 rows=1 width=8) (actual time=29967.734..29967.735 rows=1 loops=3)
Output: PARTIAL count(*)
Worker 0: actual time=29939.915..29939.916 rows=1 loops=1
Worker 1: actual time=29944.395..29944.395 rows=1 loops=1
-> Parallel Index Only Scan using metadata_status_idx on public.metadata (cost=0.43..257903.37 rows=998100 width=0) (actual time=75.385..29931.795 rows=795074 loops=3)
Output: status
Index Cond: (metadata.status = 'PROCESSED'::text)
Heap Fetches: 747151
Worker 0: actual time=128.857..29899.156 rows=916461 loops=1
Worker 1: actual time=28.609..29905.708 rows=854439 loops=1
Planning Time: 421.203 ms
Execution Time: 30019.440 ms
(18 rows)
corpus=#
Enquanto a consulta acima está sendo executada lentamente, consigo consultar a mesma tabela para qualquer um dos outros dois códigos, e essas consultas retornam em 1 segundo. Procurei por bloqueios de tabela (não há nenhum). Isso acontece mesmo quando não há outras consultas ou inserções de tabela em execução.
- Quais são as possíveis causas para essas consultas lentas intermitentes?
- Que depuração adicional posso tentar para obter mais informações sobre essas consultas lentas?
- Há alguma configuração de servidor relevante?
- Existe uma maneira mais eficiente de indexar/codificar essas colunas (por exemplo, devo usar um
CHAR(1)
), ou mesmo umSMALLINT
? Se sim, qual índice deve ser usado para a coluna?
Se eu usar um CHAR(1)
, há alguma diferença entre as seguintes restrições:
ALTER TABLE jgi_metadata ADD CONSTRAINT status_code_ck CHECK (status_code = ANY (ARRAY['Q'::char(1), 'P'::char(1), 'E'::char(1)]));
ALTER TABLE jgi_metadata ADD CONSTRAINT status_code_ck CHECK (status_code IN ('Q', 'P', 'E'));
Um índice parcial poderia ser usado para esta coluna, mesmo que isso nunca aconteça
NULL
?Devo dividir o
PROCESSED
off em uma coluna booleana e então usar astatus
coluna somente para os outros códigos e torná-la anulável com um índice parcial?
Este é o PostgreSQL 11 com configurações padrão, rodando no Linux.
Outras coisas que tentei:
- Aumentou o work_mem para 100 MB (via
postgresql.conf
). Nenhuma alteração no desempenho. - Tentei criar um índice parcial na coluna de status.
Atualização: Descobri que esse problema de desempenho não tem nada a ver com a coluna de status, mas sim com o tamanho da tabela em si, como mostra a consulta de 2 minutos a seguir:
corpus=# EXPLAIN ANALYZE VERBOSE SELECT COUNT(*) FROM metadata;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=196398.52..196398.53 rows=1 width=8) (actual time=118527.897..118554.762 rows=1 loops=1)
Output: count(*)
-> Gather (cost=196398.30..196398.51 rows=2 width=8) (actual time=118522.165..118554.756 rows=3 loops=1)
Output: (PARTIAL count(*))
Workers Planned: 2
Workers Launched: 2
-> Partial Aggregate (cost=195398.30..195398.31 rows=1 width=8) (actual time=118491.043..118491.044 rows=1 loops=3)
Output: PARTIAL count(*)
Worker 0: actual time=118475.143..118475.144 rows=1 loops=1
Worker 1: actual time=118476.110..118476.111 rows=1 loops=1
-> Parallel Index Only Scan using metadata_status_idx on public.metadata (cost=0.43..192876.13rows=1008870 width=0) (actual time=71.797..118449.265 rows=809820 loops=3)
Output: status
Heap Fetches: 552630
Worker 0: actual time=75.877..118434.476 rows=761049 loops=1
Worker 1: actual time=104.872..118436.647 rows=745770 loops=1
Planning Time: 592.040 ms
Execution Time: 118554.839 ms
(17 rows)
corpus=#
Isso parece ser muito semelhante a outras perguntas agora, então estou tentando estratégias de mitigação a partir desta resposta :
VACUUM ANALYZE metadata;
A primeira vezCOUNT(*)
que isso levou 5 segundos, as contagens subsequentes levaram 190 ms.
Outros pensamentos:
- Ajudaria se a coluna de status fosse dividida em sua própria tabela, com uma chave estrangeira na
metadata
tabela?
Nota: Estou me convencendo de que esta pergunta é uma duplicata de várias outras perguntas aqui:
- Contagens extremamente lentas do PostgreSQL
- As consultas count(*) são muito lentas, mesmo com uma varredura somente de índice
- Por que algumas consultas de contagem são tão lentas?
- Otimizando o resultado da contagem de seleção no Postgresql
- https://stackoverflow.com/questions/58449716/postgres-why-does-select-count-take-so-long
- https://stackoverflow.com/questions/16916633/if-postgresql-count-is-always-slow-how-to-paginate-complex-queries
- https://stackoverflow.com/questions/7943233/fast-way-to-discover-the-row-count-of-a-table-in-postgresql/7945274#7945274
Esta resposta pode conter a melhor solução para este problema:
Conforme solicitado, aqui está uma análise do plano de consulta com buffers:
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) SELECT COUNT(*) FROM metadata;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Finalize Aggregate (cost=80771.95..80771.96 rows=1 width=8) (actual time=26711.481..26716.494 rows=1 loops=1)
Output: count(*)
Buffers: shared hit=293915 read=19595 dirtied=282 written=12
-> Gather (cost=80771.73..80771.94 rows=2 width=8) (actual time=26711.203..26716.488 rows=3 loops=1)
Output: (PARTIAL count(*))
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=293915 read=19595 dirtied=282 written=12
-> Partial Aggregate (cost=79771.73..79771.74 rows=1 width=8) (actual time=26565.622..26565.623 rows=1 loops=3)
Output: PARTIAL count(*)
Buffers: shared hit=293915 read=19595 dirtied=282 written=12
Worker 0: actual time=26530.890..26530.891 rows=1 loops=1
Buffers: shared hit=105264 read=6760 dirtied=145 written=5
Worker 1: actual time=26530.942..26530.942 rows=1 loops=1
Buffers: shared hit=84675 read=7529 dirtied=46 written=2
-> Parallel Index Only Scan using metadata_status_idx on public.metadata (cost=0.43..77241.05 rows=1012275 width=0) (actual time=42.254..26529.232 rows=809820 loops=3)
Output: status
Heap Fetches: 17185
Buffers: shared hit=293915 read=19595 dirtied=282 written=12
Worker 0: actual time=59.291..26494.376 rows=815113 loops=1
Buffers: shared hit=105264 read=6760 dirtied=145 written=5
Worker 1: actual time=31.165..26484.729 rows=1036972 loops=1
Buffers: shared hit=84675 read=7529 dirtied=46 written=2
Planning Time: 98.400 ms
Execution Time: 26716.529 ms
(25 rows)
Meu palpite é que às vezes muitas das páginas do heap são armazenadas em buffer, e as consultas são executadas rapidamente. Outras vezes, o buffer é carregado com outras páginas, e então você acaba esperando para ler os dados do disco.
Há dois problemas com esse tipo de consulta:
O Postgres mantém estatísticas para as colunas, incluindo os histogramas de quantas linhas de cada valor esperar. Se estatísticas aproximadas forem suficientes para você, você pode simplesmente consultar
pg_stats
. Outra abordagem pode ser usar amostragem. Também é uma aproximação. Mas se você precisa de números exatos, então você deve considerar fazer algo mais sofisticado e manter as estatísticas em uma tabela separada você mesmo.Retrabalhar a arquitetura para armazenar estatísticas pré-calculadas
Há um limite para o quanto essa arquitetura pode escalar - e não é muito. Toda vez que você executar essa consulta, você terá que fazer uma varredura sequencial da tabela (os índices são inúteis).
Se esta for uma consulta crítica, você pode reformular sua arquitetura para manter as estatísticas resumidas em uma tabela separada:
counted default false
,Então, quando você precisar fazer a contagem, o que resta é somar as estatísticas e as novas contagens que não foram atualizadas pelo trabalho em segundo plano:
O
counted
precisa ser indexado (um índice parcial seria perfeito). Isso dará resultados instantaneamente.Se você precisar dar suporte à exclusão também, em vez de
counted
apenas 2 valores, você pode transformá-lo em 3 valores:JUST_INSERTED
,JUST_DELETED
,COUNTED
. Então, em vez de excluir o registro imediatamente - marque-o comoJUST_DELETED
, para atualizar as estatísticas no mesmo trabalho em segundo plano. Mas, dessa vez, subtraia as contagens.Com base nos meus testes descritos na minha pergunta revisada e nos conselhos das respostas a perguntas semelhantes (veja aqui e aqui ), implementei as seguintes alterações:
CREATE INDEX status_not_processed_idx ON metadata (status) WHERE status<>'PROCESSED';
VACUUM ANALYZE metadata;
ALTER TABLE metadata SET (autovacuum_vacuum_scale_factor = 0, autovacuum_analyze_scale_factor = 0, autovacuum_vacuum_threshold = 10000, autovacuum_analyze_threshold = 10000);
COUNT(*)
ou conte comPROCESSED
valor:O teste de desempenho inicial parecia promissor (422 ms), mas testes subsequentes dessa abordagem produziram o mesmo problema de desempenho:
Não vou marcar isso como resposta para minha pergunta porque o problema de desempenho persistiu. Estou deixando isso aqui como um exemplo de uma solução que falhou.
Também tentou:
shared_buffers
depostgresql.conf
padrão128MB
para2GB
. (nenhuma alteração significativa no desempenho da consulta)Esse problema finalmente chegou a um ponto em que não pude mais ignorá-lo.
Finalmente resolvi esse problema de desempenho movendo o código de status para sua própria tabela e usando um inteiro
luid
(identificador exclusivo local de incremento automático) da tabela principal como chave. O código de status em si agora é apenas um único caractere. O banco de dados tem até 4.736.786 registros e a visualização (também modificada, colada abaixo) conclui a consulta usando a nova tabela em menos de um segundo.Tabela "public.record_status"
Também simplifiquei a visualização de estatísticas removendo a
fast_count_rows()
função (que usavabigint
), o que cortou outros 500 ms da consulta:Exibir "public.metadata_queue_statistics"
Desempenho antes de remover
fast_count_rows
a função da exibição:Desempenho após remover
fast_count_rows
a função da visualização:Então, agora tenho essa visualização concluída em menos de meio segundo.
Para comparação com os tamanhos de buffer das minhas tentativas anteriores de resolver este problema, aqui está o plano de execução mais detalhado:
Não sei como foi, mas preciso saber o seguinte:
Existem tantas variáveis que eu resumiria em duas questões:
(1) Como @Stanislav disse acima, a cardinalidade do índice de status é muito baixa para se beneficiar do índice baseado em heap.
(2) A tabela tem muitos campos TEXT e pode aumentar a linha em cada página apenas para fornecer um número exato de linhas e, por causa da coluna TEXT no meio da linha inteira, seu desempenho seria ruim, explicando que seu ANALYZE o torna bom no início e, em seguida, despeja tudo dos shared_buffers.
--> For the second issue, here is what I did, create two tables, one to store its critical result such as media_type, status, thing .. that technically don't have TEXT field in it. And the other has UNIQUE FOREIGN KEY with PRIMARY KEY from the first table.
--> For the INSERT and UPDATE and DELETE, in your app, making an explicit transaction include two INSERT (or UPDATE/DELETE) in it. If both are OK, apply COMMIT and/or ROLLBACK if you don't like it. Then if you want to extract it content, use LEFT JOIN (not INNER JOIN). In here, the main (first) table is much smaller, allowing you to load and get the COUNT on the same amount of rows in less number of pages required to scan.