SQL CHECK

A Loja Aurora vai lançar cupons de desconto, e a dona tem uma regra: nenhum cupom passa de 50%. Ela pode pedir para todo mundo "tomar cuidado"... ou pode ensinar a regra ao próprio banco. O CHECK ("confira", "verifique") é isso: uma condição que o banco testa em toda linha que entra ou muda. Se a condição der falso, a linha é recusada, com uma mensagem de erro. Nesta aula você vai criar regras de preço, desconto e datas para as tabelas da Ana e ver o que o SQLite responde quando alguém tenta furar a regra.

Você vai aprender
  • Como escrever um CHECK numa coluna e no fim da tabela.
  • Que condições cabem num CHECK: comparações, BETWEEN, IN, funções como length(), e regras entre duas colunas.
  • O erro real CHECK constraint failed e como dar um nome à regra para a mensagem ficar clara.
  • A pegadinha do NULL: ele passa por qualquer CHECK.
  • A pegadinha dos tipos no SQLite: um texto pode passar por uma regra de número.
  • O que não pode ir num CHECK e o que fazer no lugar.

O CHECK é da família das regras que você já viu: NOT NULL, UNIQUE, PRIMARY KEY e FOREIGN KEY. Em inglês, essas regras se chamam constraints ("restrições"), palavra que aparece em todas as mensagens de erro. Os exemplos rodam de verdade no seu navegador, e cada Rodar começa do banco limpo.


A ideia: o fiscal na porta

Pense num fiscal na porta do estoque com uma prancheta. Na prancheta estão as regras: "preço tem de ser maior que zero", "desconto vai de 1 a 50". Cada mercadoria que entra (um INSERT) ou que tem a etiqueta trocada (um UPDATE) passa por ele. Se uma regra der "não", a mercadoria volta e ele diz qual regra foi.

O fiscal não confere o que já está lá dentro, nem quem sai: as regras valem para o que entra e o que muda. E ele só sabe olhar a própria mercadoria; não vai até outra sala conferir outra prateleira (isso é trabalho da chave estrangeira).


1. O primeiro CHECK

A Ana cria a tabela de cupons com a regra do desconto:

CREATE TABLE cupons (
    id       INTEGER PRIMARY KEY,
    codigo   TEXT NOT NULL,
    desconto REAL NOT NULL CHECK (desconto > 0 AND desconto <= 50)
);

INSERT INTO cupons (codigo, desconto)
VALUES ('NATAL', 15), ('BEMVINDO', 10);

SELECT * FROM cupons;
desconto REAL NOT NULL
A coluna, o tipo e a regra de não ficar vazia.
CHECK
"Confira": aqui começa a regra.
( … )
Os parênteses são obrigatórios. Dentro deles vai uma condição, escrita como a de um WHERE (aula WHERE).
desconto > 0 AND desconto <= 50
A condição: maior que zero e no máximo 50. Cada linha só entra se a condição der verdadeiro para ela.

Os dois cupons entraram. Agora alguém tenta criar a "Black Friday" com 70%:

CREATE TABLE cupons (
    id       INTEGER PRIMARY KEY,
    codigo   TEXT NOT NULL,
    desconto REAL NOT NULL CHECK (desconto > 0 AND desconto <= 50)
);

INSERT INTO cupons (codigo, desconto) VALUES ('BLACK', 70);

A resposta: CHECK constraint failed: desconto > 0 AND desconto <= 50, "a regra CHECK falhou", seguida da própria condição, para você saber qual regra foi. O cupom não foi gravado. No terminal do sqlite3, a Ana vê a mesma mensagem com o prefixo e o código do erro:

$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> CREATE TABLE cupons (id INTEGER PRIMARY KEY, codigo TEXT NOT NULL, desconto REAL NOT NULL CHECK (desconto > 0 AND desconto <= 50));
sqlite> INSERT INTO cupons (codigo, desconto) VALUES ('BLACK', 70);
Runtime error: CHECK constraint failed: desconto > 0 AND desconto <= 50 (19)
sqlite> .quit

Diferente da chave estrangeira, o CHECK não precisa ser ligado com pragma nenhum: ele vale sempre, no editor e no terminal.

Vale também no UPDATE

O fiscal confere a etiqueta trocada, não só a mercadoria nova:

CREATE TABLE cupons (
    id       INTEGER PRIMARY KEY,
    codigo   TEXT NOT NULL,
    desconto REAL NOT NULL CHECK (desconto > 0 AND desconto <= 50)
);
INSERT INTO cupons (codigo, desconto) VALUES ('NATAL', 15);

UPDATE cupons SET desconto = 60 WHERE codigo = 'NATAL';

Experimente: esta tabela não tem regra nenhuma, e o cupom de 80% entrou sem problema. Acrescente na coluna desconto um CHECK que só aceite valores de 1 a 50 (dica: BETWEEN 1 AND 50, da aula BETWEEN) e rode de novo. Agora o INSERT tem de ser recusado.

CREATE TABLE cupons (
    id       INTEGER PRIMARY KEY,
    codigo   TEXT NOT NULL,
    desconto REAL NOT NULL
);

INSERT INTO cupons (codigo, desconto) VALUES ('EXAGERO', 80);

SELECT * FROM cupons;
CREATE TABLE cupons (
    id       INTEGER PRIMARY KEY,
    codigo   TEXT NOT NULL,
    desconto REAL NOT NULL CHECK (desconto BETWEEN 1 AND 50)
);

INSERT INTO cupons (codigo, desconto) VALUES ('EXAGERO', 80);

SELECT * FROM cupons;

2. Que regras cabem num CHECK

Quase tudo que você escreve num WHERE sobre a própria linha serve. Alguns exemplos para a Loja Aurora:

RegraCHECK
Preço positivoCHECK (preco > 0)
Estoque nunca negativoCHECK (estoque >= 0)
Só 0 ou 1 (sim/não)CHECK (vip IN (0, 1))
Só alguns estadosCHECK (estado IN ('PI', 'CE', 'MA'))
Código com 4 a 12 letrasCHECK (length(codigo) BETWEEN 4 AND 12)
Código em maiúsculasCHECK (codigo = upper(codigo))
Email com arrobaCHECK (email LIKE '%@%')

Um exemplo com uma função: o código do cupom precisa ter de 4 a 12 letras. A função length() ("comprimento") conta as letras (aula Funções de Texto):

CREATE TABLE cupons (
    id       INTEGER PRIMARY KEY,
    codigo   TEXT NOT NULL CHECK (length(codigo) BETWEEN 4 AND 12),
    desconto REAL NOT NULL
);

INSERT INTO cupons (codigo, desconto) VALUES ('AB', 10);

Regra entre duas colunas: no fim da tabela

A Ana também vai cadastrar promoções, com data de início e de fim. A regra "o fim não pode ser antes do início" fala de duas colunas. Por isso ela vai no fim da tabela, depois de todas as colunas, separada por vírgula, como uma linha a mais:

CREATE TABLE promocoes (
    id     INTEGER PRIMARY KEY,
    nome   TEXT NOT NULL,
    inicio TEXT NOT NULL,
    fim    TEXT NOT NULL,
    CHECK (fim >= inicio)
);

INSERT INTO promocoes (nome, inicio, fim)
VALUES ('Natal', '2026-12-01', '2026-11-20');

As datas estão no formato AAAA-MM-DD (ano, mês, dia), o mesmo da Loja Aurora. Nesse formato, comparar como texto dá a mesma ordem que comparar como data: '2026-11-20' vem antes de '2026-12-01'. Por isso o >= funciona (mais em Datas e Horas).

Para o SQLite, tanto faz escrever o CHECK na coluna ou no fim: a documentação diz que "na prática, não faz diferença". O costume é deixar na coluna a regra que só fala dela, e no fim a que fala de várias.


3. Dando nome à regra

A mensagem CHECK constraint failed: desconto > 0 AND desconto <= 50 é boa para quem programa, mas uma tabela com cinco regras pode gerar mensagens compridas. Dá para dar um nome à regra com CONSTRAINT nome ("regra nome") antes do CHECK:

CREATE TABLE cupons (
    id       INTEGER PRIMARY KEY,
    codigo   TEXT NOT NULL,
    desconto REAL NOT NULL,
    CONSTRAINT desconto_valido CHECK (desconto BETWEEN 1 AND 50)
);

INSERT INTO cupons (codigo, desconto) VALUES ('ZERO', 0);
CONSTRAINT
"Regra": avisa que vem um nome.
desconto_valido
O nome que você escolheu. Use minúsculas e sublinhado, como nos nomes de coluna, e diga o que a regra garante.
CHECK (…)
A regra em si, igual a antes.

Agora a mensagem é CHECK constraint failed: desconto_valido. Num programa, você pode procurar esse nome na mensagem e mostrar ao usuário um aviso em português: "o desconto precisa estar entre 1% e 50%".


4. A pegadinha do NULL

O que acontece se o desconto vier vazio, sem NOT NULL na coluna?

CREATE TABLE cupons (
    id       INTEGER PRIMARY KEY,
    codigo   TEXT NOT NULL,
    desconto REAL CHECK (desconto > 0)
);

INSERT INTO cupons (codigo, desconto) VALUES ('MISTERIO', NULL);

SELECT * FROM cupons;

Entrou. NULL > 0 não dá nem verdadeiro nem falso: dá NULL, "desconhecido" (aula O Valor NULL). E a regra do CHECK é: a linha só é recusada quando a condição dá falso. Desconhecido passa. Por isso, quase sempre, o CHECK anda junto com o NOT NULL, como na seção 1.


5. A pegadinha dos tipos no SQLite

No SQLite, o tipo da coluna é uma preferência, não uma lei (aula Os Tipos no SQLite). Veja o que acontece com uma regra de número quando chega um texto:

CREATE TABLE estoque_loja (
    produto_id INTEGER NOT NULL,
    quantidade INTEGER NOT NULL CHECK (quantidade >= 0)
);

INSERT INTO estoque_loja VALUES (1, 'muitos');

SELECT quantidade, typeof(quantidade) FROM estoque_loja;

O texto 'muitos' entrou numa coluna INTEGER e ainda passou pelo quantidade >= 0. Para o SQLite, quando se compara um texto com um número, o texto é sempre "maior". Se você precisa garantir o tipo, há dois caminhos: um CHECK com typeof(), a função que diz o tipo do valor:

CREATE TABLE estoque_loja (
    produto_id INTEGER NOT NULL,
    quantidade INTEGER NOT NULL
               CHECK (typeof(quantidade) = 'integer' AND quantidade >= 0)
);

INSERT INTO estoque_loja VALUES (1, 'muitos');

Ou uma tabela STRICT ("rigorosa"), em que o SQLite recusa o tipo errado sozinho; ela é explicada em Os Tipos no SQLite.


6. Formas de fazer a mesma coisa

A regra "desconto de 1 a 50" pode ser escrita de vários jeitos. Todos funcionam:

FormaEscritaMensagem quando falhaQuando usar
Na colunadesconto REAL CHECK (desconto BETWEEN 1 AND 50)Mostra a condiçãoRegra que só fala daquela coluna. A mais comum.
No fim da tabelaCHECK (desconto BETWEEN 1 AND 50)Mostra a condiçãoRegras que comparam duas colunas.
Com nomeCONSTRAINT desconto_valido CHECK (…)Mostra o nomeQuando um programa vai ler a mensagem, ou a condição é comprida.
Várias regras numa colunaCHECK (desconto >= 1) CHECK (desconto <= 50)Mostra a que falhou primeiroQuando você quer saber exatamente qual limite foi furado.

E quando não usar CHECK: se a lista de valores permitidos muda com o tempo (as categorias da loja, por exemplo), não escreva a lista dentro do CHECK. Crie uma tabela com os valores e use uma chave estrangeira: para acrescentar um valor, basta um INSERT, sem mexer na estrutura da tabela.


Quando dá errado

CHECK constraint failed

A linha furou uma regra. A mensagem traz a condição ou o nome da regra. Se a linha fura duas regras ao mesmo tempo, o SQLite mostra só a primeira:

CREATE TABLE produtos_novos (
    nome    TEXT NOT NULL,
    preco   REAL CHECK (preco > 0),
    estoque INTEGER CHECK (estoque >= 0)
);
INSERT INTO produtos_novos VALUES ('Caneta', -1, -5);

Como consertar: corrija o valor e rode de novo; se der outra mensagem, era a segunda regra.

Esqueceu os parênteses
CREATE TABLE cupons (
    desconto REAL CHECK desconto > 0
);

A mensagem: near "desconto": syntax error, "erro de sintaxe perto de desconto". Depois de CHECK, o SQLite espera um parêntese.

Como consertar: CHECK (desconto > 0).

Uma coluna que não existe
CREATE TABLE cupons (
    id       INTEGER PRIMARY KEY,
    desconto REAL CHECK (descont > 0)
);

A mensagem: no such column: descont, "não existe a coluna descont". Faltou o o. Ao contrário da chave estrangeira, aqui o erro aparece já no CREATE TABLE.

Consultar outra tabela dentro do CHECK

A Ana quis garantir que o produto da promoção existe:

CREATE TABLE promocoes (
    produto_id INTEGER CHECK (produto_id IN (SELECT id FROM produtos)),
    desconto   REAL
);

A mensagem: subqueries prohibited in CHECK constraints, "subconsultas são proibidas em regras CHECK". O CHECK só enxerga a própria linha.

Como consertar: para "este número tem de existir em outra tabela", a ferramenta certa é a chave estrangeira: produto_id INTEGER REFERENCES produtos(id).

Acrescentar um CHECK numa tabela que já existe

A Ana quer proteger o preço dos produtos que já estão no banco:

ALTER TABLE produtos
ADD CONSTRAINT preco_positivo CHECK (preco > 0);

A mensagem: near "CONSTRAINT": syntax error. O SQLite não deixa acrescentar uma regra a uma tabela pronta. Outros bancos deixam (veja a nota abaixo).

Como consertar: no SQLite, é preciso criar uma tabela nova, já com a regra, copiar os dados e trocar uma pela outra. A aula ALTER TABLE mostra o roteiro completo. Na Loja Aurora isso funcionaria, porque o menor preço é 1.15 e o menor estoque é 0: nenhum produto atual fura as regras.


Nos outros bancos

O CHECK é do SQL padrão, e a escrita CHECK (condição) e CONSTRAINT nome CHECK (condição) é a mesma no MySQL, no PostgreSQL e no SQL Server. Também neles uma condição que dá NULL passa. Duas diferenças (trechos ilustrativos, não rodam no SQLite):

-- PostgreSQL, MySQL e SQL Server: acrescentar a regra numa tabela pronta
ALTER TABLE produtos ADD CONSTRAINT preco_positivo CHECK (preco > 0);

-- MySQL: criar a regra sem que ela seja conferida
CREATE TABLE cupons (
    desconto DECIMAL(5,2),
    CONSTRAINT desconto_valido CHECK (desconto BETWEEN 1 AND 50) NOT ENFORCED
);

A mensagem de erro muda de banco para banco. No MySQL, se você não der nome à regra, ele mesmo cria um (do tipo cupons_chk_1), e é esse nome que aparece no erro: mais um motivo para dar nomes você mesmo.


Referência rápida

EscritaO que faz
coluna TIPO CHECK (condição)Regra numa coluna.
CHECK (condição) no fim do CREATE TABLERegra da tabela; pode comparar colunas.
CONSTRAINT nome CHECK (condição)Regra com nome; a mensagem de erro mostra o nome.
Quando é conferidoEm cada INSERT e UPDATE. Não no SELECT nem no DELETE.
Condição que dá NULLPassa. Junte NOT NULL se o vazio não for aceito.
Não pode terSubconsulta (SELECT).
Ligar/desligarVale sempre. PRAGMA ignore_check_constraints = ON; desliga temporariamente (evite).
MensagemQuer dizer
CHECK constraint failed: desconto > 0A linha furou a regra mostrada.
CHECK constraint failed: desconto_validoA linha furou a regra com esse nome.
Runtime error: CHECK constraint failed: … (19)O mesmo, no terminal do sqlite3.
near "desconto": syntax errorFaltaram os parênteses depois de CHECK.
subqueries prohibited in CHECK constraintsNão pode SELECT dentro do CHECK; use chave estrangeira.
near "CONSTRAINT": syntax error (no ALTER TABLE)O SQLite não acrescenta regras a uma tabela pronta.

Exercícios

1. Preço positivo

Complete a regra para que o preço seja sempre maior que zero:

preco REAL NOT NULL (preco > 0)

2. Só sim ou não

Complete para que a coluna vip só aceite 0 ou 1:

vip INTEGER NOT NULL CHECK (vip (0, 1))

3. A coluna é desconto REAL CHECK (desconto > 0), sem NOT NULL. O que acontece com INSERT INTO cupons (desconto) VALUES (NULL);?

4. Onde a regra "a data de fim não pode ser antes da de início" precisa ser escrita?

5. A Ana escreveu CHECK (categoria_id IN (SELECT id FROM categorias)). O que o SQLite responde, e qual é a ferramenta certa?

6. Quando o CHECK é conferido?

Missão final: a promoção com datas trocadas

Missão: a tabela de promoções aceitou uma promoção que termina antes de começar. Acrescente, no fim da tabela, uma regra com o nome periodo_valido que exija fim >= inicio. Rode: o INSERT tem de ser recusado com a mensagem CHECK constraint failed: periodo_valido.

CREATE TABLE promocoes (
    id     INTEGER PRIMARY KEY,
    nome   TEXT NOT NULL,
    inicio TEXT NOT NULL,
    fim    TEXT NOT NULL
);

INSERT INTO promocoes (nome, inicio, fim)
VALUES ('Natal', '2026-12-01', '2026-11-20');

SELECT * FROM promocoes;
CREATE TABLE promocoes (
    id     INTEGER PRIMARY KEY,
    nome   TEXT NOT NULL,
    inicio TEXT NOT NULL,
    fim    TEXT NOT NULL,
    CONSTRAINT periodo_valido CHECK (fim >= inicio)
);

INSERT INTO promocoes (nome, inicio, fim)
VALUES ('Natal', '2026-12-01', '2026-11-20');

SELECT * FROM promocoes;

Depois de cumprir a missão, troque as datas do INSERT para '2026-12-01' e '2026-12-24' e veja a promoção entrar.


Resumo

  • CHECK (condição) é uma regra que o banco confere em cada INSERT e UPDATE; se a condição der falso, a linha é recusada com CHECK constraint failed.
  • Os parênteses são obrigatórios. A condição se escreve como num WHERE: comparações, BETWEEN, IN, LIKE, funções.
  • Regra de uma coluna vai na coluna; regra que compara colunas vai no fim da tabela.
  • CONSTRAINT nome CHECK (…) dá nome à regra, e a mensagem de erro passa a mostrar o nome.
  • Condição que dá NULL passa: junte NOT NULL. No SQLite, um texto pode passar por uma regra de número; use typeof() ou uma tabela STRICT.
  • Nada de subconsulta num CHECK (use chave estrangeira), e no SQLite não dá para acrescentar um CHECK com ALTER TABLE: é preciso recriar a tabela.