Volatilidade (VOLATILE / STABLE / IMMUTABLE) diz ao planner o que ele pode assumir sobre o resultado de uma função. Segurança paralela (PARALLEL SAFE / PARALLEL RESTRICTED / PARALLEL UNSAFE) diz algo completamente diferente: se a função é segura para rodar concorrentemente, em um processo worker em background. São dois eixos independentes, e confundi-los é um dos motivos mais comuns de uma query perder silenciosamente seu plano paralelo.
Uma função chamada com frequência
Considere um cálculo pequeno e puro usado em dezenas de relatórios — por exemplo, converter duas datas em uma idade em meses:
CREATE FUNCTION age_in_months(birth_date date, ref_date date)
RETURNS integer
LANGUAGE sql
AS $$
SELECT (EXTRACT(YEAR FROM age(ref_date, birth_date)) * 12
+ EXTRACT(MONTH FROM age(ref_date, birth_date)))::integer
$$;
Sem nenhum atributo, essa função assume por padrão VOLATILE e PARALLEL UNSAFE — a combinação mais conservadora e restritiva que o PostgreSQL pode assumir. Esse padrão quase sempre está errado para uma função como essa, e custa caro silenciosamente toda vez que é chamada contra uma tabela grande.
Corrigindo a volatilidade primeiro
A função recebe duas datas e sempre retorna o mesmo resultado para as mesmas duas datas — sem leitura de tabela, sem estado de sessão, sem efeitos colaterais. Essa é a definição de livro-texto de IMMUTABLE:
ALTER FUNCTION age_in_months(date, date) IMMUTABLE;
IMMUTABLE libera constant folding (se ambos os argumentos forem literais, o PostgreSQL calcula o resultado uma vez em tempo de planejamento) e torna a função elegível para uso em um índice funcional. Mas não diz nada sobre workers paralelos — isso é uma declaração separada.
PARALLEL SAFE: uma pergunta completamente diferente
Quando o PostgreSQL considera um plano paralelo, ele divide o trabalho entre processos worker em background, cada um com sua própria memória e sua própria conexão aos shared buffers — mas eles não compartilham o mesmo estado local de backend que o processo principal. Uma função só é PARALLEL SAFE se puder rodar corretamente dentro de um desses workers: sem escritas, sem uso de sequences locais de backend ou cursors, sem depender de dados que só existem na sessão do líder. As três categorias são:
- PARALLEL UNSAFE (o padrão) — a função não pode rodar em um worker paralelo de jeito nenhum. Se uma query precisa chamar essa função, o PostgreSQL desabilita o paralelismo para a query inteira, não só para a parte que toca a função.
- PARALLEL RESTRICTED — a função pode rodar em um worker, mas só o processo líder tem permissão de realmente executá-la (ex.: funções que acessam tabelas temporárias ou estado de conexão do cliente). A query ainda pode ir paralela, mas essa chamada de função específica fica presa ao líder.
- PARALLEL SAFE — a função pode rodar em qualquer worker, sem restrições. É isso que você quer para lógica pura, somente leitura, sem efeitos colaterais.
Para um cálculo puro como age_in_months, a declaração correta é:
ALTER FUNCTION age_in_months(date, date) IMMUTABLE PARALLEL SAFE;
Note os dois atributos empilhados em uma única instrução — são flags independentes, e geralmente você quer definir ambos corretamente de uma vez em vez de corrigir um e esquecer o outro.
Por que o padrão te prejudica silenciosamente
Aqui está a parte que pega as pessoas de surpresa: o PostgreSQL não lança erro nem aviso quando uma query chama uma função PARALLEL UNSAFE. Ele simplesmente cai de volta para um plano serial, silenciosamente. Compare os dois:
-- Função nos padrões: VOLATILE, PARALLEL UNSAFE
EXPLAIN SELECT id, age_in_months(birth_date, CURRENT_DATE)
FROM animals;
-- Seq Scan on animals (sem Gather, sem workers — forçado serial)
-- Função marcada IMMUTABLE PARALLEL SAFE
EXPLAIN SELECT id, age_in_months(birth_date, CURRENT_DATE)
FROM animals;
-- Gather (workers planned: 2)
-- -> Parallel Seq Scan on animals
Mesma tabela, mesma query, mesmos dados — a única diferença são os atributos declarados da função. Em uma tabela grande, esse nó Gather ausente é a diferença entre um scan que usa um núcleo de CPU e um que usa quatro ou oito. Nada na query em si sugere que essa é a causa; você precisa checar \df+ age_in_months ou o plano do EXPLAIN da query para perceber.
Volatilidade e segurança paralela interagem, mas não são a mesma coisa
São definidas independentemente, mas há uma relação prática: uma função genuinamente VOLATILE (tem efeitos colaterais, ou depende de ordem de execução) quase nunca é segura para marcar PARALLEL SAFE, porque workers paralelos não garantem nenhuma ordem específica de execução nem quantidade de invocações. Na prática:
- Funções IMMUTABLE — geralmente seguras para também marcar
PARALLEL SAFE, já que não tocam estado de sessão ou de banco. - Funções STABLE que só fazem
SELECTem tabelas — geralmente seguras para marcarPARALLEL SAFEtambém, desde que não toquem tabelas temporárias, sequences, ou configurações locais de backend. - Funções VOLATILE com efeitos colaterais (escritas,
nextval(), notify, log) — devem permanecerPARALLEL UNSAFE. Marcar essas comoPARALLEL SAFEnão é uma otimização, é um bug de corretude: múltiplos workers poderiam executar a função concorrentemente em uma ordem que você nunca pretendeu.
Pegadinha: PARALLEL SAFE é uma promessa, não algo que o PostgreSQL verifica para você. Se você marcar uma função que chama nextval() ou escreve em uma tabela como PARALLEL SAFE, o PostgreSQL vai gerar um plano paralelo e rodar de boa — e você vai ter valores de sequence duplicados, race conditions, ou estado corrompido sem nenhuma mensagem de erro apontando para a função.
Um checklist antes de fazer ALTER FUNCTION
- Só lê, com resultado determinístico para a mesma entrada? →
IMMUTABLE. - Só lê, mas o resultado pode mudar entre instruções (ex.: depende de
now()ou conteúdo de tabela)? →STABLE. - Escreve, usa sequences, ou depende de estado de sessão/conexão? → deixe
VOLATILEePARALLEL UNSAFE. Não force paralelismo aqui. - É somente leitura e não toca tabelas temporárias, cursors, ou estado de cliente? → adicione
PARALLEL SAFE. - É somente leitura mas toca algo local ao worker como uma tabela temporária? →
PARALLEL RESTRICTED, nãoPARALLEL SAFE.
Confirmando que realmente funcionou
Cheque os atributos declarados da função diretamente:
SELECT proname, provolatile, proparallel
FROM pg_proc
WHERE proname = 'age_in_months';
-- provolatile: 'i' (immutable)
-- proparallel: 's' (safe)
Depois confirme que o plano realmente muda: rode EXPLAIN na query chamadora antes e depois do ALTER FUNCTION, em uma tabela grande o suficiente para o PostgreSQL considerar paralelismo em primeiro lugar (tabelas pequenas nunca disparam um plano paralelo independente dos atributos da função — o modelo de custo do planner tem um threshold mínimo de tamanho de tabela).
Por que isso importa em escala
Uma única função auxiliar rotulada errado usada em um punhado de queries administrativas é um erro de arredondamento. A mesma função chamada a partir de um relatório que escaneia milhões de linhas, uma vez por linha, é uma query que roda de quatro a oito vezes mais devagar do que precisaria — silenciosamente, para sempre, até que alguém cheque o EXPLAIN e note o nó Gather ausente. O PG Monitoring rastreia o plano de execução de cada query ao longo do tempo, então quando os atributos declarados de uma função mudam (ou nunca foram definidos corretamente) e uma query que costumava rodar em paralelo volta a ser serial, isso aparece como uma regressão de latência naquele fingerprint específico de query — não um mistério que você precisa redescobrir do zero.