13 de mar. de 2025·8 min de leitura

Colunas geradas vs gatilhos no PostgreSQL: o que usar

Colunas geradas vs gatilhos no PostgreSQL: escolha a abordagem certa para totais, status e valores normalizados considerando tradeoffs de velocidade e depuração.

Colunas geradas vs gatilhos no PostgreSQL: o que usar

Qual problema estamos tentando resolver com campos derivados?

Um campo derivado é um valor que você armazena ou expõe porque pode ser calculado a partir de outros dados. Em vez de repetir o mesmo cálculo em toda consulta e em todas as telas, você define a regra uma vez e a reutiliza.

Exemplos comuns são fáceis de imaginar:

  • order_total é a soma dos itens, menos descontos, mais impostos
  • um status como "paid" ou "overdue" baseado em datas e registros de pagamento
  • um valor normalizado, como um e-mail em minúsculas, um telefone sem espaços, ou uma versão do nome mais amigável para buscas

Times usam campos derivados porque as leituras ficam mais simples e consistentes. Um relatório pode selecionar order_total diretamente. O suporte pode filtrar por status sem copiar lógica complicada. Uma regra compartilhada também reduz pequenas diferenças entre serviços, dashboards e jobs em background.

Os riscos são reais, porém. O maior é dado desatualizado: as entradas mudam, mas o valor derivado não. Outro é lógica escondida: a regra vive num trigger, numa função ou numa migração antiga, e ninguém lembra que ela existe. Um terceiro é duplicação: você acaba com regras “quase iguais” em vários lugares, que divergirão ao longo do tempo.

É por isso que a escolha entre colunas geradas e gatilhos no PostgreSQL importa. Você não está escolhendo apenas como calcular um valor. Você está escolhendo onde a regra vive, quanto custa nas escritas e quão fácil é rastrear um número errado até sua causa.

O resto deste artigo olha três ângulos práticos: manutenibilidade (as pessoas conseguem entender e mudar?), velocidade de consulta (leituras, escritas, índices) e depuração (como descobrir por que um valor está errado).

Colunas geradas e gatilhos: definições simples

Quando as pessoas comparam colunas geradas e gatilhos no PostgreSQL, elas estão realmente escolhendo onde um valor derivado deve viver: dentro da definição da tabela, ou dentro de lógica procedural que roda quando os dados mudam.

Colunas geradas

Uma coluna gerada é uma coluna real da tabela cujo valor é calculado a partir de outras colunas na mesma linha. No PostgreSQL, colunas geradas são armazenadas (o banco salva o resultado calculado em disco) e mantidas atualizadas automaticamente quando as colunas referenciadas mudam.

Uma coluna gerada se comporta como uma coluna normal para consultas e indexação, mas você não escreve nela diretamente. Se você precisa de um valor calculado que não é armazenado, o PostgreSQL normalmente usa uma view (ou uma expressão na consulta) em vez de uma coluna gerada.

Gatilhos

Um gatilho é lógica que roda em eventos como INSERT, UPDATE ou DELETE. Triggers podem rodar BEFORE ou AFTER da mudança, e podem rodar por linha ou por statement.

Como triggers são código, eles podem fazer mais do que matemática simples. Podem atualizar outras colunas, escrever em outras tabelas, impor regras customizadas e reagir a mudanças em múltiplas linhas.

Uma forma útil de lembrar a diferença:

  • Colunas geradas servem cálculos previsíveis, por linha (totais, texto normalizado, flags simples) que devem sempre corresponder à linha atual.
  • Gatilhos servem regras que envolvem tempo, efeitos colaterais ou lógica entre linhas/tabelas (transições de status, logs de auditoria, ajustes de inventário).

Uma nota sobre constraints: constraints embutidas (NOT NULL, CHECK, UNIQUE, chaves estrangeiras) são claras e declarativas, mas limitadas. Por exemplo, um CHECK não pode depender de outras linhas via subquery. Quando uma regra depende de mais do que a linha corrente, você geralmente acaba com gatilhos ou um redesign.

Se você constrói com uma ferramenta visual como AppMaster, essa diferença mapeia bem para regras no estilo “fórmula do modelo de dados” versus regras de “processo de negócio” que rodam quando registros mudam.

Manutenibilidade: qual fica legível ao longo do tempo?

A principal diferença de manutenibilidade é onde a regra vive.

Uma coluna gerada mantém a lógica junto à definição dos dados. Quando alguém abre o schema da tabela, consegue ver a expressão que produz o valor.

Com gatilhos, a regra vai para uma função de trigger. Você também precisa saber quais tabelas e eventos a chamam. Meses depois, “legibilidade” muitas vezes significa: alguém consegue entender a regra sem vasculhar o banco? Colunas geradas normalmente vencem porque a definição é visível em um só lugar e há menos partes móveis.

Triggers ainda podem ser limpos se você mantiver a função pequena e focada. O problema começa quando uma função de trigger vira depósito de regras não relacionadas. Pode funcionar, mas fica difícil de raciocinar e arriscado de alterar.

Mudanças são outro ponto de pressão. Com colunas geradas, updates tipicamente são migrações que mudam uma expressão única. Isso é direto de revisar e reverter. Triggers frequentemente exigem mudanças coordenadas no corpo da função e na definição do trigger, além de passos extras para backfills e checagens de segurança.

Para manter regras descobríveis ao longo do tempo, alguns hábitos ajudam:

  • Nomeie colunas, triggers e funções segundo a regra de negócio que aplicam.
  • Adicione comentários curtos que expliquem a intenção, não só a matemática.
  • Mantenha funções de trigger pequenas (uma regra, uma tabela).
  • Mantenha migrações em controle de versão e exija revisões.
  • Liste periodicamente todos os triggers no schema e remova os que não são mais necessários.

A mesma ideia vale no AppMaster: prefira regras que você possa ver e auditar rapidamente, e minimize lógica de escrita “escondida”.

Velocidade de consultas: o que muda para leituras, escritas e índices?

A pergunta de performance é basicamente: você quer pagar o custo nas leituras ou nas escritas?

Uma coluna gerada é calculada quando a linha é escrita, então é armazenada. Leituras são rápidas porque o valor já está lá. A troca é que todo INSERT e UPDATE que toca os inputs também precisa computar o valor gerado.

Uma abordagem com trigger geralmente armazena o valor derivado em uma coluna normal e o mantém atualizado com o gatilho. Leituras também ficam rápidas, mas escritas podem ser mais lentas e menos previsíveis. Triggers adicionam trabalho por linha, e o overhead fica óbvio em atualizações em massa.

Indexação é onde valores derivados armazenados importam mais. Se você frequentemente filtra ou ordena por um campo derivado (um e-mail normalizado, um total, um código de status), um índice pode transformar um scan lento em uma busca rápida. Com colunas geradas, você pode indexar o valor gerado diretamente. Com triggers, você também pode indexar a coluna mantida, mas depende do trigger para mantê-la correta.

Se você calcula o valor na própria consulta (por exemplo, numa cláusula WHERE), pode precisar de um index de expressão para evitar recalcular para muitas linhas.

Importações em massa e grandes updates são pontos comuns de estrangulamento:

  • Colunas geradas adicionam um custo de cálculo consistente por linha afetada.
  • Triggers adicionam custo de cálculo mais overhead de trigger, e lógica mal escrita pode multiplicar esse custo.
  • Grandes updates podem transformar o trabalho do trigger no gargalo.

Uma forma prática de escolher é procurar hotspots reais. Se a tabela é muito lida e o campo derivado é usado em filtros, valores armazenados (gerados ou mantidos por trigger) mais um índice geralmente vencem. Se é pesada em escrita (eventos, logs), tenha cuidado ao adicionar trabalho por linha a menos que seja realmente necessário.

Depuração: encontrar a origem de valores errados

Make debugging easier
Create an internal admin panel to review inputs and trace why a derived value looks wrong.
Build Portal

Quando um campo derivado está errado, comece tornando o bug repetível. Capture o estado exato da linha que produziu o valor ruim, então rode o mesmo INSERT ou UPDATE numa transação limpa para não perseguir efeitos colaterais.

Uma forma rápida de reduzir o escopo é perguntar: o valor vem de uma expressão determinística ou de lógica em tempo de escrita?

Colunas geradas geralmente falham de maneira consistente. Se a expressão está errada, ela erra sempre para os mesmos inputs. Surpresas comuns são tratamento de NULLs (um NULL pode tornar todo o cálculo NULL), casts implícitos (texto para numérico) e casos de borda como divisão por zero. Se resultados diferem entre ambientes, procure diferenças em collation, extensões ou mudanças de schema que alteraram a expressão.

Triggers falham de formas mais bagunçadas porque dependem de tempo e contexto. Um trigger pode não disparar quando você espera (evento errado, tabela errada, cláusula WHEN ausente). Pode disparar múltiplas vezes via cadeias de triggers. Bugs também podem vir de configurações de sessão, search_path ou da leitura de outras tabelas que diferem entre ambientes.

Quando um valor derivado parece errado, esta checklist costuma ser suficiente para localizar a causa:

  • Reproduza com um INSERT/UPDATE mínimo e a menor linha de exemplo.
  • Selecione as colunas de entrada ao lado do campo derivado para confirmar os inputs.
  • Para colunas geradas, execute a expressão em um SELECT e compare.
  • Para triggers, adicione temporariamente RAISE LOGs ou escreva em uma tabela de debug.
  • Compare schema e definições de trigger entre ambientes.

Pequenos conjuntos de testes com resultados conhecidos reduzem surpresas. Por exemplo, crie dois pedidos: um com desconto NULL e outro com desconto 0, então confirme que os totais se comportam como esperado. Faça o mesmo para transições de status e verifique que acontecem apenas nas atualizações pretendidas.

Como escolher: um fluxo de decisão

Measure read vs write cost
Check bulk updates and indexes to choose the approach that fits your workload.
Run Tests

A melhor escolha normalmente fica clara quando você responde algumas perguntas práticas.

Passos 1–3: corretude primeiro, depois carga de trabalho

Responda nesta ordem:

  1. O valor precisa sempre bater com outras colunas, sem exceções? Se sim, aplique no banco em vez de confiar que a aplicação vai manter correto.
  2. A fórmula é determinística e baseada apenas em colunas da mesma linha (por exemplo, lower(email) ou price * quantity)? Se sim, uma coluna gerada é geralmente a opção mais limpa.
  3. Você lê esse valor com mais frequência (filtrando, ordenando, gerando relatórios) ou escreve nele com frequência (muitos inserts/updates)? Colunas geradas transferem custo para escritas, então tabelas com muitas escritas podem sentir isso antes.

Se a regra depende de outras linhas, outras tabelas ou lógica sensível ao tempo (por exemplo, “defina como overdue se não houver pagamento após 7 dias”), um gatilho costuma ser mais adequado porque pode rodar lógica mais rica.

Passos 4–6: indexação, testes e simplicidade

Agora decida como o valor será usado e verificado:

  1. Você vai filtrar ou ordenar por ele frequentemente? Se sim, planeje um índice e confirme que sua abordagem o suporta claramente.
  2. Como você vai testar e observar mudanças? Colunas geradas são mais fáceis de raciocinar porque a regra vive numa única expressão. Triggers precisam de testes direcionados e logging claro porque o valor muda “por fora”.
  3. Escolha a opção mais simples que satisfaça as restrições. Se uma coluna gerada resolve, geralmente é mais fácil de manter. Se precisar de regras entre linhas, mudanças em múltiplos passos ou efeitos colaterais, aceite o trigger, mas mantenha-o pequeno e bem nomeado.

Um bom veredicto intuitivo: se você consegue explicar a regra em uma frase e ela usa só a linha atual, comece com uma coluna gerada. Se você está descrevendo um workflow, provavelmente é caso de trigger.

Usando colunas geradas para totais e valores normalizados

Colunas geradas funcionam bem quando o valor é inteiramente derivado de outras colunas na mesma linha e a regra é estável. É aí que elas são mais simples: a fórmula fica na definição da tabela e o PostgreSQL mantém a consistência.

Exemplos típicos incluem valores normalizados (como um email em minúsculas e sem espaços) e totais simples (como subtotal + imposto - desconto). Por exemplo, uma tabela orders pode armazenar subtotal, tax e discount, e expor total como coluna gerada para que toda consulta veja o mesmo número sem depender da aplicação.

Ao escrever a expressão, mantenha-a defensiva:

  • Trate NULLs com COALESCE para que totais não virem NULL inesperadamente.
  • Faça casts intencionais para evitar mistura indesejada entre integers e numerics.
  • Arredonde num único lugar e documente a regra de arredondamento na expressão.
  • Torne regras de timezone e texto explícitas (lowercase, trim, substituir espaços).
  • Prefira algumas colunas auxiliares a uma fórmula gigante.

Indexar só vale a pena quando você realmente filtra ou junta pelo valor gerado. Indexar um total gerado muitas vezes é desperdício se você nunca busca por total. Indexar uma chave normalizada como email_normalized costuma valer a pena.

Mudanças de schema importam porque expressões geradas dependem de outras colunas. Renomear uma coluna ou mudar um tipo pode quebrar a expressão — e isso é bom: você descobre durante a migração em vez de escrever dados errados silenciosamente.

Se a fórmula começar a crescer demais (muitos CASEs, muitas regras de negócio), trate isso como sinal. Ou divida em colunas separadas, ou mude de abordagem para que a regra permaneça legível e testável. No AppMaster, colunas geradas funcionam melhor quando a regra é fácil de ver e explicar numa linha.

Usando gatilhos para status e regras entre linhas

Keep ownership of your app
Export real source code when you need full control over hosting and customization.
Export Code

Gatilhos são a ferramenta certa quando um campo depende de mais do que a linha atual. Campos de status são um caso comum: um pedido vira "paid" somente depois que existe pelo menos um pagamento bem-sucedido, ou um ticket fica "resolved" somente quando todas as tarefas estiverem feitas. Esse tipo de regra cruza linhas/tabelas, algo que colunas geradas não conseguem ler.

Um bom trigger é pequeno e sem frescura. Trate-o como um guardrail, não como uma segunda camada da aplicação.

Mantenha triggers previsíveis

Escritas escondidas é o que torna triggers difíceis de conviver. Uma convenção simples ajuda outros desenvolvedores a ver o que está acontecendo:

  • Um trigger para um propósito (atualizar status, não totais + auditoria + notificações).
  • Nomes claros (por exemplo, trg_orders_set_status_on_payment).
  • Timing consistente: use BEFORE para corrigir dados de entrada, AFTER para reagir a linhas salvas.
  • Mantenha a lógica numa única função, curta o suficiente para ler de uma vez.

Um fluxo realista: payments é atualizado para succeeded. Um AFTER UPDATE em payments atualiza orders.status para paid se o pedido tiver pelo menos um pagamento sucedido e nenhum saldo em aberto.

Casos de borda para planejar

Triggers se comportam diferente em alterações em massa. Antes de avançar, decida como fará backfills e reruns. Um job SQL único para recalcular status de dados antigos costuma ser mais claro do que disparar triggers linha a linha. Também vale definir um caminho seguro de reprocessamento, como uma procedure que recompute o status de um pedido. Pense em idempotência para que rodar o mesmo update várias vezes não mude estados indevidamente.

Por fim, verifique se uma constraint ou lógica de aplicação seria mais adequada. Para valores simples permitidos, constraints são mais claras. Em ferramentas como AppMaster, muitos fluxos também são mais fáceis de manter visíveis na camada de Business Process, deixando o trigger como uma rede de segurança estreita.

Erros comuns e armadilhas a evitar

Muito do sofrimento com campos derivados é auto-infligido. A maior armadilha é escolher a ferramenta mais complexa por padrão. Comece perguntando: isso pode ser expresso como uma expressão pura na mesma linha? Se sim, uma coluna gerada costuma ser a escolha mais tranquila.

Outro erro comum é deixar os triggers virarem uma segunda camada de aplicação. Começa com “só definir o status” e vira regras de preço, exceções e casos especiais. Sem testes, mudanças pequenas podem quebrar comportamentos antigos de formas silenciosas.

Armadilhas frequentes:

  • Usar trigger para um valor por linha quando uma coluna gerada seria mais clara e auto-documentada.
  • Atualizar um total armazenado num caminho de código (checkout) e esquecer outro (edições admin, imports, backfills).
  • Ignorar concorrência: duas transações atualizam as mesmas linhas e seu trigger sobrescreve ou aplica uma mudança em dobro.
  • Indexar todo campo derivado “só por precaução”, especialmente valores que mudam muito.
  • Armazenar algo que você poderia computar na leitura, como uma string normalizada raramente pesquisada.

Um exemplo simples: você armazena order_total_cents e também permite que o suporte ajuste itens. Se a ferramenta de suporte atualiza linhas mas não toca o total, o total fica desatualizado. Se você adiciona um trigger depois, ainda precisa lidar com linhas históricas e casos como reembolsos parciais.

Se constrói com uma ferramenta visual como AppMaster, a mesma regra vale: mantenha regras de negócio visíveis em um lugar. Evite espalhar atualizações de valores derivados por múltiplos fluxos.

Verificações rápidas antes de confirmar

Move trigger logic into flows
Use Business Processes for cross-table status updates instead of burying logic in triggers.
Build Workflow

Antes de escolher entre colunas geradas e gatilhos no PostgreSQL, faça um teste rápido da regra que quer armazenar.

Primeiro, pergunte de que a regra depende. Se pode ser calculada apenas a partir de colunas da mesma linha (um telefone normalizado, um email em lowercase, line_total = qty * price), uma coluna gerada costuma ser mais fácil de conviver porque a lógica fica ao lado da definição da tabela.

Se a regra depende de outras linhas ou tabelas (um status que muda quando chega o último pagamento, uma flag de conta baseada em atividade recente), você está em território de trigger, ou deveria calcular na consulta.

Uma checklist rápida:

  • O valor pode ser derivado só da linha atual, sem lookups?
  • Você precisa filtrar ou ordenar por ele com frequência?
  • Vai precisar recomputá-lo para dados históricos depois de mudar a regra?
  • Um desenvolvedor consegue encontrar a definição e explicá-la em menos de 2 minutos?
  • Você tem um pequeno conjunto de linhas de teste que prove a regra?

Pense também em operação. Atualizações em massa, imports e backfills são onde triggers surpreendem. Triggers disparam por linha a menos que você projete diferente, e erros surgem como cargas lentas, contenção de locks ou valores parcialmente atualizados.

Um teste prático: carregue 10.000 linhas numa tabela de staging, rode seu import usual e verifique o que é calculado. Depois atualize uma coluna de input chave e confirme que o valor derivado permanece correto.

Se você estiver construindo um app com AppMaster, o mesmo princípio vale: coloque regras simples por linha no banco como colunas geradas, e mantenha mudanças multi-etapa entre tabelas num lugar onde você pode testá-las repetidamente.

Um exemplo realista: pedidos, totais e um campo de status

Stop duplicated business rules
Keep calculations consistent across dashboards, APIs, and background jobs with one shared rule.
Get Started

Imagine uma loja simples. Você tem uma tabela orders com items_subtotal, tax, total e payment_status. O objetivo é que qualquer pessoa possa responder rápido: por que esse pedido ainda está sem pagamento?

Opção A: colunas geradas para totais, status armazenado normalmente

Para matemática de dinheiro que depende apenas de valores na mesma linha, colunas geradas são uma escolha limpa. Você pode armazenar items_subtotal e tax como colunas regulares e depois definir total como coluna gerada items_subtotal + tax. Isso mantém a regra visível na tabela e evita lógica de escrita escondida.

Para payment_status, você pode mantê-lo como coluna normal que a aplicação define quando cria um pagamento. Menos automático, mas direto de entender ao ler a linha.

Opção B: gatilhos para mudanças de status acionadas por pagamentos

Agora adicione uma tabela payments. O status deixa de depender só de uma linha em orders. Depende de registros relacionados como pagamentos bem-sucedidos, reembolsos e chargebacks. Um trigger em payments pode atualizar orders.payment_status sempre que um pagamento muda.

Se escolher esse caminho, planeje um backfill: um script único para recalcular payment_status de pedidos existentes e um job repetível que você possa rodar de novo se um bug escapar.

Quando o suporte investiga “por que este pedido está sem pagamento?”, a Opção A normalmente manda para a aplicação e seu histórico de auditoria. A Opção B também manda para a lógica do banco: o trigger disparou? Falhou? Pulou por conta de alguma condição?

Após o lançamento, observe alguns sinais:

  • updates lentos em payments (triggers adicionam trabalho nas escritas)
  • updates inesperados em orders (status mudando com mais frequência que o esperado)
  • linhas onde total parece correto, mas o status está errado (lógica dividida entre lugares)
  • deadlocks ou esperas de lock durante pico de pagamentos

Próximos passos: escolha a abordagem mais simples e mantenha regras visíveis

Escreva a regra em linguagem natural antes de mexer no SQL. “Order total equals sum of line items minus discount” é claro. “Status is paid when paid_at is set and balance is zero” é claro. Se você não consegue explicar em uma ou duas frases, provavelmente pertence a um lugar onde pode ser revisado e testado, não escondido num ajuste rápido no banco.

Se estiver em dúvida, trate como experimento. Construa uma cópia pequena da tabela, carregue um dataset que pareça com a realidade e tente ambas as abordagens. Compare o que realmente importa: consultas de leitura, velocidade de escrita, uso de índices e facilidade de entendimento depois.

Uma checklist compacta para decidir:

  • Prototipe ambas as opções e inspecione planos de consulta para leituras comuns.
  • Rode um teste pesado de escrita (imports, updates) para ver o custo de manter valores atualizados.
  • Adicione um script de teste que cubra backfills, NULLs, arredondamento e casos de borda.
  • Decida quem será dono da lógica a longo prazo (DBA, backend, produto) e documente a escolha.

Se você constrói uma ferramenta interna, visibilidade vale tanto quanto corretude. No AppMaster (appmaster.io), equipes costumam manter regras simples por linha próximas ao modelo de dados e colocar mudanças multi-etapa num Business Process, assim a lógica fica legível nas revisões.

Uma última coisa que economiza horas depois: documente onde está a verdade (tabela, trigger ou lógica da aplicação) e como recomputá-la com segurança se precisar de um backfill.

FAQ

What is a derived field, and when is it worth storing one?

Use a derived field when many queries and screens need the same value and you want one shared definition. It’s most helpful for values you frequently filter, sort, or display, like normalized keys, simple totals, or a consistent flag.

When should I choose a generated column in PostgreSQL?

Pick a generated column when the value is purely a function of other columns in the same row and should always match them. It keeps the rule visible in the table schema and avoids hidden write-time code paths.

When is a trigger the better choice than a generated column?

Use a trigger when the rule depends on other rows or other tables, or when you need side effects like updating a related record or writing an audit entry. Triggers are also a fit for workflow-style transitions where timing and context matter.

Can generated columns calculate values from other tables, like summing order line items?

Generated columns can only reference columns from the same row, so they can’t look up payments, line items, or other related records. If your “total” needs to sum child rows, you typically compute it in a query, maintain it with triggers, or redesign the schema so the needed inputs live on the same row.

Which is faster: generated columns or triggers?

A generated column stores the computed value at write time, so reads are fast and indexing is straightforward, but inserts and updates pay the compute cost. Triggers also shift work to writes, and can be slower and less predictable if the logic is complex or fires in chains during bulk updates.

Should I index a derived field like a total or normalized email?

Index when you frequently filter, join, or sort by that derived value and it meaningfully narrows results, such as a normalized email or status code. If you only display the value and never search by it, an index often adds write overhead without much benefit.

Which approach is easier to maintain over time?

Generated columns are usually easier to maintain because the logic lives in the table definition where people naturally look. Triggers can stay maintainable too, but only if each trigger has a narrow purpose, a clear name, and a small function that’s easy to review.

What are the most common causes of wrong values in generated columns or triggers?

For generated columns, the most common issues are NULL handling, type casting, and rounding rules that behave differently than expected. For triggers, issues often come from the trigger not firing, firing more than once, running in an unexpected order, or depending on session settings that vary across environments.

How do I debug a derived value that looks stale or incorrect?

Start by reproducing the exact insert or update that produced the bad value, then compare the input columns next to the derived value. For a generated column, run the same expression in a SELECT to confirm it matches; for a trigger, inspect the trigger and function definitions and add minimal logging to confirm when and how it runs.

What’s a simple decision rule for choosing between generated columns and triggers?

If you can say the rule in one sentence and it only uses the current row, a generated column is a strong default. If you’re describing a workflow or referencing related records, use a trigger or compute at read time, and keep the logic in one place you can test; in AppMaster, that often means simple row rules near the data model and cross-table workflows in a Business Process.

Fácil de começar
Criar algo espantoso

Experimente o AppMaster com plano gratuito.
Quando estiver pronto, você poderá escolher a assinatura adequada.

Comece