Registro da execução ponta a ponta sobre os dados reais da RFB e da Base dos Dados, da comparação
numérica com o MVP original (AD-018) e da validação da atualização mensal. Números de 2026-10-01,
máquina de referência: MacBook (Apple Silicon, 15 núcleos, 48 GB de RAM, SSD), macOS, com
caffeinate -i (P20). Os dados ficaram fora do repositório (dados/, ignorado pelo git).
Comando: RAIZ_DADOS=dados/real-2026-09 caffeinate -i rfb pipeline --mes 2026-09 (target dev,
DBT_THREADS=8, DUCKDB_THREADS=8, DUCKDB_MEMORY_LIMIT padrão 24 GB).
Resultado: dbt build PASS=339 WARN=14 ERROR=0 (NO-OP=1), paridade bh_empresas × SQL original =
0 diferenças em 71.396.386 linhas, relatório gerado, estado gravado (2026-09, extrato de
2026-09-12). Saída 0.
| Etapa | Tempo | Observação |
|---|---|---|
| Download WebDAV (31 zips, 7,07 GB) | ~21 min (≈ 5,5 MB/s) | sem travamentos nem retomadas; Estabelecimentos0.zip tem 2,2 GB |
| Conversão CSV → Parquet | 162 s | empresas 21,8 s (70,1 M), estabelecimentos 124,2 s (73,4 M), simples 7,2 s (50,4 M), domínios e BD < 1 s cada |
| Ingestão numa reexecução (nada mudou) | 10–12 s | manifesto: só sha256 dos zips; entidades puladas |
dbt source freshness |
3,1 s | |
dbt build (8/8 threads) |
318 s | bh_empresas 153 s, paridade_bh_empresas 109 s, bh_empresas_descartes_inner_join 92 s, int_estabelecimentos__enriquecidos 49 s, bh_empresas_nome_alterado_por_trim 44 s, cnpj_dv_valido 42 s |
Relatório (docs/RELATORIO_ESTUDO_CASO.md) |
4,8 s | |
| Publicação | 0 s | MotherDuck não configurado ("nada publicado") |
| Total do pipeline (com o raw já baixado) | 339 s | + ~24 min de download/conversão na 1ª vez |
| Camada | Objeto | Linhas | Tamanho |
|---|---|---|---|
| raw | rfb/empresas |
70.085.592 | 1,1 GB (Parquet zstd) |
| raw | rfb/estabelecimentos |
73.366.147 | 3,6 GB |
| raw | rfb/simples |
50.396.768 | 208 MB |
| raw | domínios RFB (cnaes, municípios, naturezas, motivos, países, qualificações) | 1.359 / 5.572 / 91 / 63 / 255 / 68 | < 100 KB |
| raw | BD (município 5.571, cnae_2 1.356, população 191.099, PIB 122.466, censo 5.570, RM 1.385, vizinhança 522.822) | 7,7 MB | |
| staging | stg_rfb__empresas (após a deduplicação, §3) |
70.085.591 | view |
| staging | stg_rfb__estabelecimentos / stg_rfb__simples |
73.366.147 / 50.396.768 | view |
| intermediate | int_estabelecimentos__enriquecidos / int_cnaes_secundarios__explodidos |
73.366.147 / 125.421.187 | tabelas no .duckdb |
| gold (original) | bh_empresas |
71.396.386 | 3,8 GB |
| gold (original) | agg_empresas |
5.812.927 | 75 MB |
| gold (core) | fct_estabelecimentos |
73.366.147 | 1,26 GB |
| gold (core) | bridge_estabelecimento_cnae_secundario |
125.421.187 | 1,57 GB |
| gold (core) | fct_resumo_mensal (partição 2026-09) |
5.763.929 | 62 MB |
| gold (core) | dimensões (dim_data 46.279, dim_municipio 5.572, dim_cnae 1.357, natureza 92, porte 5, situação 6) |
< 1 MB | |
| gold (analytics) | mart_dinamica_mercado 16.138.510; mart_concorrencia_municipio e _area_mercado 2.027.104; mart_sobrevivencia_coorte 1.669.209; mart_fornecedores_proximos 1.726 |
313 MB | |
| gold (observabilidade) | dq_historico_testes/ (RBP-12): uma partição por execução |
6 execuções, 1.570 linhas | — |
Disco: zips 6,7 GB + raw 4,9 GB + gold 6,6 GB + warehouse.duckdb 14 GB ≈ 32 GB por mês
(os zips somem após rfb atualizar, salvo RFB_MANTER_ZIPS=true).
| Teste | Linhas | Leitura |
|---|---|---|
bh_empresas_descartes_inner_join |
1.969.761 | estabelecimentos que o SQL original descarta nos inner joins (natureza/CNAE/município sem par) — comportamento original preservado; 73.366.147 − 71.396.386 |
dat_situacao >= dat_inicio_atividade (staging) |
1.058 | datas inconsistentes na fonte |
bh_empresas_nome_alterado_por_trim |
861 | nomes com espaço nas bordas (trim declarado, ADR-0005/R2-01) |
MEI ⇒ Simples (fct_estabelecimentos) |
494 | opção MEI sem opção pelo Simples na fonte |
PIB: pib ≈ va + impostos com tolerância de 0,01 % |
477 | arredondamento da BD; o teste de 1 % (error) passou (P24, §4) |
data_nao_futura (estabelecimentos: situação 386, início 227; simples: opção Simples 222, MEI 213) |
1.048 | datas posteriores ao extrato na fonte |
accepted_range de dat_inicio_atividade ≥ 1900 |
4 | datas absurdas (R2-02), vão para sk_data = -2 |
cnaes_sem_par_bd |
3 | subclasses RFB sem par na BD |
dim_municipio.codigo_rfb not null |
1 | município sem código RFB (P24, §4) |
stg_bd__populacao.id_municipio not null |
1 | linha de 2025 sem município (§3) |
unicidade de empresas.cnpj_raiz na fonte |
1 | raiz duplicada no extrato (§3) |
extrato_desatualizado passou (extrato de 2026-09-12, 19 dias). Os avisos ficam com
store_failures no warehouse e no histórico de DQ (gold/dq_historico_testes/).
Builds completos do dbt build (2026-09, mesmos dados), pico de RSS do processo do dbt
(/usr/bin/time -l):
DBT_THREADS/DUCKDB_THREADS |
DUCKDB_MEMORY_LIMIT |
dbt build |
Pico de RSS | Resultado |
|---|---|---|---|---|
| 8 / 8 | 24 GB (padrão dev) |
318 s | 25,9 GB | ERROR=0 |
| 4 / 4 | 24 GB | 383 s | 24,9 GB | ERROR=0 |
| 4 / 4 | 12 GB | 431 s | 18,7 GB | ERROR=0 |
| 4 / 4 | 8 GB | 460 s | 13,8 GB | ERROR=0 |
O pico acompanha o memory_limit do DuckDB, não as threads: o limite vale para o buffer manager, e o
processo soma a ele os buffers de leitura de Parquet, o próprio dbt e o Python (~1–2 GB). Reduzir as
threads de 8 para 4 só baixou o pico em 1 GB e custou 20 % de tempo; reduzir o limite de 24 para 12 GB
baixou 7 GB e custou 35 %; com 8 GB o pico ficou em 13,8 GB (+45 % de tempo). O spill foi para RAIZ_DADOS/_tmp (temp_directory).
Recomendação (em docs/OPERACAO.md): máquina com ≥ 32 GB de RAM com os padrões, ou
DUCKDB_MEMORY_LIMIT ≈ metade da RAM física. Máquina de 16 GB: DUCKDB_MEMORY_LIMIT=8GB e DBT_THREADS=DUCKDB_THREADS=4 (pico medido 13,8 GB; apertado, feche outros programas — não testado numa máquina de 16 GB de fato).
A primeira execução real falhou (corretamente, saída 1) e expôs quatro problemas, todos resolvidos neste lote e cobertos por testes:
| Problema | Sintoma | Decisão |
|---|---|---|
Bytes 0x80–0x9F (controles C1) em 5 campos de Estabelecimentos0/4/7 |
o DuckDB recusa o arquivo: "File is not latin-1 encoded" (o latin-1 do Python aceita) | conversão relê o CSV após transcodificar latin-1 → UTF-8 em Python, só quando o DuckDB recusa; aviso com a contagem (conversao.py, teste test_bytes_de_controle_c1_sao_relidos_em_utf8). Escopo adaptado |
Raiz 08314885 duplicada em Empresas2 (linha "fantasma": sem razão social, natureza 0000, porte e qualificação zerados) |
unique da fonte (error) falhou; o SQL original duplicaria os 51 estabelecimentos da raiz |
staging mantém uma linha por raiz (empresa_preferida_por_raiz: com razão social e natureza ≠ 0000), a leitura da paridade emula o mesmo; teste da fonte vira warn + store_failures; unit test test_stg_rfb__empresas_raiz_duplicada_vira_uma_linha. Escopo adaptado (docs/ESCOPO.md) |
Linha da BD populacao (2025) sem id_municipio nem UF (5.877 hab.) |
not_null (error) falhou |
warn + store_failures: a linha não casa com município nenhum e não afeta os marts |
CNPJ alfanumérico (00000000 E08G 12, Banco do Brasil, o primeiro do extrato) |
paridade_bh_empresas FAIL 2: a emulação do inferSchema fazia try_cast(cnpj_ordem as bigint) → NULL |
o Spark tiparia a coluna como texto se um valor não fosse inteiro; a emulação passou a fazer o mesmo por coluna e mês (inferschema_inteiro_ou_texto, unit test test_audit__bh_empresas_sql_original_cnpj_alfanumerico). bh_empresas já estava certo |
SET temp_directory reaplicado pelo dbt-duckdb a cada cursor |
"Cannot switch temporary directory after the current one has been used" em builds parciais com 8 threads | temp_directory passou de settings para config_options (aplicado uma vez no connect) |
Extrato real de fev/2025 (o mesmo mês do MVP; arquivos internos …D50208… → _data_referencia
= 2025-02-08), convertido dos CSVs do projeto v1 (só leitura). Base dos Dados: as 4 tabelas do
original (municipio, cnae_2, populacao, pib) na versão usada pelo v1; censo, RM e vizinhança
copiadas do raw de 2026-09 (não existiam no original). Comando:
RAIZ_DADOS=dados/real-2025-02 rfb pipeline --sem-ingestao --mes 2025-02.
Build: PASS=344 WARN=9 ERROR=0 em 258 s (pico 27,0 GB, 8/8 threads); paridade_bh_empresas
PASS (0 diferenças em 62.586.264 linhas; 64.547.773 estabelecimentos, 1.961.509 descartados pelos inner
joins do original). Avisos a mais que em 2026-09: extrato_desatualizado (3 entidades: o extrato tem
mais de 65 dias, esperado) e idade fora de [0, 200] (2 linhas).
As consultas do notebook 4 foram reexecutadas literalmente (o SQL publicado, sobre o gold do port:
agg_empresas, bh_empresas e municipio_bd = raw da BD):
| Pergunta (notebook 4) | Publicado no original | Port, SQL literal | Port, analysis/relatório | Divergência |
|---|---|---|---|---|
| 1. Concorrentes 4741500 em Fundão/ES | 1 ativa (Empresário Individual) + 4 inativas | 1 ativa (EI, MICRO) + 4 inativas (3 EI + 1 Ltda.) | 1 ativo e 4 inativos | nenhuma |
| 2. Idade média das ativas | 3,9 anos | 3,8 | 3,8 | explicada (P15) — abaixo |
| 3. Porte das ativas | MICRO | MICRO (1 empresa) | MICRO | nenhuma |
| 4a. Fabricante (2071100) no município | 0 | 0 | 0 | nenhuma |
| 4b. Fabricante na microrregião | 0 | 0 | 1 | adaptação declarada (R3-04) |
| 4c. Fabricante na mesorregião | 0 | 0 | 1 | idem |
| 4d. Atacadistas (4679601, 4679699) na mesorregião | 0 | 0 | 12 | idem |
| 4e. CNAE de fornecedor como secundário na mesorregião | 0 | 0 | 155 | idem |
| 4f. Idem, na UF (ES) | "muitas opções", citando Serra, Vila Velha, Vitória, Cariacica e Linhares | 1.977 ativos; os maiores municípios são Serra (479), Vila Velha (315), Cariacica (246), Vitória (156), Cachoeiro (118), Linhares (65) | 1.977 | nenhuma (o original não publicou a contagem) |
P15 — idade 3,8 × 3,9. A única ativa iniciou atividade em 2021-05-06. O port mede a idade na
data do extrato (ADR-0004): 2025-02-08 − 2021-05-06 = 1.374 dias / 365,25 = 3,76 → 3,8. O notebook
usava now() no momento em que rodou: 3,9 corresponde a qualquer data entre 2025-03-13 e
2025-04-17 (1.407 a 1.442 dias), ou seja, o notebook foi executado de 5 a 10 semanas depois da
publicação do extrato. Reprodução com --vars (dbt run --select bh_empresas agg_empresas --vars '{mes_referencia: 2025-02, data_referencia: <data>}' e a consulta da pergunta 2):
data_referencia |
Idade média (MICRO, 1 empresa) |
|---|---|
| (padrão: 2025-02-08, data do extrato) | 3,8 |
| 2025-03-12 | 3,8 |
| 2025-03-13 | 3,9 |
| 2025-04-17 | 3,9 |
| 2025-04-18 | 4,0 |
A diferença é a adaptação declarada do ADR-0004 (determinismo), não erro de cálculo.
Pergunta 4 (R3-04). O SQL literal do notebook dá 0 nas buscas por micro e mesorregião porque compara
microrregiao_municipio/mesorregiao_municipio de agg_empresas (MAIÚSCULAS, herdadas do SQL do
notebook 3) com nome_microrregiao/nome_mesorregiao da BD em grafia mista ("Linhares"): o port
reproduz esse 0 com o SQL literal, e a analysis adaptada (upper() dos dois lados) mostra o que o
original deveria ter achado — 1 fabricante na micro/mesorregião, 12 atacadistas e 155 estabelecimentos
com o CNAE de fornecedor como secundário. A conclusão do notebook ("não há fornecedores nas
imediações") decorre desse bug; o relatório do port traz a nota de adaptação.
2026-09 para comparação: a empresa ativa de 2025 passou a inativa; em 2026-09 Fundão tem 0 ativos
e 5 inativos no CNAE 4741500 (relatório em docs/RELATORIO_ESTUDO_CASO.md).
dim_municipio.codigo_rfbnot null (warn): 1 linha, exatamente a prevista — Boa Esperança do Norte/MT (IBGE 5101837, criado em 2025), semid_municipio_rfna BD. Fica na dimensão (para o BI), não casa com estabelecimentos;uniquedecodigo_rfbpassou. Mantido como warn.- Identidade
pib ≈ va + impostos_liquidos: o teste de 1 % (error, RBP-06) passou nas 122.466 linhas. O de 0,01 % (warn) acusou 477 linhas, todas com diferença de exatamente R$ 1.000 (máximo 0,037 % do PIB; ex.: 2210383/2002, PIB 2.720.000 × VA + impostos 2.721.000): é o arredondamento em milhares das séries antigas do IBGE convertidas para reais, não troca de coluna nem de unidade. Decisão: manter error em 1 % e warn em 0,01 % (o aviso documenta o arredondamento; um erro real de unidade daria diferenças de 1.000× ou de 100 %). - População sem município (warn novo, §2): 1 linha (2025, 5.877 habitantes, sem
id_municipionem UF) — provavelmente o mesmo município novo; não casa com nada e não afeta densidade.
RAIZ_DADOS=dados/real-2026-09 rfb atualizar, depois do pipeline de 2026-09, contra o WebDAV da RFB:
nenhum mês novo (mais recente completo: 2026-09; último processado: 2026-09)
Saída 0 em 0,56 s, só com a listagem (PROPFIND): nenhum arquivo baixado (_baixados/2026-09/
inalterado), nenhum dbt executado. Em 2026-10-01 a pasta de 2026-10 ainda não existia no WebDAV.
O fluxo com mês novo, falha do dbt e retenção roda a cada make ci sobre as fixtures de dois meses
(tests/integration/test_atualizar.py).
Medição somente leitura sobre o gold real de 2026-09 (raw por symlink num RAIZ_DADOS próprio; o gold do B8 não
foi tocado): dbt build -s +mart_concorrencia_area_mercado --resource-type model seed, DBT_THREADS=8,
DUCKDB_MEMORY_LIMIT 24 GB (target dev), 2026-10-01.
| Antes (grão local) | Depois (com vazios) | |
|---|---|---|
| Linhas | 2.027.104 | 3.936.977 (1,94×) |
das quais vazios (tem_estabelecimento_local = false) |
— | 1.909.873 (48,5 %) |
| CNAEs / municípios distintos | — | 1.343 / 5.571 |
| Parquet | 74 MB | 142 MB |
| Tempo do modelo | — | 2,1 s (build das 27 dependências + modelo: 41 s) |
| Pico de RSS do processo do dbt | — | 20,8 GB (limite DuckDB de 24 GB; o build refaz int_estabelecimentos__enriquecidos) |
Abaixo do limite combinado (3× ou 15 M linhas): sem mudança de abordagem. Os 18 testes do mart e o singular
area_mercado_invariantes (nenhum par com ativos na área fica de fora) passam sobre as 3,9 M linhas. Vazios só para CNAE e município conhecidos (-1 fica de fora: 722 linhas a
menos que na primeira medição, 3.937.699 / 1.910.595); 0 vazios com sk_cnae = -1.
O grão cruzado completo seria ~6× o original; os vazios (só onde vizinhos/RM têm ≥ 1 ativo) são ~0,94×.