flowchart TD
I[Integridade dos dados]
E1[Erro humano] --> I
E2[Falha de software] --> I
E3[Falha de hardware] --> I
E4[Ataque malicioso] --> I
E5[Erro de transmissao] --> I
E6[Concorrencia mal gerida] --> I
I --> D1[Restricoes de integridade]
I --> D2[Transacoes ACID]
I --> D3[Controle de acesso]
I --> D4[Backup e recuperacao]
Integridade de dados e restrições
Quero começar este módulo te fazendo uma pergunta que parece simples, mas que sustenta toda a confiança que depositamos num banco de dados: por que você acredita nos números que vê numa consulta? Quando o sistema da faculdade mostra que você está matriculado em seis disciplinas, ou quando o banco diz que sua conta tem determinado saldo, por que confiamos nessa informação a ponto de tomar decisões com base nela? A resposta está num conceito que parece técnico, mas que é, na verdade, o coração da confiabilidade de qualquer sistema de informação: a integridade de dados. E é exatamente por isso que ela cai em prova com tanta frequência. O examinador da USP sabe que um candidato que domina restrições de integridade entende o modelo relacional de verdade, e não apenas decorou comandos SQL. Vou te conduzir por esse tema com calma, mostrando o conceito, as ameaças que ele enfrenta, e principalmente os tipos de restrição que o SGBD nos oferece para defender os dados, com muitos exemplos em PostgreSQL que você poderá reproduzir.
O que é integridade de dados e por que ela sustenta o banco
Quando falo em integridade de dados, estou me referindo à propriedade de os dados permanecerem precisos, consistentes, completos e confiáveis ao longo de todo o seu ciclo de vida, não importa quantas vezes sejam lidos, gravados, atualizados ou transmitidos. Repare comigo nessas quatro qualidades, porque elas resumem tudo. Um dado preciso reflete fielmente a realidade que representa; um dado consistente não se contradiz dentro do próprio banco; um dado completo não tem omissões indevidas; e um dado confiável serve como base segura para decisões. Quando qualquer uma dessas qualidades se rompe, dizemos que a integridade foi violada, e o banco deixa de ser uma fonte de verdade para se tornar uma fonte de dúvida.
Definição. Integridade de dados é a garantia de que os dados armazenados são precisos, consistentes, completos e confiáveis durante todo o seu ciclo de vida, resistindo a operações de inserção, atualização, exclusão e transmissão sem se corromper.
Por que isso é tão decisivo? Pense numa organização que toma decisões com base em relatórios extraídos do banco. Se os dados estão corrompidos, as decisões serão erradas, por melhor que seja a equipe. Há também a dimensão da conformidade regulatória: regulamentos como a LGPD no Brasil, ou GDPR e HIPAA no exterior, exigem que as informações sejam mantidas íntegras e rastreáveis. Há a eficiência operacional, porque dados corretos eliminam o retrabalho de corrigir cadastros e reconciliar planilhas. E há a segurança da informação, em que a integridade é um dos três pilares clássicos da tríade CIA, ao lado da confidencialidade e da disponibilidade. Em outras palavras, a integridade não é um detalhe de implementação: é uma das razões de existir do próprio SGBD.
As ameaças à integridade
Para entender por que precisamos de mecanismos de defesa, preciso te mostrar contra o que estamos lutando. As ameaças à integridade vêm de muitas direções, e algumas são bem mais sutis do que parecem. O erro humano é a fonte mais comum: alguém digita um CPF errado, exclui acidentalmente um registro ou atualiza o campo errado. Falhas de software entram em seguida, quando um bug numa aplicação grava dados inconsistentes. Falhas de hardware, como um setor defeituoso num disco, podem corromper dados fisicamente. Ataques maliciosos, incluindo injeção de SQL, tentam alterar dados para proveito de terceiros. Problemas de transmissão corrompem dados durante a transferência entre sistemas. E, talvez a ameaça mais traiçoeira de todas, as atualizações concorrentes: em ambientes multiusuário, duas operações simultâneas podem deixar o banco num estado inconsistente se não forem coordenadas.
Repare que o diagrama mostra dois lados: as ameaças que pressionam de cima e os mecanismos de defesa que o SGBD oferece de baixo. Neste módulo vou me concentrar no primeiro mecanismo, as restrições de integridade, porque elas são a linha de defesa mais elegante e poderosa. As transações ACID você verá com profundidade no módulo de controle de concorrência, e os triggers e procedures terão atenção especial no módulo 14. Mas antes de chegar às restrições propriamente ditas, preciso te lembrar por que o modelo relacional é o terreno ideal para implementá-las.
A relação entre o modelo relacional e a integridade
Quando Edgar F. Codd propôs o modelo relacional em 1970, ele não estava apenas inventando uma forma de guardar dados em tabelas. Ele estava criando uma estrutura matemática rigorosa em que a integridade pudesse ser declarada, e não apenas programada. Essa diferença entre declarar e programar é o que torna o modelo relacional tão especial para o nosso tema. Numa abordagem declarativa, eu digo ao banco qual regra deve valer, por exemplo que um preço precisa ser positivo, e o SGBD se encarrega de garantir essa regra em toda inserção e toda atualização, para sempre, independentemente de qual aplicação tocou os dados. Numa abordagem programada, eu teria que escrever essa verificação em cada lugar do código que grava preços, e bastaria esquecer de um único ponto para a integridade se romper.
O modelo relacional dá esse poder por causa de alguns fundamentos. Os dados ficam organizados em relações, que são as tabelas, onde cada tupla, ou linha, representa uma instância da entidade, e cada atributo, ou coluna, representa uma propriedade. Cada atributo tem um domínio, que é o conjunto de valores permitidos. As chaves identificam unicamente as tuplas, e os relacionamentos entre tabelas se expressam por chaves estrangeiras. Sobre essa estrutura, Codd assentou dois princípios de integridade que você precisa saber de cor: a integridade de entidade, que exige que toda tupla seja unicamente identificável e que a chave primária nunca seja nula, e a integridade referencial, que exige que toda referência entre tabelas aponte para algo que realmente exista.
Atenção para a prova. Integridade de entidade significa que a chave primária não pode ser nula nem repetida. Integridade referencial significa que uma chave estrangeira ou aponta para uma tupla existente na tabela referenciada ou é nula.
A partir desses fundamentos, podemos classificar as restrições em quatro grandes famílias. Vou te apresentar cada uma com a definição, o propósito e a implementação em PostgreSQL, porque é aí que a teoria vira prática.
Restrições de domínio
Começo pelas restrições de domínio porque são as mais básicas e, ao mesmo tempo, as mais subestimadas. Uma restrição de domínio define o conjunto de valores que um atributo pode legitimamente assumir. A forma mais elementar de restrição de domínio é o próprio tipo de dado: quando declaro uma coluna como INTEGER, já estou dizendo ao banco que ali não cabe texto livre; quando declaro DATE, estou exigindo uma data válida. Veja como o tipo já é a primeira muralha de defesa.
Mas o tipo de dado sozinho não basta. Posso querer, por exemplo, que o salário nunca seja negativo, ou que um status só assuma certos valores. Para isso uso três mecanismos complementares: a restrição CHECK, que define uma condição lógica que todo valor precisa satisfazer; o DEFAULT, que fornece um valor automático quando nenhum é informado; e o NOT NULL, que proíbe a ausência de valor. Observe como eles se combinam numa definição realista.
Repare em quantas regras de negócio ficaram cravadas no esquema com tão poucas linhas. O nome é obrigatório, o preço precisa ser positivo, o estoque começa em zero e nunca fica negativo, e a categoria só aceita um dos quatro valores previstos. Qualquer aplicação que tente violar essas regras receberá um erro do banco, sem que eu precise escrever uma única linha de validação na camada de aplicação. Esse é o poder da abordagem declarativa que mencionei. Vale notar uma sutileza sobre o NULL que costuma confundir: o valor nulo significa ausência de informação, e uma restrição CHECK só rejeita um valor quando a condição resulta explicitamente em falso. Se a coluna for nula e não houver NOT NULL, a condição do CHECK resulta em desconhecido, e o banco aceita. Por isso eu combino CHECK com NOT NULL quando quero realmente blindar a coluna.
Restrições de chave
Passo agora às restrições de chave, que estabelecem a identidade dos registros. A chave primária, declarada com PRIMARY KEY, identifica unicamente cada tupla da tabela e, por implicar a integridade de entidade, ela é automaticamente NOT NULL e única ao mesmo tempo. A chave única, declarada com UNIQUE, também garante unicidade, mas com uma diferença que cai em prova: ela admite valores nulos, e em geral admite vários nulos, porque dois nulos não são considerados iguais entre si no modelo relacional. Veja a distinção em ação.
Aqui o id é a chave primária, então é obrigatório e único. O cpf e o email são chaves candidatas alternativas: garantem que não haverá dois clientes com o mesmo CPF ou o mesmo e-mail, mas permitem que um cliente ainda sem cadastro de CPF tenha esse campo nulo. Em PostgreSQL, quando quero que o banco gere o identificador automaticamente, costumo usar uma coluna de identidade, que é a forma moderna e padronizada de fazer chaves surrogadas, ou seja, chaves artificiais sem significado de negócio.
Há ainda um caso importante: a chave composta, formada por mais de uma coluna. Ela é a forma natural de identificar tuplas em tabelas associativas, aquelas que representam um relacionamento muitos-para-muitos. Imagine um pedido que contém vários itens, e cada item identificado pela combinação do pedido com o produto.
Repare que coloquei a chave primária no nível de tabela, depois das colunas, porque ela abrange duas colunas ao mesmo tempo. Restrições que envolvem mais de uma coluna sempre precisam ser declaradas nesse nível. Essa decisão entre escrever a restrição junto à coluna ou no nível de tabela é puramente sintática quando a restrição é simples, mas torna-se obrigatória quando ela é composta.
Síntese rápida sobre chaves. A PRIMARY KEY é única e obrigatória, e só pode haver uma por tabela. A UNIQUE é única mas admite nulos, e pode haver várias por tabela. Ambas criam automaticamente um índice, o que acelera buscas por esses campos.
Restrições de integridade referencial
Chegamos ao mecanismo que, na minha experiência, mais separa quem entende o modelo relacional de quem apenas o usa. A integridade referencial garante que os relacionamentos entre tabelas permaneçam sempre consistentes. Ela se materializa na chave estrangeira, declarada com FOREIGN KEY, que é uma coluna ou conjunto de colunas de uma tabela que precisa corresponder à chave primária de outra tabela. A regra é simples e implacável: o valor de uma chave estrangeira ou aponta para uma tupla que realmente existe na tabela referenciada, ou é nulo. Nunca pode apontar para algo inexistente, pois isso criaria o que chamamos de referência órfã.
Com essa declaração, o banco passa a recusar qualquer pedido cujo cliente_id não exista na tabela clientes. Igualmente importante, ele passa a recusar a exclusão de um cliente que ainda tenha pedidos, porque isso deixaria pedidos apontando para o vazio. Veja a relação desenhada.
erDiagram
CLIENTES ||--o{ PEDIDOS : "faz"
PEDIDOS ||--|{ ITENS_PEDIDO : "contem"
PRODUTOS ||--o{ ITENS_PEDIDO : "aparece em"
CLIENTES {
int id PK
string nome
char cpf UK
}
PEDIDOS {
int id PK
int cliente_id FK
date data_pedido
}
ITENS_PEDIDO {
int pedido_id PK_FK
int produto_id PK_FK
int quantidade
}
PRODUTOS {
int id PK
string nome
numeric preco
}
Agora vem a parte mais rica: o que deve acontecer quando alguém tenta excluir ou atualizar uma tupla pai que tem filhos dependentes? O SQL nos dá um conjunto de ações referenciais que definimos nas cláusulas ON DELETE e ON UPDATE. A ação CASCADE propaga a operação para os filhos, então excluir o pai exclui também os filhos. A ação SET NULL coloca a chave estrangeira como nula, desligando o filho do pai sem apagá-lo. A ação SET DEFAULT faz o mesmo, mas usando o valor padrão da coluna. A ação RESTRICT proíbe a operação imediatamente se houver filhos. E a ação NO ACTION, que é o comportamento padrão, também proíbe, mas adia a verificação para o fim da transação, o que permite arranjos temporariamente inconsistentes que se resolvem antes do commit.
| Ação | O que faz ao excluir/atualizar o pai | Quando usar |
|---|---|---|
CASCADE |
Propaga a exclusão ou atualização aos filhos | Quando o filho não faz sentido sem o pai |
SET NULL |
Coloca a chave estrangeira como nula | Quando o filho pode existir sem pai |
SET DEFAULT |
Coloca a chave estrangeira no valor padrão | Quando há um pai padrão de fallback |
RESTRICT |
Bloqueia a operação na hora | Quando se quer impedir órfãos imediatamente |
NO ACTION |
Bloqueia, mas verifica no fim da transação | Padrão; útil com restrições adiáveis |
Veja como aplico isso num exemplo realista. Quando um pedido é excluído, faz sentido que seus itens desapareçam junto, porque um item não tem vida própria fora do pedido. Já quando excluo um produto do catálogo, talvez eu não queira apagar o histórico de itens vendidos, então prefiro bloquear a exclusão.
CREATE TABLE itens_pedido (
pedido_id INTEGER NOT NULL,
produto_id INTEGER NOT NULL,
quantidade INTEGER NOT NULL CHECK (quantidade > 0),
preco_unit NUMERIC(10,2) NOT NULL CHECK (preco_unit >= 0),
PRIMARY KEY (pedido_id, produto_id),
FOREIGN KEY (pedido_id) REFERENCES pedidos (id)
ON DELETE CASCADE
ON UPDATE CASCADE,
FOREIGN KEY (produto_id) REFERENCES produtos (id)
ON DELETE RESTRICT
ON UPDATE CASCADE
);Repare na escolha deliberada de ações diferentes para cada chave estrangeira na mesma tabela. Excluir um pedido leva seus itens junto, por CASCADE. Tentar excluir um produto que ainda aparece em itens é bloqueado, por RESTRICT. Essa decisão não é técnica, é de negócio, e por isso o examinador gosta tanto dela: ela testa se você entende as consequências práticas de cada opção. Vou ilustrar o fluxo de decisão que o SGBD percorre numa exclusão de pai.
flowchart TD
A[DELETE numa tupla pai] --> B{Existem filhos referenciando?}
B -->|Nao| C[Exclui normalmente]
B -->|Sim| D{Qual a acao ON DELETE?}
D -->|CASCADE| E[Exclui pai e filhos]
D -->|SET NULL| F[Anula a FK dos filhos]
D -->|SET DEFAULT| G[FK dos filhos vira o default]
D -->|RESTRICT / NO ACTION| H[Bloqueia e gera erro]
Cuidado com o efeito dominó. O CASCADE é poderoso, mas perigoso. Uma única exclusão pode disparar uma cadeia de exclusões em várias tabelas relacionadas. Use-o conscientemente, sabendo exatamente até onde a cascata pode chegar, ou você poderá apagar muito mais do que pretendia.
Restrições de integridade semântica
As três famílias anteriores cuidam da estrutura: tipos, identidade e referências. Mas há regras de negócio que vão além da estrutura, e é delas que cuida a integridade semântica, também chamada de integridade de negócio. São regras específicas do domínio que tornam os dados não apenas estruturalmente válidos, mas significativos no contexto da aplicação. A ferramenta declarativa para muitas dessas regras continua sendo o CHECK, mas agora num uso mais elaborado, inclusive comparando colunas entre si.
CREATE TABLE emprestimos (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
cliente_id INTEGER NOT NULL REFERENCES clientes (id),
valor NUMERIC(12,2) NOT NULL CHECK (valor > 0),
renda_mensal NUMERIC(12,2) NOT NULL CHECK (renda_mensal > 0),
data_inicio DATE NOT NULL,
data_fim DATE NOT NULL,
CHECK (valor <= renda_mensal * 0.3),
CHECK (data_fim > data_inicio)
);Observe duas regras semânticas reais cravadas no nível de tabela. A primeira impede que o valor do empréstimo ultrapasse trinta por cento da renda mensal, uma típica regra de crédito. A segunda exige que a data de fim seja posterior à de início, uma regra de coerência temporal que envolve duas colunas. Esse tipo de CHECK multicoluna precisa ficar no nível de tabela justamente porque referencia mais de uma coluna. O NOT NULL e o DEFAULT, que já vimos no domínio, também atuam no campo semântico quando expressam regras de negócio, como a obrigatoriedade de um campo ou um valor inicial significativo.
Existe um limite, porém, no que o CHECK consegue expressar. Por padrão, ele só enxerga a própria linha sendo inserida ou atualizada. Quando a regra precisa olhar para várias linhas, ou para outras tabelas, entramos no terreno das restrições verdadeiramente avançadas.
Restrições avançadas: assertions, triggers e procedures
O SQL padrão prevê um recurso elegante para regras que abrangem múltiplas tabelas: a ASSERTION, que declara uma condição que deve ser verdadeira para o banco inteiro o tempo todo. Imagine uma regra como a soma de todos os salários do departamento de RH não pode ultrapassar seu orçamento. Conceitualmente ela seria escrita assim.
Preciso ser honesto contigo sobre isso, porque é exatamente onde muita gente erra na prova: apesar de prevista no padrão SQL, a ASSERTION praticamente não é implementada pelos SGBDs comerciais, inclusive o PostgreSQL não a suporta. A razão é prática: verificar uma condição global a cada modificação de qualquer tabela envolvida seria extremamente custoso. Então, na vida real, regras desse tipo são implementadas por triggers e stored procedures, que são o assunto central do próximo módulo. Vou te dar apenas o gosto agora, para você ver o encaixe. Um trigger é um procedimento que o banco dispara automaticamente em resposta a um evento, como uma inserção, atualização ou exclusão, e dentro dele posso executar qualquer verificação, inclusive consultando outras tabelas.
CREATE OR REPLACE FUNCTION valida_idade_funcionario()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.data_nascimento > CURRENT_DATE - INTERVAL '18 years' THEN
RAISE EXCEPTION 'Funcionario deve ter ao menos 18 anos';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_valida_idade
BEFORE INSERT OR UPDATE ON funcionarios
FOR EACH ROW
EXECUTE FUNCTION valida_idade_funcionario();Note como o trigger me permite impor uma regra antes de cada inserção ou atualização, abortando a operação com uma exceção quando a condição falha. Uma stored procedure, por sua vez, encapsula lógica mais ampla que posso invocar para validar conjuntos de operações, como checar se um pedido inteiro respeita o limite de crédito do cliente antes de confirmá-lo. Vou aprofundar a sintaxe, os tipos de trigger e os casos de uso no módulo 14; por ora, o que preciso que você guarde é a hierarquia de escolha: prefira sempre a restrição declarativa mais simples que resolva o problema, e só suba para trigger ou procedure quando a regra realmente exigir.
flowchart TD
A[Preciso impor uma regra] --> B{Regra cabe num CHECK de uma linha?}
B -->|Sim| C[Use CHECK, NOT NULL ou DEFAULT]
B -->|Nao| D{Envolve identidade ou referencia?}
D -->|Sim| E[Use PRIMARY KEY, UNIQUE ou FOREIGN KEY]
D -->|Nao| F{Envolve varias linhas ou tabelas?}
F -->|Sim| G[Use trigger ou stored procedure]
Implementação e alteração de restrições em SQL
Quero fechar a parte prática mostrando que restrições não são definidas apenas no CREATE TABLE. Posso nomeá-las explicitamente, o que recomendo fortemente, porque um nome próprio faz a mensagem de erro do banco ficar legível e facilita remover ou alterar a restrição depois. Posso também adicioná-las a uma tabela já existente com ALTER TABLE. Veja como dar nome às restrições e como acrescentá-las depois.
CREATE TABLE contas (
id INTEGER GENERATED ALWAYS AS IDENTITY,
titular_id INTEGER NOT NULL,
saldo NUMERIC(14,2) NOT NULL DEFAULT 0,
CONSTRAINT pk_contas PRIMARY KEY (id),
CONSTRAINT ck_saldo_nao_negativo CHECK (saldo >= 0)
);
ALTER TABLE contas
ADD CONSTRAINT fk_contas_titular
FOREIGN KEY (titular_id) REFERENCES clientes (id)
ON DELETE RESTRICT;A coluna de prefixos que adotei, com pk para chave primária, ck para check e fk para chave estrangeira, é uma convenção que torna o esquema autoexplicativo. Quando o banco rejeitar uma operação, a mensagem citará o nome da restrição violada, e você saberá na hora qual regra de negócio foi tocada. Resumo na tabela a seguir o panorama completo das restrições para você fixar de uma vez.
| Tipo de restrição | Palavra-chave SQL | Garante | Nível de integridade |
|---|---|---|---|
| Domínio | tipo de dado, CHECK, DEFAULT, NOT NULL |
Valores dentro do permitido | Domínio |
| Chave primária | PRIMARY KEY |
Unicidade e não nulidade | Entidade |
| Chave alternativa | UNIQUE |
Unicidade, admite nulos | Entidade |
| Chave estrangeira | FOREIGN KEY ... REFERENCES |
Referências válidas | Referencial |
| Regra de negócio | CHECK multicoluna, trigger, procedure |
Coerência semântica | Semântica |
Os benefícios das restrições
Quero que você termine este módulo convencido de que restrições não são burocracia, e sim economia de esforço e de risco. O primeiro benefício é a consistência: restrições previnem anomalias, eliminam dados inválidos, impedem duplicações e barram referências órfãs antes mesmo que entrem no banco. O segundo é a confiabilidade, porque dados validados na origem produzem relatórios e análises em que se pode confiar, reduzindo o trabalho de limpeza de dados que tanto consome tempo. O terceiro é a centralização da lógica: ao colocar a regra no esquema, garanto que ela valha para toda aplicação que tocar os dados, sem duplicar validações em cada sistema, o que reduz código e elimina divergências entre aplicações. O quarto é a documentação implícita, já que ler as restrições de um esquema é ler as regras de negócio da organização. E o quinto, que muitos esquecem, é o desempenho: chaves primárias e únicas criam índices automaticamente, e o otimizador de consultas usa o conhecimento das restrições para gerar planos de execução melhores, além de fazer operações inválidas falharem rapidamente, sem desperdiçar processamento.
É claro que há um preço a pagar, e não seria honesto esconder. Restrições complexas adicionam custo de verificação em inserções e atualizações, podem virar gargalo em sistemas de altíssima escala, e nem todas são portáveis entre SGBDs diferentes. A arte está no equilíbrio: impor as regras essenciais no banco, manter validações de interface na aplicação, e medir o impacto antes de exagerar. Mas o saldo é amplamente favorável, e por isso bancos bem projetados são generosos em restrições.
Síntese para a prova. Integridade de dados é a garantia de que os dados permanecem precisos, consistentes, completos e confiáveis ao longo do tempo, e ela sustenta toda a confiabilidade do banco. As ameaças vêm de erro humano, falhas de software e hardware, ataques, transmissão e concorrência. O modelo relacional favorece a integridade por permitir restrições declarativas. Memorize os quatro tipos. As restrições de domínio usam tipo de dado, CHECK, DEFAULT e NOT NULL. As restrições de chave dividem-se em PRIMARY KEY, que é única e não nula e implica integridade de entidade, e UNIQUE, que é única mas admite nulos. As restrições de integridade referencial usam FOREIGN KEY apontando para a chave primária de outra tabela, com as ações ON DELETE e ON UPDATE, sendo CASCADE, SET NULL, SET DEFAULT, RESTRICT e NO ACTION as cinco opções; lembre que NO ACTION é o padrão e adia a verificação para o fim da transação. As restrições semânticas expressam regras de negócio com CHECK multicoluna e, quando preciso olhar várias linhas ou tabelas, com triggers e stored procedures. A ASSERTION existe no padrão SQL mas quase nunca é implementada, e o PostgreSQL não a suporta. Os benefícios principais são consistência, confiabilidade, centralização da lógica, documentação implícita e ganho de desempenho via índices automáticos. Triggers e procedures, citados aqui, serão aprofundados no módulo 14.