sqlpostgresqlgenerated-columnsmysql

Colunas geradas no PostgreSQL: GENERATED ALWAYS AS STORED

Como manter um valor derivado em um unico lugar com GENERATED ALWAYS AS (...) STORED, indexa-lo e escolhe-lo no lugar de um trigger ou uma view.

3 min de leituraReferencesql · postgresql · generated-columns · mysql · clickhouse

Um valor derivado — um imposto, o total de um pedido, um email normalizado — costuma se duplicar pela aplicacao e divergir com o tempo. Uma coluna gerada o calcula dentro da tabela, de modo que o valor permanece coerente com os dados de origem e vive em um unico lugar.

O que GENERATED ALWAYS AS faz

Uma coluna gerada e uma coluna cujo valor o banco calcula a partir de outras colunas da mesma linha, por meio de uma expressao fixa. No PostgreSQL ela e sempre STORED: o resultado e gravado fisicamente em disco como qualquer coluna e recalculado sempre que os campos de origem sao inseridos ou atualizados.

CREATE TABLE orders (
  id         bigint PRIMARY KEY,
  user_id    bigint NOT NULL,
  amount     numeric(12,2) NOT NULL,
  status     text NOT NULL,
  created_at timestamptz NOT NULL DEFAULT now(),
  tax_rate   numeric(4,3) NOT NULL DEFAULT 0.20,
  total      numeric(12,2) GENERATED ALWAYS AS (amount * (1 + tax_rate)) STORED
);

INSERT INTO orders (id, user_id, amount, status)
VALUES (1, 100, 50.00, 'paid');

SELECT amount, tax_rate, total FROM orders WHERE id = 1;
-- 50.00 | 0.200 | 60.00

Voce nao pode escrever em total diretamente — isso e tarefa do servidor:

INSERT INTO orders (id, user_id, amount, status, total)
VALUES (2, 101, 10.00, 'paid', 12.00);
-- ERROR: cannot insert a non-DEFAULT value into column "total"

Uma unica fonte de verdade

O ganho real e um invariante no nivel do esquema. Todo cliente — o backend, um job de ETL, uma sessao avulsa de psql — ve o mesmo valor, e ninguem consegue esquecer de recalcula-lo. Isso e pratico para normalizacao:

ALTER TABLE users
  ADD COLUMN email_norm text
  GENERATED ALWAYS AS (lower(btrim(email))) STORED;

SELECT id FROM users WHERE email_norm = lower(btrim('  Bob@Example.COM '));

Agora as buscas por email ignoram maiusculas e espacos, e voce nao precisa mais normalizar a coluna em cada consulta.

Indexar uma coluna gerada

Como o valor e STORED, voce pode indexa-lo como qualquer outra coluna — em geral mais limpo do que um indice de expressao sobre a formula crua.

CREATE INDEX idx_orders_total ON orders (total);
CREATE INDEX idx_users_email_norm ON users (email_norm);

EXPLAIN SELECT * FROM orders WHERE total > 1000;

Uma consulta por faixa sobre total usa o indice diretamente, sem reavaliar amount * (1 + tax_rate).

Restricoes para lembrar

A expressao precisa ser deterministica e depender apenas da linha atual:

  • So sao permitidas funcoes IMMUTABLE. now(), random(), referencias a outras tabelas e subconsultas sao rejeitadas.
  • Ela pode referenciar somente colunas da mesma linha — e nao outras colunas geradas.
  • O PostgreSQL suporta apenas STORED; nao ha colunas geradas VIRTUAL antes da versao 18.
  • Voce nao pode atribuir o valor manualmente, apenas via DEFAULT.
-- Falha: now() nao e IMMUTABLE
ALTER TABLE orders
  ADD COLUMN age_days int
  GENERATED ALWAYS AS (now() - created_at) STORED;
-- ERROR: generation expression is not immutable

Pegadinha: lower() e marcada como IMMUTABLE, mas seu resultado depende do LC_COLLATE do banco. Se voce mudar a collation depois, os valores armazenados em email_norm terao de ser reconstruidos a mao — o servidor nao os recalcula sozinho.

Quando um trigger ou uma view e melhor

Uma coluna gerada brilha com uma formula pura e local a linha. Alem disso:

  • Um trigger BEFORE INSERT/UPDATE — quando voce precisa de dados de outras tabelas, valores nao deterministicos (now()) ou logica complexa. O preco e manter o invariante voce mesmo.
  • Uma VIEW — quando o valor derivado so e necessario na leitura e nao deve ocupar espaco nem entrar em um indice. E calculado em tempo de consulta e sempre esta atualizado.
CREATE VIEW order_totals AS
SELECT id, amount, amount * (1 + tax_rate) AS total
FROM orders;

Regra pratica: formula deterministica de uma unica linha, use uma coluna gerada; dados externos ou escrita manual, um trigger; somente leitura sem armazenamento, uma view.

Diferencas em outros bancos

  • MySQL/MariaDB: suportam tanto STORED quanto VIRTUAL (o padrao e VIRTUAL). A sintaxe e total AS (amount * 1.2) STORED; ambos os tipos podem ser indexados.
  • ClickHouse: o equivalente sao as colunas MATERIALIZED; elas nao aparecem em SELECT * e sao calculadas apenas na insercao.

No PostgreSQL, fique com STORED e expressoes IMMUTABLE, e o valor derivado continuara coerente sem uma unica linha de codigo na aplicacao.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador