Todo sistema de RH caseiro tem uma coluna chamada escala que guarda um texto como “Seg a Sex, 8h às 17h”. Funciona até a primeira troca de folga, a primeira escala 12×36 ou o primeiro domingo trabalhado. Aí alguém precisa responder “quantos dias seguidos o Bruno trabalhou?”, e a resposta está dentro de uma string que nenhum SQL consegue ler.
O problema não é a escala, é o modelo. Texto livre não tem regra, não tem consulta e não tem auditoria. A saída é separar o que é cadastro, o que é planejado e o que aconteceu.
Resposta rápida: modele a escala com cinco tabelas:
funcionario(quem),turno(modelo de horário),escala_dia(uma linha por pessoa por dia, com trabalho, folga ou descanso),ponto(o que realmente aconteceu) ebanco_horas(lançamentos de saldo, como um livro-razão). As regras da CLT viram restrições (CHECK,UNIQUE) e consultas, nunca texto.
Planejado e realizado são fatos diferentes
O erro mais caro de modelagem é misturar o que a empresa programou com o que o funcionário cumpriu. A escala diz que a Ana entra às 8h; o ponto mostra que ela bateu às 8h12. Se as duas informações moram na mesma linha, qualquer correção apaga a outra.
Por isso o desenho usa duas tabelas de fatos por dia: escala_dia guarda a intenção, e ponto guarda a marcação. Comparar as duas é a base de qualquer relatório de atraso, falta ou hora extra. No Brasil, o ponto eletrônico segue regras próprias da Portaria MTP 671/2021, e o sistema de RH normalmente importa essas marcações em vez de substituí-las.
As cinco tabelas e o que cada uma responde
| Tabela | Pergunta que responde | Chave natural |
|---|---|---|
funcionario | Quem trabalha aqui, com qual jornada e escala base? | matrícula |
turno | Que horários existem (manhã, tarde, noite, 12 horas)? | nome do turno |
escala_dia | O que está previsto para cada pessoa em cada data? | funcionário + data |
ponto | O que de fato foi registrado? | funcionário + data |
banco_horas | Que créditos e débitos de horas existem, e quando vencem? | lançamento |
Perceba que não existe uma tabela chamada “6×1” ou “12×36”. Essas siglas são padrões de preenchimento de escala_dia, não entidades. Guardá-las como linhas, uma por dia, facilita auditar e permite trocar uma folga sem reescrever uma regra inteira.
O esquema em SQL
Os exemplos abaixo foram testados em SQLite. Para levá-los ao PostgreSQL, troque os campos de texto por DATE e TIME, substitua strftime por EXTRACT ou date_trunc e repita a expressão no HAVING em vez de usar o apelido.
CREATE TABLE funcionario (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
matricula TEXT NOT NULL UNIQUE,
horas_semanais REAL NOT NULL CHECK (horas_semanais BETWEEN 1 AND 44),
tipo_escala TEXT NOT NULL CHECK (tipo_escala IN ('6x1','5x2','12x36','outra'))
);
CREATE TABLE turno (
id INTEGER PRIMARY KEY,
nome TEXT NOT NULL,
inicio TEXT NOT NULL,
fim TEXT NOT NULL,
intervalo_min INTEGER NOT NULL DEFAULT 60
);
CREATE TABLE escala_dia (
id INTEGER PRIMARY KEY,
funcionario_id INTEGER NOT NULL REFERENCES funcionario(id),
data TEXT NOT NULL,
turno_id INTEGER REFERENCES turno(id),
tipo TEXT NOT NULL CHECK (tipo IN ('trabalho','folga','dsr','feriado')),
UNIQUE (funcionario_id, data),
CHECK ((tipo = 'trabalho' AND turno_id IS NOT NULL)
OR (tipo <> 'trabalho' AND turno_id IS NULL))
);O CHECK final impede dois defeitos clássicos: um dia de trabalho sem turno e uma folga com turno. Tentando inserir ('trabalho', turno_id NULL), o banco responde com CHECK constraint failed e o dado errado nem chega à tela.

A consulta que flagra dias demais sem folga
O art. 67 da CLT garante descanso semanal de 24 horas consecutivas, de preferência aos domingos. Em termos de dados, isso vira: nenhum bloco de dias de trabalho pode passar de seis seguidos. A técnica é numerar cada folga com uma soma acumulada, que separa os blocos.
WITH dias AS (
SELECT funcionario_id, data,
CASE WHEN tipo = 'trabalho' THEN 1 ELSE 0 END AS trab,
SUM(CASE WHEN tipo <> 'trabalho' THEN 1 ELSE 0 END)
OVER (PARTITION BY funcionario_id ORDER BY data) AS grupo
FROM escala_dia
)
SELECT funcionario_id, MIN(data) AS de, MAX(data) AS ate,
SUM(trab) AS dias_seguidos
FROM dias
WHERE trab = 1
GROUP BY funcionario_id, grupo
HAVING dias_seguidos > 6;No teste, a Ana trabalhou seis dias, folgou um domingo e depois trabalhou mais sete seguidos de 21 a 27 de dezembro de 2026. A consulta devolveu exatamente esse segundo bloco, com dias_seguidos = 7. A explicação sobre funções de janela está na documentação do SQLite sobre funções de janela.
Horas planejadas por semana, sem gambiarra de planilha
Para somar a jornada prevista de cada semana, junte escala_dia com turno e subtraia o intervalo:
SELECT e.funcionario_id,
strftime('%Y-%W', e.data) AS semana,
ROUND(SUM(
(strftime('%s','2000-01-01 ' || t.fim)
- strftime('%s','2000-01-01 ' || t.inicio)) / 3600.0
- t.intervalo_min / 60.0), 2) AS horas
FROM escala_dia e
JOIN turno t ON t.id = e.turno_id
GROUP BY 1, 2;Com o turno de 8h às 16h20 e uma hora de intervalo (7h20 por dia), a semana de seis dias retornou 44,00 horas, e a semana de sete dias, 51,33. O segundo número é o sinal vermelho: passou do teto de 44 horas e do limite de seis dias seguidos ao mesmo tempo.
Atenção a turnos noturnos: quando o fim é menor que o início (22h às 6h), a subtração dá negativo. A correção é somar 24 horas nesse caso, o que é fácil em SQL com um CASE e, em Python, com timedelta.
Saldo de horas como livro-razão, não como número
É tentador guardar o saldo do banco de horas numa coluna saldo em funcionario. Evite. Um número que só é atualizado perde a história e impede explicar a diferença no dia em que o funcionário pedir acerto. Em vez disso, a tabela banco_horas registra cada lançamento (minutos positivos ou negativos, origem e data de vencimento), e o saldo é uma soma.
O vencimento importa porque a CLT limita o banco de horas por acordo individual ao prazo de seis meses para compensação, e por acordo ou convenção coletiva, a um ano (art. 59 da CLT). Com a coluna vence_em, uma consulta lista o que vence nos próximos 30 dias, e o RH age antes de a hora virar passivo.
Armadilhas que só aparecem em produção
- Turno que cruza a meia-noite: a data do turno é a de início; o fim cai no dia seguinte.
- Feriados municipais: guarde em tabela própria por cidade. O mesmo funcionário pode ter feriado diferente do colega de outra filial.
- Troca de folga: altere a linha de
escala_diae registre quem trocou e quando, numa tabela de auditoria. Sem isso, a fiscalização não consegue reconstruir o que foi combinado. - Mudança de jornada: a redução prevista pela Emenda Constitucional 141/2026 não pode apagar o histórico. Guarde
horas_semanaiscom vigência (data inicial e final), em vez de sobrescrever. - Fuso horário: o Brasil acabou com o horário de verão em 2019, mas empresas com filiais em estados diferentes ainda precisam definir se a hora é local ou única.
Perguntas que aparecem quando o modelo vai para produção
Posso guardar a escala 12×36 como regra em vez de gerar as linhas?
Pode, mas é mais difícil de auditar. Gerar uma linha por dia (para os próximos 60 ou 90 dias) permite comparar cada dia com o ponto, aplicar exceções e responder “o que estava previsto para o dia 15?” sem recalcular.
Preciso usar SQLite ou PostgreSQL?
Para começar e testar, SQLite basta. Para ambiente com vários usuários e relatórios pesados, use PostgreSQL, que traz tipos DATE e TIME nativos, restrições de exclusão e generate_series para criar escalas.
Onde fica a regra do intervalo de 11 horas entre jornadas?
Em uma consulta que compara o fim de um turno com o início do turno seguinte da mesma pessoa. Se a diferença for menor que 11 horas, o sistema recusa ou alerta. O art. 66 é o fundamento.
Esse modelo serve para o ponto eletrônico oficial?
Não substitui. O registro oficial de ponto tem requisitos próprios (arquivos fiscais, comprovantes), definidos na Portaria 671/2021. O seu banco recebe as marcações já geradas por quem emite o registro.
Por onde começar, conforme o tamanho do sistema
Se você está criando um sistema novo, comece por funcionario, turno e escala_dia e crie as restrições logo, porque corrigir dado ruim depois custa mais do que prevenir. Se você herdou uma planilha, transforme a coluna de texto em linhas de escala_dia por um script, rode a consulta de dias seguidos e veja quantas violações já existem; esse número costuma ser a melhor justificativa para o projeto. E se você só precisa de um relatório, calcular o CET de um empréstimo em Python mostra o mesmo princípio de conferir uma conta exata por código antes de confiar em planilha.











