# Profiling da fonte — evidência para validar hipótese, quando houver acesso

## Conteúdo

- LGPD e acesso — quando o profiling roda
- Ordem e orçamento
- Bateria por tabela candidata
- Formato de evidência (obrigatório no bundle, `tabelas/*.md`)
- Armadilhas do próprio profiling

## LGPD e acesso — quando o profiling roda

Quando houver acesso ao banco (credencial) ou um select de exemplo, use este
roteiro para validar hipóteses — na maioria dos casos isso só acontece na
esteira, depois do escopo aprovado, não durante a fase de documento. Enquanto
não houver acesso, as decisões estruturais seguem como `hipótese`/
`a-perguntar` (`references/incognitas.md`) — profiling não é pré-requisito
para avançar o documento.

Quando profiling rodar, respeite contrato/cláusula de operador de dados
vigente para acesso a produção do cliente; sem contrato vigente, registre a
pendência em `projeto.md` e trate o acesso como ainda não disponível, em vez
de pedir acesso fora do contrato. PII (nome, CPF, e-mail, telefone, endereço)
NUNCA entra em `SELECT` de linha crua — só `COUNT`/agregação/domínio (query 7
lê valores de status/tipo, não de pessoa). Toda linha de exemplo que precisar
ilustrar um achado é mascarada ANTES de entrar no bundle. Wireframes usam dado
FICTÍCIO por padrão — nunca amostra real, mesmo mascarada.

Decisões estruturais (grain, load type, chave, dedupe) se **testam** contra o dado
real, não se declaram no papel. Kimball: grain não checado contra a fonte = "design
em areia movediça". O profiling roda em modo somente-leitura:

```bash
lumo credential schema credential:<id>          # tabelas/colunas declaradas
lumo credential query credential:<id> "<SQL>"   # queries ad-hoc read-only
```

## Ordem e orçamento

**KPI primeiro, banco depois.** O banco diz o que existe, não o que importa — só
profile tabelas candidatas a fato/dimensão de KPIs já conversados (os Critical
Data Elements). Nunca documente o schema inteiro do cliente.

**Orçamento: a bateria fixa abaixo (≈8 queries) por tabela candidata, e só.**
Achado estranho: registre como "achado a aprofundar" e SIGA — mergulhar em
anomalia é negociação explícita com o analista, nunca reflexo (defesa contra a
espiral infinita de análise). **Acima dos thresholds abaixo**, o achado pesa
mais e vira pergunta prioritária para o analista (aceitar dedupe? excluir?
registrar como risco e seguir?) em vez de nota de rodapé — a resposta pode ser
"segue com hipótese registrada", não precisa travar a fase:

| Achado | Threshold que pede pergunta prioritária |
|---|---|
| Duplicidade na chave candidata (grain check) | > 5% |
| Linha de total embutida na dimensão (`Total`, `Todos`, `Consolidado`, `Geral`, `-1`) | qualquer ocorrência |
| Dimensão cíclica em texto sem ordem declarada (mês, dia da semana) | qualquer ocorrência |
| Órfãos de FK (pseudo + reais) | > 2% |
| Nulos em coluna crítica (chave, data, valor) | > 10% |

## Bateria por tabela candidata

Parametrize `<t>`, `<chave>`, `<dt>`, `<fk>`/`<dim>`/`<pk>`:

```sql
-- 1. Volumetria
SELECT COUNT(*) FROM <t>;

-- 2. GRAIN CHECK — a chave candidata é única? (a query mais importante da fase)
SELECT COUNT(*) AS linhas, COUNT(DISTINCT <chave>) AS distintos FROM <t>;
-- linhas != distintos → grain assumido está errado OU precisa de dedupe. Amostre:
SELECT <chave>, COUNT(*) FROM <t> GROUP BY <chave> HAVING COUNT(*) > 1 LIMIT 20;

-- 3. Nulos nas colunas críticas (chave, data, valor)
SELECT COUNT(*) - COUNT(<col>) AS nulos FROM <t>;

-- 4. Histórico, gaps e DEFASAGEM ATUAL (retenção + timeliness/DMBOK)
SELECT MIN(<dt>), MAX(<dt>), COUNT(DISTINCT DATE_TRUNC('month', <dt>)),
       NOW() - MAX(<dt>) AS defasagem_atual FROM <t>;
-- defasagem_atual > a cadência que vai para aceite.md = achado prioritário:
-- "a fonte já está atrasada hoje; a promessa de frescor é sobre o pipeline ou
-- sobre a origem?" — vale esclarecer antes de escrever o item de frescor do
-- aceite, não precisa travar a fase por isso.

-- 5. IMUTABILIDADE da coluna de tracking (pré-condição de carga Incremental/Temporal)
SELECT COUNT(*) FROM <t> WHERE <updated_at> < <created_at>;   -- deveria ser 0
SELECT COUNT(*) FROM <t> WHERE <updated_at> IS NULL;          -- volume relevante = não confiável

-- 6. Órfãos de FK (fato ↔ dimensão candidatas) — reporte os dois separados
SELECT COUNT(*) FROM <t> f LEFT JOIN <dim> d ON f.<fk>=d.<pk>
 WHERE d.<pk> IS NULL AND f.<fk> IS NOT NULL;                 -- pseudo-órfãos
SELECT COUNT(*) FROM <t> WHERE <fk> IS NULL;                  -- órfãos reais

-- 7. Domínio real das colunas de decisão (status, tipo) + tipo real vs declarado
SELECT <col>, COUNT(*) FROM <t> GROUP BY <col> ORDER BY 2 DESC LIMIT 20;
-- 7b. Plausibilidade das colunas de VALOR (reasonableness/DMBOK): sinal, faixa, outlier
SELECT MIN(<valor>), MAX(<valor>), AVG(<valor>),
       COUNT(*) FILTER (WHERE <valor> < 0) AS negativos FROM <t>;
-- negativos > 0 numa coluna que deveria ser não-negativa = achado prioritário
-- antes de fechar a aditividade do card (sinal invertido? estorno misturado
-- com venda? pergunte, não assuma)

-- 8. Crescimento mensal (insumo direto de Total vs Temporal vs Incremental)
SELECT DATE_TRUNC('month', <dt>) AS mes, COUNT(*) FROM <t> GROUP BY 1 ORDER BY 1;
```

Leituras que saem da bateria:
- **2** valida (ou derruba) o grain e revela necessidade de `dedupe`/chave vigente.
- **4+8** decidem load type: pequena e estável → Total; grande, crescimento
  contínuo, com coluna de data imutável → Temporal; append com upsert → Incremental.
- **5** é pré-condição de Temporal/Incremental: coluna de partição/tracking MUTÁVEL
  (ex. `data_vencimento`, `updated_at` inconsistente) = duplicatas na carga.
  Mitigação padrão quando o tracking é imperfeito: chave única + janela de lookback
  — nunca filtro ingênuo "maior que o último timestamp". Registrar também: carga
  incremental por timestamp NÃO captura deletes na fonte (se o negócio precisa
  refletir exclusão, anotar como risco/decisão).
- **7** pega numérico-como-texto, formatos de data mistos e o vocabulário real de
  status (alimenta a armadilha "status que conta" da entrevista); **7b** pega
  sinal invertido e estorno misturado antes de a aditividade fechar.
- **8** também é o teste de completude populacional: mês com COUNT muito abaixo
  da vizinhança é achado (sazonalidade real, a validar, ou gap de carga na
  origem — em fato fiscal/financeiro, mês inteiro ausente é suspeito por padrão).

Leituras opcionais (fora do orçamento fixo de ~8, valem quando o caso pede):
- **Dimensão degenerada**: `COUNT(DISTINCT <chave-dimensão>)` no fato ≈
  `COUNT(*)` do fato → a dimensão não carrega atributo próprio, é degenerada —
  vira coluna do fato, não tabela separada.
- **Extração que agrega na origem**: a query de extração tem `GROUP BY`? O
  grain real da fato é a lista de colunas do GROUP BY — documentar TODAS;
  mudar métrica depois é reescrever a query de extração. Decisão estrutural de
  alto risco, checar antes de fechar o grain (ver `limites-horus.md`).
- **Cardinalidade dos candidatos a `key_columns`**: `COUNT(DISTINCT <col>)` /
  `COUNT(*)` do candidato — cardinalidade baixa demais é sinal de pergunta
  antes de fechar o grain, nunca decisão silenciosa.

## Formato de evidência (obrigatório no bundle, `tabelas/*.md`)

Uma linha por decisão estrutural:

```
| Decisão | Teste | Resultado | Status |
|---|---|---|---|
| Grain: 1 linha = item de pedido | COUNT vs COUNT DISTINCT(id_item) em pedidos_item | 6.712.007 = 6.712.007 | Validado (02/07/2026) |
| Load Temporal por data_pedido | imutabilidade: updated<created | 0 linhas; porém 12% updated_at NULL | Premissa não validada — RISCO |
```

Sem query rodada (sem acesso, tabela grande demais para o orçamento, credencial
indisponível)? A decisão é **premissa** e entra em riscos com dono e plano de
validação — **incluindo o rastro da tentativa**, não só a alegação:
`credencial_solicitada_em`, `solicitada_a` (pessoa), `resposta:
pendente|negada|nao-solicitada`. Marque `nao-solicitada` até efetivamente
tentar o pedido — isso não impede avançar as demais fases, só deixa
registrado que o acesso ainda nem foi pedido.

**Checklist de credencial** (junto do rastro de tentativa, mesmo bloco de
risco): role DEDICADA e restrita só às tabelas candidatas (nunca a credencial
de produção do cliente inteira); zero permissão de DDL/DML; timeouts
configurados (lock ~3-5s, statement ~30s — profiling não pode travar a origem
do cliente); janela de acesso combinada com o cliente (sem janela definida =
mesma trava de "credencial negada", não suposição de acesso irrestrito);
réplica de leitura, se existir, em vez do primário; encerramento registrado
com `credencial_revogada_em` + `confirmado_por` — acesso que não se sabe
quando foi revogado é acesso que nunca foi revogado. Enquanto o acesso não libera, peça ao stakeholder 2
exemplos concretos da linha candidata a grain (um típico + uma exceção que ele
SABE existir — duplicado, sem chave) e registre como `hipotese_negocio` no bloco
de risco — a query, quando rodar, confirma ou derruba a hipótese. Nunca apresente suposição como fato validado — e o
auto-review compara `Resultado` × `Status` linha a linha: grain check com
`linhas ≠ distintos` e `Status: Validado` na mesma linha é achado
prioritário — pergunte antes de assumir o dedupe, não precisa travar a fase.

## Armadilhas do próprio profiling

- **Amostra ≠ prova.** Resultado obtido com `LIMIT`/amostra deve ser marcado como
  amostral — unicidade e distribuição só se provam com agregação completa.
- **Homologação ≠ produção.** Host/nome da credencial sugere staging/homolog/dev?
  Aviso obrigatório em riscos: volumetria e sujeira de produção podem divergir;
  pedir confirmação.
- **`COUNT(DISTINCT)` em tabela grande sem índice custa caro** no banco DO CLIENTE.
  Prefira horário combinado com o analista; se inviável, registre como premissa.
- **Schema declarado mente.** O que `credential schema` retorna é o declarado;
  tipo real vem da query 7. Coluna "data" em VARCHAR é achado, não detalhe.
