Coluna gerada é uma coluna computada
Uma coluna gerada (ou coluna computada) é uma coluna cujo valor vem de uma expressão, e não de um INSERT. Você declara a fórmula uma única vez no CREATE TABLE e o SQLite cuida do resto. Não dá para escrever nela — qualquer tentativa resulta em erro.
O exemplo mais simples possível:
total nunca foi inserido, mas aparece em toda linha. O SQLite recalcula esse valor a partir de price + tax toda vez que você lê o registro. Atualize qualquer uma das colunas e total acompanha.
A expressão GENERATED ALWAYS AS é obrigatória. O ALWAYS é só uma formalidade do padrão SQL — no SQLite não existe outra opção.
SQLite virtual vs stored: qual usar?
Toda coluna gerada no SQLite tem um de dois sabores. O padrão é VIRTUAL:
O modelo mental:
VIRTUAL— não ocupa nenhum byte em disco, mas gasta CPU a cada leitura. Barata de criar e fácil de alterar depois.STORED— ocupa espaço em disco e não tem custo extra na leitura. Vale a pena quando a expressão é cara ou quando a coluna é lida muito mais do que escrita.
Se você não especificar nada, o SQLite assume VIRTUAL. E esse costuma ser o padrão certo na grande maioria dos casos.
Por que usar? Valores derivados indexáveis
O grande trunfo é que dá pra criar um índice em coluna gerada. Isso te dá buscas rápidas em valores derivados sem precisar reescrever consulta nenhuma.
Imagine que você queira buscar e-mails ignorando maiúsculas e minúsculas:
O índice cobre a versão em minúsculas. Uma consulta que filtra por email_lower usa o índice direto. O SQLite até tem índices em expressão (CREATE INDEX ... ON users(lower(email))), mas uma coluna gerada deixa o valor derivado visível como uma coluna de verdade, que você pode usar no SELECT, referenciar em views e reaproveitar no código da aplicação.
Extraindo valores de JSON
As colunas geradas brilham quando o assunto é JSON. O suporte a JSON no SQLite oferece o operador ->> para extrair um valor escalar; embrulhe isso numa coluna gerada e você ganha um campo tipado e indexável em cima de um blob flexível.
user_id e kind aparecem como colunas normais nas suas queries, mas o dado real fica em payload. Se você mudar o JSON, as colunas se atualizam sozinhas. E o índice em user_id deixa a busca rápida.
Regras e restrições
Tem algumas coisas que o SQLite impõe — vale conhecer antes de esbarrar nelas:
- A expressão precisa ser determinística. Funções como
random(),datetime('now')e outras não-determinísticas não são permitidas. O valor tem que ser reproduzível a partir da linha. - A expressão só pode referenciar colunas da mesma linha. Nada de subqueries, agregações ou outras tabelas.
- Você não pode fazer
INSERTnemUPDATEdireto em uma coluna gerada.INSERT INTO products (total) VALUES (5)dá erro. - Colunas
STOREDnão podem ser adicionadas viaALTER TABLE ... ADD COLUMN. Só dá pra adicionarVIRTUALdepois que a tabela já existe. - Colunas geradas aceitam
NOT NULL,CHECK,UNIQUEe atéFOREIGN KEY. Para essas restrições, elas se comportam como qualquer outra coluna.
Uma demonstração rápida da regra de escrita:
sqlite> INSERT INTO products (price, tax, total) VALUES (10, 1, 999);
Runtime error: cannot INSERT into generated column "total"
A solução é tirar a coluna gerada da lista do INSERT e deixar o SQLite calcular o valor sozinho.
VIRTUAL ou STORED: qual escolher?
Na prática, a escolha depende da relação entre leituras e escritas e do custo da expressão:
Regras práticas:
- Por padrão, vai de
VIRTUAL. Não custa nada na escrita e dá conta do recado em quase tudo. - Mude para
STOREDquando você for criar índice na coluna em uma tabela com muita escrita (o índice já precisa do valor persistido de qualquer jeito), ou quando a expressão for realmente cara de calcular. - Não fique agonizando com isso. O tipo faz parte do schema, mas dá para dropar e recriar a coluna se você mudar de ideia — pelo menos no caso de
VIRTUAL.
Coluna computada no SQLite vs views
Existe uma sobreposição com views: as duas expõem valores calculados sem armazená-los (bom, às vezes). A divisão costuma ser assim:
- Uma coluna gerada pertence a uma linha e uma tabela. Use quando a derivação é por linha — formatar um email, extrair um campo JSON, calcular um total.
- Uma view é uma query salva. Use quando o cálculo envolve joins, agregação ou filtros entre várias linhas.
Dá para combinar as duas. Uma view pode fazer SELECT em uma tabela que tem colunas geradas e ainda dar join com contexto extra. Colunas geradas vivem na camada de armazenamento; views vivem na camada de consulta.
A seguir: ATTACH DATABASE
As colunas geradas permitem que uma tabela calcule seus próprios valores. A próxima página vai no sentido oposto: conectar vários bancos SQLite ao mesmo tempo com ATTACH DATABASE, para que uma única query possa percorrer arquivos diferentes.
Perguntas frequentes
O que é uma generated column no SQLite?
É uma coluna cujo valor é calculado a partir de uma expressão envolvendo outras colunas da mesma linha. Você declara usando GENERATED ALWAYS AS (expressão) no CREATE TABLE. Não dá para escrever nela diretamente — o SQLite calcula o valor sozinho, seja na hora de ler ou de gravar a linha.
Qual a diferença entre generated columns VIRTUAL e STORED?
Uma coluna VIRTUAL é recalculada a cada leitura e não ocupa espaço em disco — é o padrão. Já uma STORED é calculada uma única vez no momento da escrita e gravada no banco, o que deixa as leituras mais baratas em troca de uma gravação um pouco mais cara. As duas podem ser indexadas, mas a STORED costuma fazer mais sentido quando a expressão é pesada ou quando a coluna é lida muito mais do que escrita.
Dá para criar índice em uma generated column no SQLite?
Dá sim. O CREATE INDEX funciona normalmente em generated columns, tanto VIRTUAL quanto STORED. Esse é justamente o principal motivo de usar esse recurso: você indexa um valor derivado (como lower(email) ou um campo de JSON extraído com ->>) e o planejador de consultas passa a aproveitar esse índice sem precisar reescrever cada query.
Posso usar ALTER TABLE para adicionar uma generated column?
Pode, mas só para colunas VIRTUAL. O comando ALTER TABLE ... ADD COLUMN ... GENERATED ALWAYS AS (...) VIRTUAL funciona sem problema. Já adicionar uma coluna STORED via ALTER TABLE não é suportado — nesse caso, você precisaria recriar a tabela. Então, se você quer colunas armazenadas em tabelas que já existem, é melhor planejar isso desde o início.