O formato .dbc e o streaming
Anatomia de um .dbc
Um .dbc do DATASUS não é um formato próprio de compressão — é um DBF (dBase III) com uma cirurgia:
┌────────────────────────────┐
│ cabeçalho DBF em claro │ ← contagem de registros, larguras,
│ (tamanho nos bytes 8–9) │ descritores de campo, language driver
├────────────────────────────┤
│ 4 bytes (CRC32) │ ← ignorados na leitura
├────────────────────────────┤
│ registros comprimidos em │ ← PKWare DCL ("implode"),
│ PKWare DCL │ o mesmo do PKZIP 1.x
└────────────────────────────┘O corpo descomprimido é o corpo do DBF original: registros de largura fixa, byte 0 = flag de deleção (0x20 ativo, 0x2A deletado), campos concatenados sem separador, e um 0x1A de EOF no final.
Por que os microdados "pesam"
Duas multiplicações em cascata:
- Expansão do DCL: o DBF de largura fixa comprime muito bem (campos com espaços, códigos repetidos), então o
.dbcexpande tipicamente 4–8× ao descomprimir. - Materialização como
String: cadaStringem Julia carrega ~40 bytes de overhead de objeto. Um DBF de 60 campos × 1 milhão de registros materializado ingenuamente vira dezenas de GB de pressão no GC. (O R mitiga isso por acidente: factors são pooled e há um string pool global.)
O MicroSUS ataca as duas pontas: nunca materializa o arquivo descomprimido (streaming) e, quando materializa valores, usa InlineStrings (sem ponteiro) e PooledArray (categóricas).
O descompressor DCL
src/dcl.jl é um porte puro Julia do blast.c de Mark Adler (o descompressor de referência do formato, usado pelo read.dbc do R via C). Propriedades que importam aqui:
- O DCL é um LZ77 com janela de até 4 KiB e códigos Huffman fixos (tabelas embutidas no formato) — descomprime incrementalmente por natureza.
- O porte emite a janela a um
sink(chunk)a cada preenchimento: memória O(1) em relação ao arquivo, exatamente a arquitetura dooutfundo blast original. - A cópia de matches é byte a byte para preservar a semântica de sobreposição (
dist < len⇒ repetição de padrão) e lida com o wrap circular da janela.
O pipeline
.dbc ──DCL, chunks de 4 KiB──▶ montagem de registros ──▶ filtro ──▶ parse ──▶ lotes
(task própria) (atravessa fronteiras (campo (só as (Channel de
de chunk) sob colunas NamedTuples)
demanda) pedidas)Cada estágio é encadeado por Channels com buffer pequeno, então o backpressure é automático: se o consumidor (seu for sobre Tables.partitions, ou o Arrow.write) desacelera, a descompressão espera. O teto de memória do processo é O(tamanho_lote) — o lote em construção mais um em trânsito — independente do tamanho do arquivo.
Consequência prática: um SINASC nacional multi-ano, ou o recorte de um único município dentro do DNBA inteiro, passam pelo leitor de um notebook sem drama.
Ordem das otimizações no leitor
Para cada registro bruto:
- flag de deleção — registros
0x2Asão descartados antes de qualquer coisa; filtro— recebe umMicroSUS.RegistroDBF, uma visão leve sobre os bytes;r[:CAMPO]decodifica só o campo consultado. Linhas rejeitadas não materializam nenhuma coluna;- parse das colunas selecionadas — numéricos e datas são parseados direto dos bytes (sem
Stringintermediária); texto passa pelo fast path ASCII e só paga transcodificação quando há byte ≥0x80.