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;
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);
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.
ALTER TABLE orders
ADD COLUMN age_days int
GENERATED ALWAYS AS (now() - created_at) STORED;
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.
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.00Voce nao pode escrever em
totaldiretamente — 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
totalusa o indice diretamente, sem reavaliaramount * (1 + tax_rate).Restricoes para lembrar
A expressao precisa ser deterministica e depender apenas da linha atual:
IMMUTABLE.now(),random(), referencias a outras tabelas e subconsultas sao rejeitadas.STORED; nao ha colunas geradasVIRTUALantes da versao 18.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 immutablePegadinha:
lower()e marcada comoIMMUTABLE, mas seu resultado depende doLC_COLLATEdo banco. Se voce mudar a collation depois, os valores armazenados ememail_normterao 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:
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.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
STOREDquantoVIRTUAL(o padrao eVIRTUAL). A sintaxe etotal AS (amount * 1.2) STORED; ambos os tipos podem ser indexados.MATERIALIZED; elas nao aparecem emSELECT *e sao calculadas apenas na insercao.No PostgreSQL, fique com
STOREDe expressoesIMMUTABLE, e o valor derivado continuara coerente sem uma unica linha de codigo na aplicacao.