PostgreSQL: max_parallel_maintenance_workers define um teto, não garante paralelismo real
Christophe Pettus destrincha o GUC que controla o paralelismo em CREATE INDEX e VACUUM, e mostra por que subir o número quase nunca é o suficiente para o Postgres usar mais workers.

Poucos parâmetros do PostgreSQL↳PostgreSQL11 conteúdosPostgreSQL via SSL com GolangData · abr 20195 itens legais sobre data types do PostgreSQLData · mar 20195 serviços gratuitos na cloud para bancos de dados PostgresData · fev 2025Ver tudo em Data → geram tanta confusão silenciosa quanto max_parallel_maintenance_workers. Ele parece simples: define quantos processos paralelos uma operação de manutenção pode usar. Na prática, é um teto que raramente é alcançado, porque outras três ou quatro regras entram na frente dele. É esse o argumento central do post "All Your GUCs in a Row: max_parallel_maintenance_workers", de Christophe Pettus, mais um capítulo de uma série que já passou por maintenance_work_mem, max_logical_replication_workers e max_files_per_process.
O parâmetro tem contexto user (pode ser setado por sessão), aceita valores de 0 a 1024, com 0 desligando paralelismo de manutenção, e o default é 2. Ele chegou no PostgreSQL 11 junto com a construção paralela de índices B-tree. O PostgreSQL 13 trouxe vacuum paralelo de índices, o 17 estendeu para BRIN e o 18 para GIN. Hash, GiST e SP-GiST continuam construindo de forma serial até o 18 e até a beta 3 do 19 (à qual Pettus teve acesso para os testes do post).
Um teto por comando, não um pool
O erro mais comum, o próprio Pettus admite ter cometido em um post de 2023, é tratar esse GUC como um limite global de workers de manutenção simultâneos no cluster inteiro. Não é. É um limite por comando. Pettus rodou três construções de índice ao mesmo tempo com o parâmetro em 2 e encontrou seis workers vivos, dois sob cada processo líder. O teto real para o total de workers ativos vem de outros dois parâmetros: max_parallel_workers e max_worker_processes.
Isso muda a conta de quem administra um banco grande com múltiplas cargas de manutenção concorrentes. Rodar REINDEX em paralelo em várias tabelas ao mesmo tempo, ou disparar pg_restore com -j, pode facilmente estourar o pool de workers disponível, mesmo que cada comando individual esteja "dentro do limite".
Como o Postgres decide quantos workers pedir
A parte mais densa do post explica o cálculo em três etapas que uma construção de índice faz antes mesmo de olhar para esse parâmetro:
- A escada de tamanho de tabela, a mesma usada pelo planner para decidir scans paralelos: um worker a partir de 8MB de heap, e mais um a cada vez que o tamanho da tabela triplica. É uma escada lenta. Uma tabela de 782MB pediu cinco workers com o parâmetro em 8, e continuou pedindo cinco com o parâmetro em 1024. Uma tabela de 17GB chega a oito workers, e um terabyte, a onze. Na prática, até a tabela passar de 4TB, configurar esse GUC para 64 não faz diferença nenhuma em relação a configurá-lo para 12.
- O teto do próprio
max_parallel_maintenance_workers, que corta o que a escada pediu. - O piso de memória: cada participante, incluindo o líder, precisa ficar com pelo menos 32MB de
maintenance_work_mem. No default de 64MB, uma construção de índice tem direito a exatamente um worker, não importa quão alto esteja o parâmetro em questão. Ou seja: numa instalação padrão, sem tocar emmaintenance_work_mem, o índice nunca chega nem ao próprio default de 2 workers deste GUC. A fórmula é N workers exigem 32MB × (N + 1): 160MB para quatro workers, 224MB para seis, 288MB para oito.
Há ainda um atalho: o parâmetro de armazenamento parallel_workers, definido por tabela, pula a escada e o piso de memória (mas não o teto deste GUC). No teste de Pettus, com parallel_workers em 6, o parâmetro global em 8 e 64MB de memória, a construção pediu os seis workers, deixando cerca de 9MB para cada um ordenar dados, o que é pouco. Como esse mesmo parâmetro de armazenamento também influencia planos de query paralelos contra a tabela, o conselho é setá-lo para a construção do índice e resetá-lo em seguida.
O resultado de tudo isso é apenas um pedido. Se os workers realmente existirem depende do pool disponível no momento, e uma falha nessa entrega é silenciosa: o comando roda com o que conseguiu, sem erro nem aviso.
O que nunca paraleliza
Algumas construções nem chegam a pedir workers. Tabelas temporárias sempre constroem de forma serial. O mesmo vale para qualquer índice cuja expressão ou predicado chame uma função que não esteja marcada PARALLEL SAFE. Como CREATE FUNCTION assume PARALLEL UNSAFE por padrão, isso cobre a maioria das funções IMMUTABLE caseiras que times criam sem revisar esse detalhe. Uma função SQL simples que acaba sendo inlined pelo planner escapa dessa checagem, o que faz o comportamento parecer aleatório até se conhecer a regra.
Esse caminho de decisão vale para mais comandos do que só CREATE INDEX: REINDEX (nas duas formas CONCURRENTLY, onde só o primeiro scan da tabela é paralelo), ALTER TABLE ... ADD PRIMARY KEY, ADD UNIQUE, e as reconstruções de índice ao final de CLUSTER e VACUUM FULL também pediram workers normalmente na versão 18.6 testada por Pettus.
VACUUM segue outra lógica
O VACUUM usa o parâmetro de um jeito diferente. Só as fases de índice rodam em paralelo, a unidade de trabalho é um índice inteiro (não uma fração dele), e o processo líder já toma um índice para si. O número de workers é a quantidade de índices com pelo menos min_parallel_index_scan_size (512kB), menos um, limitado pela opção PARALLEL se ela foi passada, e limitado de novo por este GUC. Numa tabela com quatro índices, VACUUM (PARALLEL 8) no default lança dois workers.
Aqui não existe a regra dos 32MB, então um VACUUM manual simples nessa mesma tabela já lança dois workers numa instalação padrão, sem que ninguém peça explicitamente. Uma tabela com um único índice enorme não recebe nenhum worker, seja qual for a configuração, e o scan da heap nunca é paralelo.
Um ponto que muda o planejamento de manutenção: o autovacuum ignora completamente esse parâmetro até o PostgreSQL 18. Ele nunca usa workers paralelos, então max_parallel_maintenance_workers só afeta o VACUUM digitado manualmente (ou disparado por vacuumdb). O PostgreSQL 19 introduz um parâmetro separado, autovacuum_max_parallel_workers (contexto sighup, default 0), que não interage com o antigo. No teste de Pettus na beta 3 do 19, com o parâmetro clássico em 0 e o novo em 2, o autovacuum planejou e lançou dois workers enquanto um VACUUM (PARALLEL 4) manual, ao mesmo tempo, rodou de forma totalmente serial.
Ajustando na prática e conferindo se funcionou
Os números que Pettus já recomendava em 2023 continuam valendo: 2 para máquinas pequenas, 4 acima de oito núcleos, 6 para hardware bem maior que isso. Eles só fazem sentido se houver memória disponível para serem alcançados, e uma faixa global de 256MB a 1GB de maintenance_work_mem cobre o piso para qualquer um desses valores. Como o contexto do parâmetro é user, a recomendação para uma construção grande e pontual é setar os dois parâmetros na própria sessão, sem tocar nos globais do cluster.
O ponto onde isso costuma dar errado é pg_restore -j. Cada job é sua própria sessão rodando seu próprio comando, então quatro jobs com o parâmetro em 4 podem pedir dezesseis workers de um pool que, em configuração padrão, tem sete para oferecer. No teste do post, quatro jobs pediram três workers cada, e o máximo vivo ao mesmo tempo foi sete, divididos em três, três e um. A solução prática é algo como:
PGOPTIONS='-c max_parallel_maintenance_workers=2 -c maintenance_work_mem=1GB' pg_restore ...Isso ajusta os dois parâmetros só para as sessões da restauração. A regra de bolso é fazer jobs × (workers + 1) ficar próximo do número de núcleos disponíveis, e garantir que max_parallel_workers cubra jobs × workers. Se isso exigir subir também max_worker_processes, essa mudança exige reinício do servidor.
Depois de ajustar, vale checar se o número realmente mudou. VACUUM (VERBOSE) informa diretamente: "launched 2 parallel vacuum workers for index vacuuming (planned: 2)". Construções de índice são mais discretas: SET client_min_messages = debug1 mostra uma linha dizendo que o índice está sendo construído "with request for N parallel workers", ou "serially" — isso é o pedido, não a entrega. Para saber o que de fato foi lançado, é preciso contar linhas com backend_type = 'parallel worker' em pg_stat_activity, agrupadas por leader_pid, enquanto a construção roda.
Como resume o próprio Pettus, "raising it is easy. Getting a command to use what you raised it to is the harder problem": subir o valor é fácil, o difícil é fazer o comando de fato usar o que foi liberado. Para quem administra bancos grandes, a lição prática é não confiar no valor do GUC como indicador de paralelismo real: sempre medir o que aconteceu de verdade, seja via VACUUM (VERBOSE), seja consultando pg_stat_activity durante a janela de manutenção.
Fonte: Planet PostgreSQL
Este artigo foi escrito por Roberto Diniz, colunista de banco de dados. Conteúdo produzido por agente de IA da redação iMasters, sob revisão editorial humana. Saiba como produzimos no expediente.














