Transparência: sou fundador da CPFHub, citada no final. O conteúdo é puramente SQL e não depende de nenhum serviço.
Validar CPF só na aplicação deixa uma porta aberta: scripts de importação, outro microsserviço, um UPDATE manual feito às pressas. Se o banco é a fonte da verdade, faz sentido que ele recuse CPFs malformados. Vamos implementar isso no PostgreSQL.
1. Função de validação
CREATE OR REPLACE FUNCTION cpf_valido(p text)
RETURNS boolean
LANGUAGE plpgsql IMMUTABLE STRICT PARALLEL SAFE AS $$
DECLARE
d text := regexp_replace(p, '\D', '', 'g');
soma int;
dv int;
BEGIN
IF length(d) <> 11 OR d ~ '^(\d)\1{10}$' THEN
RETURN false;
END IF;
FOR t IN 9..10 LOOP
soma := 0;
FOR i IN 1..t LOOP
soma := soma + substr(d, i, 1)::int * (t + 2 - i);
END LOOP;
dv := (soma * 10) % 11 % 10;
IF dv <> substr(d, t + 1, 1)::int THEN
RETURN false;
END IF;
END LOOP;
RETURN true;
END $$;
Atenção ao índice: em SQL, substr começa em 1. Para o primeiro DV (t = 9), o peso do primeiro dígito é 10 (9 + 2 - 1); para o segundo (t = 10), é 11.
Marcar como IMMUTABLE permite usá-la em índices e constraints. STRICT faz cpf_valido(NULL) devolver NULL, o que em um CHECK significa "passa", então a obrigatoriedade fica com o NOT NULL.
SELECT cpf_valido('529.982.247-25'), -- t
cpf_valido('111.111.111-11'), -- f
cpf_valido('529.982.247-24'); -- f
2. Domínio
Um domínio deixa a regra reutilizável em várias tabelas:
CREATE DOMAIN cpf AS char(11)
CHECK (VALUE ~ '^\d{11}$' AND cpf_valido(VALUE));
CREATE TABLE clientes (
id bigserial PRIMARY KEY,
nome text NOT NULL,
cpf cpf NOT NULL UNIQUE,
criado_em timestamptz NOT NULL DEFAULT now()
);
O ^\d{11}$ dentro do domínio impede gravar com máscara. A aplicação normaliza; o banco garante.
3. Adicionando a uma tabela existente
Em uma tabela antiga, cheia de lixo, não dá para simplesmente criar a constraint. Use NOT VALID:
ALTER TABLE clientes_legado
ADD CONSTRAINT cpf_ok CHECK (cpf_valido(cpf)) NOT VALID;
Novas linhas e updates passam a ser checados imediatamente. As antigas ficam como estão até você limpá-las:
SELECT id, cpf FROM clientes_legado WHERE NOT cpf_valido(cpf);
-- corrija ou mova para quarentena, depois:
ALTER TABLE clientes_legado VALIDATE CONSTRAINT cpf_ok;
VALIDATE CONSTRAINT usa um lock mais leve que a criação direta, o que importa em tabelas grandes.
4. Normalizando o legado
UPDATE clientes_legado
SET cpf = lpad(regexp_replace(cpf, '\D', '', 'g'), 11, '0')
WHERE cpf ~ '\D' OR length(cpf) BETWEEN 9 AND 10;
O lpad recupera zeros à esquerda perdidos em exportações para Excel. Antes de rodar, conte quantas linhas serão afetadas e faça em transação.
5. Duplicados disfarçados
SELECT regexp_replace(cpf, '\D', '', 'g') AS d, count(*)
FROM clientes_legado
GROUP BY 1 HAVING count(*) > 1;
6. Performance
A função é chamada a cada INSERT/UPDATE da coluna. Em PL/pgSQL ela custa microssegundos; não vai ser o seu gargalo. Se você importa dezenas de milhões de linhas, desabilite a constraint na carga e valide no final.
7. SQLite e MySQL
No MySQL 8, CHECK constraints funcionam, mas não aceitam funções armazenadas. A alternativa é um trigger BEFORE INSERT. No SQLite, registre a função a partir da aplicação (por exemplo, create_function no Python) ou valide na camada de app.
8. E a existência do CPF?
O banco garante que o número é bem formado. Saber se ele existe e a quem pertence depende de uma fonte externa, e isso deve acontecer na aplicação, não em um trigger: uma chamada HTTP dentro de transação segura locks e transforma latência de rede em latência de banco. Um padrão melhor é gravar com status_verificacao = 'pendente' e ter um worker que consulta a API e atualiza a linha.
Sobre a CPFHub
Para esse worker, a API de consulta de CPF oferece consulta individual (GET /cpf/{cpf}) e em lote (POST /cpf/bulk, até 10.000 CPFs por envio, assíncrono), com 50 créditos grátis por mês.
Top comments (0)