5

Importei 55 milhões de empresas para PostgreSQL em menos de 3 horas, e como a IA não ajudou nisso

Construí o CNPJ Aberto em cima dos CSVs da Receita (latin-1, ;, vários ZIPs). O desafio foi repetir a carga todo mês sem passar o dia inteiro esperando.

Resumo do que aprendi: ORM e executemany não são ferramenta de bulk load. O caminho foi COPY, temp table, dropar índice na carga e CREATE INDEX CONCURRENTLY depois.

Resumo mais importante: A IA é limitada neste ponto, ela otimiza um pouco, mas depois é com você. A pesquisa ainda se faz necessária para chegar nas 3 horas. Não cheguei a fazer benchmark disso, mas estivo que, depois de conversar muito com IA e todos modelos, cheguei ao máximo de ~4hrs.

Números rápidos

TabelaRegistros (ordem de grandeza)CSV
empresas~55M~4 GB
estabelecimentos~70M~15 GB
sócios~25M~3 GB
simples~35M~2 GB

Uns ~25 GB descompactados.

O que não funcionou

SQLAlchemy em loop — milhares de inserts por segundo, horas de fila.

executemany em batch — melhor, mas ainda um INSERT por linha (parse/plan por statement). Continuava na casa das horas.

O que funcionou: COPY

O COPY do Postgres escreve no heap sem passar pelo mesmo caminho de um INSERT normal. Na prática foi ordens de magnitude mais rápido.

Sessão mais “agressiva” só para carga (se cair, reimporta):

SET synchronous_commit = off;
SET work_mem = '256MB';

Índices: derrubar os que não forem PK, importar, recriar com CONCURRENTLY para não travar leitura do site.

Upsert mensal: COPY não faz ON CONFLICT. Fluxo que usei: CREATE TEMP TABLE tmp (...) LIKE real INCLUDING DEFAULTSCOPY na temp → INSERT ... ON CONFLICT DO UPDATE da temp para a tabela final → dropar temp.

Paralelismo: ZIPs independentes → ThreadPoolExecutor com poucos workers importando tabelas diferentes em paralelo; SSD aguenta bem.

Comparativo (ordem de grandeza)

AbordagemThroughputTempo total (empresa + resto)
ORM~1,2k/s12+ h (só empresas já dava nisso)
executemany~4k/s4+ h
COPY + temp + índice depois + paralelo~80k/s< 3 h

Lições que eu levaria para outro projeto, ou que você deve levar ao seu projeto

  1. COPY é o default mental para carga massiva no Postgres.
  2. Índice na carga quase sempre piora tempo total; recriar depois ganha.
  3. Temp table liga COPY a merge/upsert sem gambiarra no parser.
  4. synchronous_commit = off em job de importação controlado é tradeoff aceitável.
  5. Tamanho de batch (~200k linhas por transação COPY) equilibra memória vs overhead.

O que acabou salvando bastante foi o uso de trigramas na base. Vou organizar um post sobre isso na sequência.

Carregando publicação patrocinada...
1

Trabalho com essa base de CNPJs.

Inclusive os arquivos da receita tem alguns dados inconsistentes.

Entre os arquivos e o banco final (uso o AWS Aurora Postgresql) eu usei o DuckDB. E um app feito em GO para fazer o download, descompactação e adequação a UTF8, o app então usa o DuckDB para carregar os arquivos em um arquivo único, normalizo o que preciso no DuckDB e retiro as inconsistências. Depois o DuckDB já exporta chunks já com gzip e subo para o S3. Depois o Aurora carrega tudo do S3. Índices são carregados depois.

Mensalmente ao invés de eu recriar a base do zero, adotei um padrão de dectar as diferenças entre meses localmente com DuckDB e só subo as atualizações. Inclusive uso um modelo SCD Tipo 2 no banco de destino. Assim consigo ter os históricos das alterações.

2

É uma boa lógica, no meu caso, cheguei a conslusão que é mais fácil reimportar tudo mês a mês. Poupa dor de cabeça nos upserts. Não conhecia o duckdb, vou dar uma olhada.

1
1
1

Eu tenho exatamente o mesmo projeto com as mesmas base, a diferença que fiz com python FASTAPI e Duckdb e de quebra coloquei wren.ai para gerar automagicamente as condultas SQL no duckdb para mim fazendo uma pergunta simples, como, " Liste as empresas que tem capital maior que 100k"

0
0