Procedimento Operacional — Arquitetura e Modelagem de Dados
Código: HORUS-DAT-PRO-001 Versão: 2.4 — 2026-10-05 Escopo: Regras e processos que a equipe deve seguir ao propor, revisar, aprovar e implementar mudanças no modelo de dados dos Sistemas do DTIC (PostgreSQL operacional e BigQuery analítico). Público: Arquitetos de dados, Tech Leads, desenvolvedores e gestores responsáveis por sistemas.
Resumo Executivo
Este documento define como a equipe trabalha com dados nos Sistemas do DTIC. Ele cobre o processo de aprovação de mudanças de schema, os padrões de nomenclatura, as regras de versionamento, os critérios de classificação de dados e o fluxo de promoção entre ambientes (HOM → PROD).
O objetivo é garantir que o crescimento do modelo de dados seja previsível, rastreável e auditável — especialmente em períodos em que múltiplas mudanças acontecem em paralelo (como migrações e modernizações de sistemas).
1. Processo de Mudança de Schema
Toda alteração no modelo de dados — criação de tabela, adição de coluna, criação de índice, alteração de constraint — segue o fluxo abaixo.
1.1 Fluxo de aprovação
[Solicitante] → Proposta de mudança (Issue/PR)
↓
[Arquiteto de Dados] → Revisão técnica (nomenclatura, impacto, LGPD)
↓
[Gestor do Domínio] → Validação de negócio
↓
[Tech Lead] → Revisão de performance e operação
↓
Aprovação → Merge para HOM → Validação → Merge para PROD
1.2 O que exige aprovação formal (todos os 3 revisores)
- Criação de nova tabela
- Módulo novo ou módulo que passa a usar entidade mestre (anexar a ficha arquitetural do Doc 06, Seção 8)
- Criação de schema novo
- Alteração de chave primária ou chave estrangeira
- Remoção de coluna ou tabela
- Adição de coluna com restrição NOT NULL em tabela com dados
- Alteração de tipo de dado, tamanho ou precisão de coluna existente
Criação de schema novo e mudança em entidade mestre (
mdm) exigem, além dos três revisores, homologação do Comitê de Dados (Doc 04, Seção 8.2).
1.3 O que exige apenas aprovação do Arquiteto de Dados
- Adição de coluna nullable em tabela existente
- Criação de índice
- Alteração de comentário ou descrição de coluna
- Criação de view fora do domínio do próprio proponente
1.4 O que o desenvolvedor pode fazer sem aprovação prévia
- Criação de tabela temporária ou de staging em schema de desenvolvimento
- Ajuste de dados em HOM sem impacto estrutural
- Criação de view em schema próprio do domínio.
2. Padrões de Nomenclatura
2.1 PostgreSQL
| Elemento | Padrão | Exemplo |
|---|---|---|
| Schema (quando o sistema organiza o banco em schemas) | snake_case, singular | {dominio} |
| Tabela | snake_case, plural | pessoas, municipios, processos |
| Coluna | snake_case, com prefixo de tipo de dado quando a coluna se enquadra em um termo do catálogo (Seção 2.1.1) | dt_nascimento, cd_ibge, nr_cpf |
| Chave primária | id_{singular_da_tabela} (UUID) — tabela pessoas → id_pessoa | id_pessoa, id_processo |
| Chave estrangeira (coluna) | id_{entidade}; com papel, id_{entidade}_{papel} | id_municipio, id_usuario_cadastro |
| Constraint FK | fk_{tabela}_{coluna} | fk_processos_id_municipio |
| Constraint CHECK | ck_{entidade}_{descricao} (entidade no singular) | ck_participante_origem |
| Índice | idx_{entidade}_{coluna(s)} (entidade no singular) | idx_pessoa_nr_cpf, idx_processo_dt_abertura |
| Índice único | uq_{entidade}_{coluna(s)} (entidade no singular) | uq_pessoa_nr_cpf |
| Sequence | seq_{tabela}_{coluna} | (evitar — usar UUID) |
| View | vw_{descricao} | vw_processos_ativos |
| Função | fn_{verbo}_{descricao} | fn_buscar_municipio_por_ibge |
| Trigger | tg_{momento}_{tabela}_{acao} | tg_before_pessoas_update |
| Enum | {nome}_enum, com prefixo do schema quando houver | tipo_pessoa_enum |
Nomes de objeto (tabela, constraint, índice) são limitados a 63 bytes no PostgreSQL. Abreviar de forma determinística e documentada — nunca depender de truncamento silencioso. Por brevidade, alguns exemplos deste documento usam o nome da entidade no singular; em objetos reais, o nome da tabela é sempre plural, a menos que não exista a versão em plural da palavra.
2.1.1 Prefixo de coluna
Quando a coluna se enquadra em um dos termos da tabela abaixo, o nome começa por um prefixo de 2 letras seguido de _, que informa o que a coluna é. Coluna que não se enquadra em nenhum termo não leva prefixo. O prefixo indica o tipo do dado (ou o papel de chave), não a tabela de origem — por isso não se confunde com o prefixo de entidade do legado, que é proibido (ver abaixo). Ele corresponde ao termo de representação da ISO/IEC 11179-5 (data, número, nome, indicador etc.) e não ao tipo físico da coluna: fl_ costuma ser BOOLEAN, mas nr_cpf é texto.
| Prefixo | Significa | Quando usar | Exemplo |
|---|---|---|---|
id_ | Identificador | PK, FK e identificador de origem. Formato id_{entidade}; com papel: id_{entidade}_{papel} | id_pessoa, id_municipio, id_usuario_cadastro, id_origem_externa |
dt_ | Data | Data sem hora | dt_nascimento, dt_vencimento |
dh_ | Data e hora | TIMESTAMPTZ | dh_assinatura, dh_cadastro |
nr_ | Número | Número de negócio emitido (processo, contrato) e numeral de documento oficial (CPF, CNPJ, RG); nunca PK/FK | nr_processo, nr_cpf, nr_cnpj |
vl_ | Valor | Valor monetário ou numérico | vl_contrato, vl_renda_mensal |
qt_ | Quantidade | Contagem | qt_membros_familia |
nm_ | Nome | Nome próprio ou denominação | nm_completo, nm_razao_social, nm_municipio |
ds_ | Descrição | Texto livre | ds_observacao |
fl_ | Flag | Indicador booleano | fl_excluido, fl_urgente |
tp_ | Tipo | Classificação por valor controlado, quando não modelada como tabela de domínio | tp_documento, tp_origem_cadastro |
sg_ | Sigla | Sigla | sg_uf |
cd_ | Código | Código de referência externa ou de domínio | cd_ibge, cd_situacao, cd_cnae_principal |
js_ | JSON | Documento JSON/JSONB | js_antes, js_depois |
gm_ | Geometria | Tipo geoespacial (PostGIS) | gm_limite |
Regras
- Se a coluna se enquadra em um termo da tabela, o prefixo correspondente é obrigatório, inclusive nas colunas de auditoria e de soft delete (Seção 3), que sempre se enquadram.
- Coluna que não se enquadra em nenhum termo fica sem prefixo e segue
snake_casedescritivo, em português (Seção 2.1). O catálogo é fechado: um prefixo novo entra neste documento, com aprovação do Arquiteto de Dados, antes de ser usado. - O prefixo é sempre de 2 letras;
id_segue a mesma regra e é o único prefixo de chave. - Os prefixos de objeto da Seção 2.1 (
idx_,uq_,fk_,fn_,tg_,seq_) identificam o tipo de objeto e não são usados em colunas.
Como escolher: é chave ou identificador de origem? id_. Senão: data? dt_ (com hora, dh_). Número emitido ou documento oficial? nr_. Valor? vl_. Quantidade? qt_. Nome? nm_. Texto livre? ds_. Verdadeiro/falso? fl_. Valor de lista controlada? tp_ (tipo) ou cd_ (código de domínio ou externo). Sigla? sg_. JSON? js_. Geometria? gm_. Se nenhuma resposta servir, a coluna fica sem prefixo.
Proibido:
- Abreviações não óbvias (usar
municipio, nãomun) - Prefixos de entidade/tabela de origem herdados do legado (ex.:
CidNome,PesCpf) — não confundir com os prefixos de tipo de dado da Seção 2.1.1, que são obrigatórios - Nomes em inglês para entidades de negócio (usar português)
- Mistura de idiomas no mesmo identificador ("Portinglês" — ex.:
dt_birth_date,status_processo,fl_completed,order_numero): se o termo de negócio é em português, todo o identificador segue em português: prefixo de tipo (Seção 2.1.1), radical e eventuais sufixos - Acentuação e caracteres especiais (
á,ç,ã, etc.) em nomes de schema, tabela, coluna, constraint ou índice — usar apenas[a-z0-9_](ex.:situacao, nuncasituação); a acentuação correta do termo de negócio vive no glossário do Knowledge Catalog (Seção 7.4) e em comentários (COMMENT ON), não no identificador - Nomes genéricos:
dados,info,tabela,registro - Colunas com o mesmo nome da tabela:
pessoa.pessoa_id→ usarpessoa.id_pessoa
2.2 BigQuery
| Elemento | Padrão | Exemplo |
|---|---|---|
| Dataset | {dominio}_{camada} | pedidos_bronze, clientes_gold |
| Tabela fato | fato_{processo}, no plural | fato_pedidos, fato_atendimentos |
| Tabela dimensão | dim_{entidade}, no plural | dim_municipios, dim_clientes |
| Tabela raw/staging | {sistema}_{tabela}_raw | crm_clientes_raw |
| Coluna | snake_case, com prefixo de tipo (Seção 2.1.1) quando aplicável; colunas de auditoria usam os mesmos nomes do PG (Seção 7.1) | vl_contrato, cd_ibge, dh_cadastro |
| Particionamento | Preferir DATE(dh_cadastro) ou DATE(dh_evento), conforme a coluna de data/hora disponível na tabela | — |
| Clustering | Coluna mais filtrada após a partição | id_municipio, id_programa |
3. Colunas Obrigatórias em Toda Tabela
Toda tabela de domínio e de dados mestres criada no PostgreSQL deve conter as colunas abaixo. Elas garantem rastreabilidade, auditoria e soft delete. Ficam fora desta regra as tabelas de log e de eventos, que são imutáveis (sem UPDATE nem DELETE para as roles de aplicação, ex.: auditoria.log_operacao) e dispensam as colunas de auditoria e de soft delete, e as tabelas de staging, que podem usar DELETE físico (Doc 04, Seção 3.1).
3.1 Identidade
id_{tabela} UUID NOT NULL DEFAULT uuidv7() PRIMARY KEY
UUIDs são obrigatórios como PKs. Nunca usar
SERIALouBIGSERIALcomo chave primária em tabelas de domínio. Sequências numéricas podem ser usadas como chaves de negócio secundárias quando necessário (ex.: número de processo).
A PK usa UUIDv7 (
uuidv7(), nativo a partir do PostgreSQL 18): ordenável por tempo, reduz a fragmentação de índices.gen_random_uuid()(UUIDv4) não é usado comoDEFAULTde PK.
Escopo: esta regra vale para as tabelas de domínio criadas pelo time; objetos criados por extensões do SGBD e pela ferramenta de migration estão fora do escopo. Em tabelas particionadas, a PK é composta por
id_{tabela}e a coluna de partição (Seção 6.5).
3.2 Auditoria
dh_cadastro TIMESTAMPTZ NOT NULL DEFAULT NOW()
id_usuario_cadastro UUID NOT NULL -- id_usuario dos Sistemas do DTIC
dh_atualizacao TIMESTAMPTZ NOT NULL DEFAULT NOW()
id_usuario_atualizacao UUID NOT NULL -- id_usuario dos Sistemas do DTIC
O campo
dh_atualizacaodeve ser mantido automaticamente via trigger (ver Seção 6).
3.3 Soft Delete
fl_excluido BOOLEAN NOT NULL DEFAULT FALSE
dh_exclusao TIMESTAMPTZ
id_usuario_exclusao UUID
A flag e a data não podem divergir: toda tabela com soft delete leva
CONSTRAINT ck_{entidade}_exclusao CHECK (fl_excluido = (dh_exclusao IS NOT NULL)).
O filtro
fl_excluido = FALSEnão é repetido à mão em cada consulta. A aplicação lê tabelas com soft delete por uma view que o aplica (vw_{descricao}, listando as colunas) ou pelo escopo padrão da ORM (ex.:paranoiddo Sequelize, mapeado paradh_exclusao):CREATE VIEW vw_processos_ativos ASSELECT id_processo, nr_processo, dh_cadastro -- demais colunas de negócioFROM processosWHERE fl_excluido = FALSE;
Nunca usar
DELETEfísico em tabelas de domínio (a role da aplicação não recebe o privilégioDELETEnelas). O registro é marcado comfl_excluido = TRUE.
LGPD: soft delete não equivale a eliminação. Dado pessoal deve ter prazo de guarda definido e, ao fim da finalidade ou a pedido do titular (LGPD, art. 18), passar por procedimento auditado de eliminação ou anonimização das colunas de PII. Em tabelas com soft delete, a unicidade de código de negócio usa índice único parcial (
WHERE fl_excluido = FALSE) — ver Seção 6.4.
Histórico de exclusões de pessoas (
mdm.pessoas_exclusoes): tabela satélite (REG-CM-05, Doc 06) que registra cada exclusão e reativação de uma pessoa, para consulta administrativa e auditoria:CREATE TABLE mdm.pessoas_exclusoes (id_pessoa UUID NOT NULL REFERENCES mdm.pessoas (id_pessoa) ON DELETE RESTRICT,dh_evento TIMESTAMPTZ NOT NULL,tp_evento VARCHAR(20) NOT NULL CHECK (tp_evento IN ('EXCLUSAO','REATIVACAO')),id_usuario_evento UUID NOT NULL,PRIMARY KEY (id_pessoa, dh_evento) -- composta: permite múltiplos ciclos de exclusão/reativação da mesma pessoa);
id_pessoacontinua sendo a chave primária demdm.pessoas; o soft delete (fl_excluido = TRUE) continua sendo o mecanismo operacional e nenhuma linha é removida fisicamente, preservando as FKs deprocessos,atendimentos,documentosetc.- A tabela é populada por trigger
AFTER UPDATEquandofl_excluidomuda de valor e é imutável: semUPDATE/DELETEpara as roles de aplicação, o que dispensa as colunas de auditoria e de soft delete.- Não guarda CPF nem outro dado pessoal: os dados são obtidos por join com
mdm.pessoas, que nunca é apagada fisicamente. Assim não há cópia de dado mestre (Seção 11) nem de PII.- A unicidade de CPF continua garantida pelo índice único parcial de
mdm.pessoas(Seção 6.4,WHERE fl_excluido = FALSE).- Escopo exclusivo de
pessoas. Outras entidades com chave de negócio única (CNPJ, matrícula de imóvel etc.) usam apenas o índice único parcial (Seções 3.3 e 6.4).
3.4 Classificação LGPD (obrigatória em colunas sensíveis)
Toda coluna que contenha dado pessoal deve ter um comentário de classificação aplicado via COMMENT ON COLUMN:
COMMENT ON COLUMN pessoas.nr_cpf IS 'LGPD:RESTRITO — CPF do titular. Mascarar em HOM. Não replicar para BQ sem pseudonimização.';
COMMENT ON COLUMN usuarios.ds_senha_hash IS 'LGPD:SIGILOSO — Hash de senha. Nunca replicar para BQ ou HOM.';
| Classificação | Exemplos | Regra |
|---|---|---|
| PÚBLICO | Nome do empreendimento, endereço de núcleo | Pode replicar livremente |
| INTERNO | Dados operacionais sem PII | Replicar apenas entre ambientes internos |
| RESTRITO | CPF, RG, CNPJ (a coluna também guarda CNPJ de empresário individual, que identifica pessoa natural), renda, endereço residencial, data de nascimento, dados socioeconômicos | Mascarar em HOM; pseudonimizar no BQ |
| SIGILOSO | Senha (mesmo hash), JWT, dados de saúde | Nunca replicar; nunca expor em BQ |
4. Versionamento de Schema — Migrations
4.1 Mecanismo
Não há ferramenta de migration definida. Toda mudança de schema é um script .sql versionado na pasta db/migration/ do repositório institucional dos Sistemas do DTIC e aplicado em ordem de versão, conforme as regras das Seções 4.2 a 4.4.
4.2 Nomenclatura de arquivos
V{versao}__{descricao}.sql ← migration versionada (executa uma vez)
R__{objeto}.sql ← migration repetível (views, SPs, funções)
Exemplos:
V001__create_schema_dominio.sql
V002__create_table_pessoas.sql
V003__add_column_pessoas_tipo.sql
R__vw_pessoas_ativas.sql
Documentos de documentação seguem o padrão {NN}_{assunto}.{ext}:
NNcom 2 dígitos, na ordem da sequência de leitura (01_,02_, ...).snake_caseem minúsculas, sem acento, sem espaço e sem redundância comodocumento_de(ex.:03_modelo_corporativo_dados.md).- A versão fica no cabeçalho do documento, nunca no nome do arquivo (nada de
(1)ou_v2). - O PDF de um documento tem o mesmo nome do Markdown, só muda a extensão.
- Todo documento tem um código
HORUS-{ÁREA}-{SIGLA}-{NNN}no cabeçalho, que não muda entre versões:HORUS-ARQ-DF-001(01 Data Foundation),HORUS-REQ-FUN-001(02 Requisitos Funcionais),HORUS-DAT-MCD-001(03 Modelo Corporativo de Dados),HORUS-DAT-GOV-001(04 Governança PostgreSQL),HORUS-DAT-PRO-001(05 Procedimento de Modelagem),HORUS-ARQ-CM-001(06 Cadastro Mestre). Documento novo recebe o próximoNNNdo seuÁREA-SIGLAe o código entra nesta lista.
4.3 Regras de escrita de migrations
- Um migration = uma mudança lógica. Não agrupar
CREATE TABLE+ALTER TABLE+INSERTem um único arquivo. - Migrations são imutáveis após merge para PROD. Nunca editar um arquivo
V*já executado. Para corrigir, criar um novoV{n+1}. - Migrations devem ser reversíveis quando possível. Documentar no comentário do arquivo o rollback equivalente.
- Migrations com dados iniciais usam prefixo
V{n}__seed_{tabela}.sqle são separados dos DDL. - Nunca usar
DROP TABLEouDROP COLUMNsem aprovação formal (Seção 1.2). - Sem DDL manual em HOM/PROD fora da ferramenta de migration. DDL emergencial exige migration retroativa registrada.
- Mudança sem parada: renomear ou remover coluna em uso segue o padrão expandir–migrar–contrair (adicionar a nova estrutura → migrar dados e uso → remover a antiga em migration posterior).
- Bloqueios: em tabelas grandes, definir
lock_timeoutestatement_timeoutno início da migration; criar índices comCREATE INDEX CONCURRENTLY(em migration própria, fora de transação) e constraints em tabelas com dados comNOT VALIDseguido deVALIDATE CONSTRAINT.
4.4 Rollback
PostgreSQL suporta DDL transacional — CREATE TABLE, ALTER TABLE, DROP COLUMN etc. podem rodar dentro de BEGIN ... ROLLBACK e serem desfeitos como qualquer outra transação, diferente da maioria dos SGBDs relacionais. A exceção são comandos que não podem rodar dentro de transação, como CREATE INDEX CONCURRENTLY e VACUUM (ver Seção 4.3) — para esses, não há rollback automático possível. Ainda assim, para cada migration de risco (remoção de coluna, alteração de tipo, ou qualquer comando fora de transação), deve existir um script de rollback documentado no PR, já que a ferramenta de migration não reverte automaticamente uma migration já aplicada em PROD (Seção 4.3).
5. Fluxo HOM → PROD
5.1 Ambientes
| Ambiente | Projeto GCP | Banco PG | Propósito |
|---|---|---|---|
| HOM | {projeto}-hom | PostgreSQL HOM | Desenvolvimento, testes de integração, homologação |
| PROD | {projeto}-prod | PostgreSQL PROD | Produção — dados reais |
5.2 Checklist de promoção HOM → PROD
Antes de promover qualquer mudança de schema para PROD, verificar:
- Migration executada com sucesso em HOM sem erros
- Testes de regressão passando em HOM (smoke test mínimo)
- Colunas com PII têm comentário LGPD aplicado
- Índices de performance criados para colunas de filtro frequente
- View ou query de validação executada confirmando integridade dos dados
- PR aprovado por Arquiteto de Dados + Gestor do Domínio + Tech Lead
- Script de rollback documentado no PR para mudanças destrutivas
- Pipelines BQ ajustados se a mudança impacta colunas lidas por queries Bronze/Silver
5.3 Dados sensíveis em HOM
- HOM não deve conter CPF, RG, renda ou dados sigilosos reais.
- Dados de PII em HOM devem ser sintéticos ou mascarados (substituição por dados fictícios plausíveis).
- A replicação de dados de PROD para HOM deve passar por pipeline de mascaramento antes da carga.
6. Padrões de Implementação PostgreSQL
6.1 Trigger de atualização automática de dh_atualizacao
CREATE OR REPLACE FUNCTION fn_atualizar_dh_atualizacao()
RETURNS TRIGGER AS $$
BEGIN
NEW.dh_atualizacao = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
-- Aplicar em cada tabela de domínio:
CREATE TRIGGER tg_before_pessoas_update
BEFORE UPDATE ON pessoas
FOR EACH ROW EXECUTE FUNCTION fn_atualizar_dh_atualizacao();
6.2 UUID padrão
-- Usar em colunas de PK (UUIDv7, nativo a partir do PostgreSQL 18):
id_pessoa UUID NOT NULL DEFAULT uuidv7()
uuidv7()é nativo a partir do PostgreSQL 18 e não depende de extensão. Valores gerados fora do banco (aplicação, ETL) também devem ser UUIDv7, para manter a ordenação temporal dos índices.
6.3 Enum tipado vs. tabela de domínio
| Caso | Usar |
|---|---|
| Valores fixos, raramente mudam, < 10 opções | ENUM do PostgreSQL |
| Valores que crescem com o tempo, gerenciados pela área de negócio | Tabela de domínio (tipos_{entidades}, no plural) |
Convenção de valores: tanto os rótulos de
ENUMquanto ocd_tipode tabela de domínio usamUPPER_SNAKE_CASEem português, sem acento (ex.:RASCUNHO,EM_ANALISE,HOMOLOGADO,CANCELADO). Não misturar convenções de estado entre domínios (ex.:PENDINGcomFINALIZADO).
Exemplo de tabela de domínio:
CREATE TABLE tipos_pessoas (
id_tipo_pessoa UUID PRIMARY KEY DEFAULT uuidv7(),
cd_tipo VARCHAR(30) NOT NULL UNIQUE, -- ex.: 'BENEFICIARIO', 'SERVIDOR'
ds_tipo TEXT NOT NULL,
fl_ativo BOOLEAN NOT NULL DEFAULT TRUE
);
6.4 Constraints de integridade
- Toda FK deve ter
ON DELETE RESTRICTpor padrão. UsarCASCADEapenas com aprovação explícita e documentação do motivo. - Campos obrigatórios de negócio devem ser
NOT NULLno banco — não confiar somente na validação da aplicação. - Campos de código de negócio (CPF, CNPJ, código IBGE) devem ter constraint
UNIQUE. Em tabelas com soft delete, usar índice único parcial (CREATE UNIQUE INDEX uq_… ON … (coluna) WHERE fl_excluido = FALSE). Duplicidades pré-existentes devem ser saneadas, com decisão registrada, antes de criar a restrição. - Toda coluna que referencia outra entidade (
id_{entidade}) deve ter constraint de FK. Exceção:id_usuario_cadastro,id_usuario_atualizacao,id_usuario_exclusao(referem o usuário da aplicação, autenticado por IdP externo) e os identificadores de origemid_origem_externaeid_legado. Referência a entidade mestre hospedada em outro banco é FK lógica, validada pela API de Cadastro Mestre do sistema dono, e mantém o índice (Doc 06, REG-CM-18). Coluna legada mantida apenas como histórico usa o sufixo_legadoe tem prazo de remoção definido. - Toda coluna de FK deve ter índice — o PostgreSQL não o cria automaticamente na coluna referenciadora.
- Todo instante de tempo usa
TIMESTAMPTZ;timestampsem fuso não é permitido. Regras de formato e faixa de valores devem ser expressas comCHECK.
6.5 Volumetria, particionamento e retenção
- Tabelas de eventos, logs e históricos, com crescimento contínuo no tempo, devem ter política de retenção definida na criação.
- Quando o crescimento previsto justificar — ordem de dezenas de GB ou de dezenas de milhões de linhas, índices que deixam de caber em memória, ou expurgo periódico por data — usar particionamento declarativo (
PARTITION BY RANGEsobre coluna de data), com partições criadas com antecedência e expurgo porDETACH/DROPde partição. Abaixo desse porte, índices adequados e manutenção regular são preferíveis: particionar tem custo operacional. - Em tabela particionada, a PK deve incluir a coluna de partição (limitação do PostgreSQL) e as FKs para ela são lógicas. Nesse caso, a PK é composta por
id_{tabela}e pela coluna de partição. - O crescimento por tabela é acompanhado periodicamente; a decisão de particionar é do Arquiteto de Dados com o Tech Lead.
7. Modelagem BigQuery — Padrões Adicionais
7.1 Camadas e responsabilidades
| Camada | Responsabilidade | Quem escreve |
|---|---|---|
| Bronze | Dado bruto da fonte, sem transformação. Schema espelha o PG operacional. | Pipeline CDC / scheduled query |
| Silver | Limpeza, tipagem, deduplicação, joins resolvidos. Sem lógica de negócio. | Job de transformação (Dataform ou SQL schedulado) |
| Gold | Fatos e dimensões prontos para analytics. Lógica de negócio aplicada. | Job de transformação com aprovação do Arquiteto |
7.2 Proibições no BQ
- Nunca escrever dados de PROD diretamente no BQ de HOM sem mascaramento de PII.
- Nunca criar tabelas com nomes genéricos (
dados_municipio_v2,teste_final). Toda tabela em produção deve seguir a nomenclaturafato_*oudim_*ou{sistema}_{tabela}_raw. - Nunca usar
SELECT *em views ou queries Gold — listar colunas explicitamente.
7.3 Documentação de tabelas BQ
Toda tabela Gold deve ter description definida no schema:
-- Exemplo via DDL BQ:
CREATE TABLE `{projeto}.{dominio}_gold.dim_municipios`
OPTIONS (description = 'Dimensão município canônica. Fonte: IBGE. Chave: cd_ibge. Dono: time de dados.')
AS ...
7.4 Classificação e Descoberta via Knowledge Catalog
O Google Cloud Knowledge Catalog é a ferramenta institucional padrão de descoberta, classificação e metadados — usada tanto no BigQuery quanto no PostgreSQL/Cloud SQL (ver Doc 04, Seção 8.3).
- Tag Templates de PII: toda coluna que armazene dados pessoais (classificação RESTRITO ou SIGILOSO — Seção 3.4) deve ser mapeada e tagueada via Knowledge Catalog.
- Policy Tags: o acesso a colunas PII no BigQuery é restringido via Data Policies nativas do BigQuery, amarradas à taxonomia do Knowledge Catalog, garantindo mascaramento para perfis de menor privilégio (IAM).
- Indicadores de qualidade e confiança: o Knowledge Catalog também classifica a confiabilidade do dado, sinalizando campos que requerem revisão ou estão com baixa confiança.
- Glossário de termos corporativos: o dicionário de termos permitidos e sinônimos (ex.: definir se o sistema usa
municipiooucidade,bairrooudistrito) é mantido no Knowledge Catalog, que é a fonte de verdade; o Doc 03, Seção 7, apenas o espelha. Consultar o Knowledge Catalog antes de nomear uma entidade nova é parte do rito da Seção 11.
8. Responsabilidades
| Papel | Responsabilidade |
|---|---|
| Arquiteto de Dados | Aprovação de mudanças estruturais; guardião do modelo de dados institucional; revisão de nomenclatura e LGPD; execução das migrations em PROD; operação do banco e dos pipelines de ETL, incluindo monitoramento de conexões e storage (Doc 04) |
| Tech Lead | Aprovação de performance e revisão de código das migrations |
| Gestor do Domínio | Validação de regras de negócio; aprovação de mudanças no domínio sob sua responsabilidade |
| Desenvolvedor | Proposta e implementação de mudanças; escrita dos migrations; documentação no PR |
| Time de Dados | Manutenção dos pipelines BQ; promoção HOM → PROD no BQ; gestão das tabelas de dados mestres externos (IBGE, SEADE) |
9. Checklist de Revisão de Modelagem
Use este checklist ao revisar qualquer proposta de nova tabela ou mudança significativa. Sempre que possível, automatize estas verificações sobre o catálogo do banco (em CI e periodicamente): revisão apenas manual não escala.
Nomenclatura
- Nome da tabela no plural, snake_case, em português
- Colunas que se enquadram em um termo do catálogo usam o prefixo correspondente; as que não se enquadram ficam sem prefixo (
dt_,dh_,nr_,vl_,qt_,ds_,fl_,tp_,sg_— Seção 2.1.1) - Colunas sem prefixo de entidade/tabela herdado do legado (padrão de sistema anterior não migrado)
- Sem mistura de idioma no mesmo identificador (Portinglês) e sem acentuação/caracteres especiais em schema, tabela, coluna, constraint ou índice (Seção 2.1)
- FK nomeada como
id_{entidade_referenciada} - Índices nomeados com padrão
idx_ouuq_ - Valores de
ENUM/cd_tipode tabela de domínio emUPPER_SNAKE_CASE, português, sem acento (Seção 6.3)
Estrutura
- PK é UUID com
DEFAULT uuidv7() - Colunas de auditoria presentes:
dh_cadastro,id_usuario_cadastro,dh_atualizacao,id_usuario_atualizacao - Soft delete presente (tabela de domínio ou mestre):
fl_excluido,dh_exclusao,id_usuario_exclusao, comCHECKde coerência entrefl_excluidoedh_exclusao - Trigger de
dh_atualizacaocriado
Tabela mestre (schema mdm) — regras completas no Doc 06
- Tabela criada no schema
mdm(nunca empublicou em schema de domínio) e sem duplicar entidade mestre existente (REG-CM-04) - Linhagem presente:
tp_origem_cadastro,dh_sincronizacao_fonte,id_origem_externa(REG-CM-14) - Situação cadastral em
cd_situacaocomCHECKde domínio; semstatus,fl_ativoouis_active(REG-CM-07 e 10) - Papéis contextuais em tabela associativa do módulo, não como coluna da tabela mestre (REG-CM-11)
- Trigger
auditoria.fn_registrar_operacaoaplicada com a PK (REG-CM-16) - Ficha arquitetural de módulo anexada à proposta (Doc 06, Seção 8)
LGPD
- Colunas com PII identificadas e classificadas com
COMMENT ON COLUMN - Colunas com PII classificadas com o prefixo
LGPD:RESTRITOouLGPD:SIGILOSOno comentário, pois é ele que define o mascaramento no log de auditoria (Doc 04, Seção 7.2) - Campos sigilosos (senha, JWT) ausentes da tabela (não devem existir no BQ e nem no modelo analítico)
Integridade
- FKs com
ON DELETE RESTRICT(ou justificativa para CASCADE) - Campos obrigatórios de negócio marcados
NOT NULL - Campos de código único com constraint
UNIQUE(índice único parcialWHERE fl_excluido = FALSEem tabelas com soft delete — Seção 6.4) - FK entre tabelas aponta sempre para a PK UUID (
id_{entidade}), nunca para chave de negócio (nr_cpf,nr_cnpj,cd_ibge,nr_processo) - Tabela migrada de legado: coluna
id_legadocom acesso restrito e plano de expurgo (Doc 04, Seção 6.4) - Toda coluna
id_{entidade}que referencia outra tabela tem FK e índice (excetoid_usuario_*,id_origem_externa,id_legadoe FK lógica entre bancos, Doc 06, REG-CM-18) - Instantes de tempo em
TIMESTAMPTZ - Tabela de crescimento contínuo tem política de retenção e avaliação de particionamento (Seção 6.5)
Documentação
- Migration em arquivo separado com nome descritivo
- Script de rollback documentado (para mudanças destrutivas)
- Entidade adicionada ao modelo corporativo de dados institucional, quando aplicável ao sistema, se for nova
A automação do checklist tem exemplo no Apêndice A.
10. Diretrizes de ETL/ELT
Esta seção orienta o time na escolha e operação dos motores de ETL/ELT usados para ingestão e transformação de dados públicos, dentro do mesmo padrão institucional dos Sistemas do DTIC (100% GCP, sem infraestrutura própria a manter).
10.1 Critério de escolha da ferramenta
Regra geral: nenhuma ferramenta de ETL/ELT, orquestração ou streaming fora do GCP é permitida. Toda a esteira de dados é construída exclusivamente com serviços gerenciados GCP, listados abaixo — não há avaliação caso a caso de ferramenta de terceiros ou auto-hospedada fora desta lista.
| Necessidade | Ferramenta GCP a usar | Observação |
|---|---|---|
| Transformação em SQL (Silver/Gold) | BigQuery Scheduled Queries ou Dataform (GCP) | Sem orquestrador dedicado quando a lógica for majoritariamente SQL e não depender de outras etapas — mesmo padrão da Seção 7.1 |
| Ingestão/transformação em código (Python) | Cloud Run Jobs (contêiner) | Job agendado ou disparado por evento; sem servidor de longa duração dedicado ao ETL |
| Rotinas reativas curtas (reconciliações, verificações entre cargas) | Cloud Functions | Disparada por evento ou agendamento, sem servidor dedicado; falha gera alerta (Seção 10.3) |
| Orquestração de pipeline com múltiplas etapas dependentes | Cloud Composer (Apache Airflow gerenciado) | Único orquestrador permitido; ver regra de uso ponta a ponta abaixo |
| Processamento em streaming (quando necessário) | Dataflow (Apache Beam), alimentado por Pub/Sub | Usar apenas quando o requisito for latência baixa/contínua; caso contrário, preferir processamento em lote (Seção 10.3) |
| Mensageria/eventos | Pub/Sub | Único serviço de mensageria permitido |
- Uso do Airflow (Cloud Composer) é permitido para orquestrar as transformações Silver/Gold, desde que o DAG cubra o processo de ponta a ponta — da origem do dado (extração) até o destino final (tabela Gold consumível) — dentro do mesmo pipeline. Não é permitido usar o Airflow para orquestrar apenas uma etapa intermediária e deixar as demais soltas em agendamentos paralelos e desconexos: se há dependência entre extração, Bronze, Silver e Gold, o DAG único do Airflow é responsável pela cadeia inteira.
- Quando a transformação Silver/Gold não tiver dependência de etapas anteriores dentro do mesmo pipeline (ex.: uma consulta agendada isolada, sem orquestração de extração), não é necessário Airflow — usar BigQuery Scheduled Query ou Dataform diretamente, conforme a tabela acima.
- Antes de adicionar um novo DAG/job, avaliar se a necessidade já é coberta por uma peça já existente na lista acima — reduzir o número de ferramentas distintas reduz custo de manutenção e superfície de atualização.
10.2 Atualização e versionamento dos pipelines
- Scripts e definições de pipeline (SQL, jobs, arquivos de configuração de agendamento) são versionados no mesmo repositório do código, revisados pelo mesmo rito de PR aplicado às migrations (Seção 4).
- Atualização de versão de imagem/dependência dos jobs de ETL segue o mesmo processo de verificação de vulnerabilidade (SAST/Trivy) exigido para qualquer outro serviço em contêiner.
- Mudanças que alterem o schema de destino (novas colunas, novas tabelas) seguem o mesmo fluxo de aprovação de mudança de schema (Seção 1) — o pipeline de ETL não cria estrutura de tabela por conta própria fora desse rito.
10.3 Janela e frequência de processamento
- Cargas incrementais são preferíveis a reprocessamento completo: usar marca d'água (watermark) de data/hora (
dh_atualizacaode origem) ou CDC para carregar apenas o que mudou, reduzindo tempo de execução e custo de processamento. - Toda falha de execução do pipeline deve gerar alerta nos padrões de observabilidade institucionais (Cloud Logging/Monitoring) — silenciar falha de ETL não é aceitável.
10.4 Recomendação de Melhores Práticas de DataOps
- Consultas de transformação (BigQuery) devem filtrar por partição e evitar
SELECT *, controlando custo de processamento por consulta. - Preferir agendamento nativo (consulta agendada, job de contêiner agendado) a manter um serviço em execução contínua apenas esperando o próximo disparo.
- Não é permitida a adoção de ferramenta de ETL/ELT, orquestração, streaming ou mensageria fora da lista da Seção 10.1 (fora do GCP). Qualquer necessidade não coberta pela lista é levada ao Comitê de Dados para avaliação de qual serviço GCP gerenciado deve supri-la — não para aprovação de ferramenta externa (Doc 04, Seção 8).
11. Reuso de Dados Mestres Já Existentes
Antes de propor uma nova tabela, toda equipe deve verificar se a entidade já existe no modelo corporativo de dados institucional dos Sistemas do DTIC, quando aplicável ao sistema (ver Modelo Corporativo de Dados). Isso vale especialmente para dados de referência e cadastros mestres (ex.: dados territoriais, cadastrais ou outros dados compartilhados entre domínios), independentemente de a equipe proponente conhecer previamente todo o modelo já existente.
- É responsabilidade de quem propõe a mudança consultar o Modelo Corporativo de Dados e, em caso de dúvida sobre a existência de uma entidade equivalente, confirmar com o Arquiteto de Dados antes de seguir com a modelagem — não presumir que o dado ainda não existe apenas por falta de acesso ou conhecimento prévio do modelo.
- A ausência confirmada de uma entidade equivalente é o único cenário em que a criação de nova tabela é justificável — e, mesmo assim, segue o rito normal de aprovação (Seção 1).
Regra de fonte única da verdade: os Sistemas do DTIC operam com uma única fonte da verdade por entidade de dado mestre. É proibida a duplicação de dados já existentes no modelo corporativo em qualquer sistema ou domínio — tabelas próprias que repliquem um dado mestre já modelado não são permitidas, mesmo como cópia local, cache persistente ou tabela de apoio. O consumo do dado mestre de outro banco pela API de Cadastro Mestre (Doc 06, REG-CM-17) respeita essa regra, pois não há cópia local.
Apêndice A — Exemplo de automação do checklist
Exemplo ilustrativo, não um artefato em uso. Uma consulta sobre o catálogo do PostgreSQL devolve uma linha por violação das regras verificáveis por máquina, e um workflow de CI falha o PR enquanto houver linha. Salvar a consulta como db/checks/verificar_modelagem.sql.
Cobertura: PK UUID com uuidv7(), colunas obrigatórias de auditoria e soft delete, prefixo obrigatório por tipo (booleano, data, data e hora, JSON), prefixo de objeto usado em coluna, identificadores sem acento ou maiúscula, FK com índice e com ON DELETE RESTRICT, TIMESTAMPTZ, trigger de dh_atualizacao, comentário LGPD: em colunas de PII conhecidas, UNIQUE clássico em tabela com soft delete e CHECK de coerência do soft delete. Tabelas de log e de staging que não seguem as colunas obrigatórias (ex.: auditoria.log_operacao) devem ficar fora do escopo, ajustando a lista de schemas e tabelas excluídas no início da consulta. Seguem para revisão manual: plural, idioma, qualidade semântica do nome, identificação de PII não listada e adequação de índices.
WITH t AS (
SELECT c.oid, n.nspname, c.relname
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'p')
AND NOT c.relispartition
AND n.nspname !~ '^pg_'
AND n.nspname NOT IN ('information_schema', 'auditoria')
AND c.relname <> 'schema_migrations'
AND c.relname !~ '_(exclusoes|fusoes)$' -- tabelas de histórico imutáveis
),
col AS (
SELECT t.oid, t.nspname, t.relname, a.attnum, a.attname, a.atttypid
FROM t
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum > 0 AND NOT a.attisdropped
)
-- 1. PK deve ser UUID com DEFAULT uuidv7()
SELECT 'PK_UUIDV7' AS regra, format('%I.%I', t.nspname, t.relname) AS objeto
FROM t
LEFT JOIN pg_index i ON i.indrelid = t.oid AND i.indisprimary
LEFT JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY (i.indkey)
LEFT JOIN pg_attrdef d ON d.adrelid = t.oid AND d.adnum = a.attnum
WHERE i.indexrelid IS NULL
OR a.atttypid <> 'uuid'::regtype
OR COALESCE(pg_get_expr(d.adbin, d.adrelid), '') NOT LIKE '%uuidv7()%'
UNION ALL
-- 2. Colunas obrigatórias de auditoria e soft delete
SELECT 'COLUNA_OBRIGATORIA: ' || r.nm, format('%I.%I', t.nspname, t.relname)
FROM t
CROSS JOIN unnest(ARRAY['dh_cadastro', 'id_usuario_cadastro', 'dh_atualizacao',
'id_usuario_atualizacao', 'fl_excluido', 'dh_exclusao',
'id_usuario_exclusao']) AS r(nm)
WHERE NOT EXISTS (SELECT 1 FROM col WHERE col.oid = t.oid AND col.attname = r.nm)
UNION ALL
-- 3. Prefixo obrigatório pelo tipo e prefixo de objeto usado em coluna
SELECT 'PREFIXO_COLUNA', format('%I.%I.%I', nspname, relname, attname)
FROM col
WHERE (atttypid = 'boolean'::regtype AND attname !~ '^fl_')
OR (atttypid = 'date'::regtype AND attname !~ '^dt_')
OR (atttypid = 'timestamptz'::regtype AND attname !~ '^dh_')
OR (atttypid = 'jsonb'::regtype AND attname !~ '^js_')
OR attname ~ '^(idx|uq|fk|fn|tg|seq)_'
UNION ALL
-- 4. Identificadores só com [a-z0-9_] (sem acento nem maiúscula)
SELECT 'IDENTIFICADOR_INVALIDO', format('%I.%I', nspname, relname)
FROM t
WHERE relname !~ '^[a-z0-9_]+$' OR nspname !~ '^[a-z0-9_]+$'
UNION ALL
SELECT 'IDENTIFICADOR_INVALIDO', format('%I.%I.%I', nspname, relname, attname)
FROM col
WHERE attname !~ '^[a-z0-9_]+$'
UNION ALL
-- 5. FK sem índice na coluna referenciadora
SELECT 'FK_SEM_INDICE', format('%I.%I (%s)', t.nspname, t.relname, con.conname)
FROM t
JOIN pg_constraint con ON con.conrelid = t.oid AND con.contype = 'f'
WHERE NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = t.oid
AND (i.indkey::int2[])[0:cardinality(con.conkey) - 1] = con.conkey)
UNION ALL
-- 6. FK deve ser ON DELETE RESTRICT
SELECT 'FK_NAO_RESTRICT', format('%I.%I (%s)', t.nspname, t.relname, con.conname)
FROM t
JOIN pg_constraint con ON con.conrelid = t.oid AND con.contype = 'f'
WHERE con.confdeltype <> 'r'
UNION ALL
-- 7. Instante de tempo deve ser TIMESTAMPTZ
SELECT 'TIMESTAMP_SEM_FUSO', format('%I.%I.%I', nspname, relname, attname)
FROM col
WHERE atttypid = 'timestamp'::regtype
UNION ALL
-- 8. Tabela sem trigger de dh_atualizacao
SELECT 'TRIGGER_DH_ATUALIZACAO', format('%I.%I', t.nspname, t.relname)
FROM t
WHERE NOT EXISTS (
SELECT 1 FROM pg_trigger tg
JOIN pg_proc p ON p.oid = tg.tgfoid
WHERE tg.tgrelid = t.oid AND NOT tg.tgisinternal
AND p.proname = 'fn_atualizar_dh_atualizacao')
UNION ALL
-- 9. Coluna de PII conhecida sem comentário LGPD
SELECT 'LGPD_SEM_COMENTARIO', format('%I.%I.%I', nspname, relname, attname)
FROM col
WHERE attname IN ('nr_cpf', 'nr_cnpj', 'nr_rg', 'nm_completo', 'nm_social', 'nm_mae', 'dt_nascimento')
AND COALESCE(col_description(oid, attnum), '') NOT LIKE 'LGPD:%'
UNION ALL
-- 10. UNIQUE clássico em tabela com soft delete (usar índice único parcial)
SELECT 'UNIQUE_COM_SOFT_DELETE', format('%I.%I (%s)', t.nspname, t.relname, con.conname)
FROM t
JOIN pg_constraint con ON con.conrelid = t.oid AND con.contype = 'u'
WHERE EXISTS (SELECT 1 FROM col WHERE col.oid = t.oid AND col.attname = 'fl_excluido')
UNION ALL
-- 11. Soft delete sem CHECK de coerência entre fl_excluido e dh_exclusao
SELECT 'CHECK_SOFT_DELETE', format('%I.%I', t.nspname, t.relname)
FROM t
WHERE EXISTS (SELECT 1 FROM col WHERE col.oid = t.oid AND col.attname = 'fl_excluido')
AND NOT EXISTS (
SELECT 1 FROM pg_constraint con
WHERE con.conrelid = t.oid AND con.contype = 'c'
AND pg_get_constraintdef(con.oid) LIKE '%fl_excluido%'
AND pg_get_constraintdef(con.oid) LIKE '%dh_exclusao%')
ORDER BY 1, 2;
Workflow de exemplo (GitHub Actions), aplicando as migrations em um PostgreSQL descartável e rodando a consulta:
name: verificar-modelagem
on:
pull_request:
paths: ['db/migration/**', 'db/checks/**']
jobs:
checks:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:18
env: { POSTGRES_PASSWORD: ci }
ports: ['5432:5432']
options: >-
--health-cmd "pg_isready -U postgres" --health-interval 5s --health-retries 10
env:
PGHOST: localhost
PGUSER: postgres
PGPASSWORD: ci
steps:
- uses: actions/checkout@v4
- name: Aplicar migrations
run: |
for f in $(ls db/migration/V*.sql | sort -V); do
psql -v ON_ERROR_STOP=1 -f "$f"
done
- name: Verificar regras de modelagem
run: |
psql -At -F ' | ' -f db/checks/verificar_modelagem.sql > violacoes.txt
cat violacoes.txt
test ! -s violacoes.txt