DuckDB ou Polars: como processar 50GB sem estourar a RAM
Seu pipeline quebra de madrugada com OutOfMemoryError ao carregar um arquivo de 50GB no Pandas. Na disputa DuckDB ou Polars, use o DuckDB para ter limites rígidos de memória via SQL e spill-to-disk nativo. Já o Polars vence se você prefere pipelines em Python com execução preguiçosa e streaming veloz.
Se você busca contornar esses gargalos mantendo a API tradicional, veja o tutorial do Pandas 3.0. Para arquivos de 50GB em máquinas locais, migrar para motores colunares vetorizados evita desperdício de recursos e falhas de memória.
Como escolher entre DuckDB ou Polars para 50GB?
A diferença central entre os projetos nasce da arquitetura de execução. Enquanto o DuckDB atua como um banco SQL analítico embutido, o Polars processa DataFrames colunares em Rust. Cada motor responde à pressão de memória com mecanismos distintos.
No DuckDB, o controle passa pelo buffer manager nativo. Quando uma consulta exige mais memória do que o teto configurado, o motor descarrega blocos excedentes no disco. Isso protege a execução contra interrupções repentinas do sistema operacional. O analista ganha estabilidade mesmo em junções e agregações volumosas.
Já o Polars divide o volume de dados em lotes estruturados sobre o Apache Arrow. Em vez de represas na memória, ele processa blocos sequenciais em paralelo nos núcleos da CPU. Essa divisão contínua impede que o arquivo sature a RAM, mantendo a execução vetorizada das expressões em Rust rápida e previsível.
| Critério | DuckDB | Polars |
|---|---|---|
| Interface nativa | SQL analítico e API relacional | Expressões e DataFrames fluentes |
| Gestão de excesso | Despejo automático em disco (spill-to-disk) | Processamento em lotes (streaming) e gravação direta em disco via sinks |
| Linguagem base | C++ de alto desempenho | Rust com Apache Arrow |
| Ponto forte | Estabilidade estrita com limites de buffer | Velocidade pura em transformações encadeadas |
Como processar arquivos maiores que a RAM no Polars?
Para ler 50GB no Polars sem estourar a memória, utilize sempre a API preguiçosa com a função scan_parquet. A função lê apenas o cabeçalho e os metadados em vez de carregar dados brutos. O processamento real só acontece quando você dispara a coleta ou a gravação em disco.
A Lazy API do Polars permite processar dados em lotes via streaming. Esse fluxo possibilita executar consultas em datasets que não cabem na memória RAM. Conforme o guia de streaming do Polars, esse motor alivia o consumo de memória. Ele também apresenta desempenho superior ao motor em memória.
Para ativar o modo streaming em uma consulta no Polars, deve-se passar o parâmetro engine="streaming" para o método collect. Quando uma operação não oferece suporte a streaming, o Polars faz fallback automático para o motor de processamento em memória. Lembre-se de que o collect sempre materializa o resultado na RAM.
Consultas de agregação reduzem o volume final, tornando o collect(engine="streaming") seguro para gerar tabelas resumidas. Se o resultado mantiver dezenas de gigabytes, a materialização causará erro fatal de memória. Para saídas volumosas, use sink_parquet() para descarregar o fluxo diretamente no disco.
import polars as pl
# Leitura preguiçosa sem carregar o arquivo na RAM
consulta = (
pl.scan_parquet("pedidos_50gb.parquet")
.filter(pl.col("status") == "entregue")
.group_by("regiao")
.agg(pl.col("total").sum().alias("receita_total"))
)
# Cenário 1: Agregação resumida coletada na memória
tabela_resumo = consulta.collect(engine="streaming")
print(tabela_resumo)
# Cenário 2: Gravação massiva direto no disco sem estourar a RAM
consulta_grande = (
pl.scan_parquet("pedidos_50gb.parquet")
.filter(pl.col("status") == "entregue")
)
consulta_grande.sink_parquet("pedidos_filtrados.parquet")
Antes de iniciar processamentos extensos, inspecione as etapas da consulta para antecipar gargalos. É possível inspecionar o plano físico de execução de uma consulta em streaming plotando o gráfico físico no Polars. Você pode checar o plano chamando consulta.explain() ou gerar a visualização gráfica com consulta.show_graph().
A legenda exibida no gráfico do plano físico indica o quão intensiva em uso de memória cada operação pode ser. Observe as cores e indicadores de intensidade nos nós para identificar operações críticas. Se um nó indicar alto consumo ou fallback para a memória, reposicione filtros para reduzir linhas previamente.
Como limitar o consumo de recursos no DuckDB?
O DuckDB se destaca pela previsibilidade em ambientes com restrição de hardware. Em notebooks ou servidores compartilhados, consumir toda a RAM livre derruba outros processos. O mecanismo evita falhas no sistema por meio de parâmetros diretos de controle de recursos.
Para evitar que a execução comprometa outros serviços no host, limite a memória via SQL com SET max_memory. Em conexões em memória com duckdb.connect(), aponte o diretório de despejo com SET temp_directory para salvar dados excedentes no disco.
import duckdb
# Conexão em memória com spill-to-disk configurado manualmente
conexao = duckdb.connect()
# Limite estrito de memória e pasta para despejo em disco
conexao.execute("SET max_memory = '8GB';")
conexao.execute("SET temp_directory = './temp_duckdb';")
# Cenário 1: Agregação resumida materializada com .df()
resumo = conexao.execute("""
SELECT regiao, SUM(total) AS receita_total
FROM read_parquet('pedidos_50gb.parquet')
WHERE status = 'entregue'
GROUP BY regiao
""").df()
print(resumo)
# Cenário 2: Gravação massiva sem carregar o volume na RAM
conexao.execute("""
COPY (
SELECT *
FROM read_parquet('pedidos_50gb.parquet')
WHERE status = 'entregue'
) TO 'pedidos_filtrados.parquet' (FORMAT PARQUET);
""")
Mesmo em bancos persistentes como duckdb.connect("dados.duckdb"), definir o temp_directory explicitamente via SQL permite direcionar o spill-to-disk para um disco rápido, como um NVMe dedicado. Isso acelera a leitura e a escrita dos blocos temporários quando a consulta ultrapassa o limite de RAM.
O método .df() materializa o resultado na memória RAM, sendo adequado apenas para agregações pequenas. Extrair milhões de linhas sem agregar com .df() esgota a memória do host. Para descarregar transformações volumosas no DuckDB com segurança, utilize o comando SQL COPY (...) TO 'saida.parquet' (FORMAT PARQUET).
Apesar do controle de memória por arquivo temporário, etapas e operadores SQL que não suportam execução fora da memória ainda podem estourar a RAM se o volume intermediário exceder o limite configurado. Simplifique subconsultas complexas e aplique filtros antecipados antes de cruzar tabelas gigantes.
Quando escolher cada ferramenta no seu pipeline?
A escolha entre os motores depende diretamente da linguagem do pipeline e da tolerância a limites de memória. Para estruturar fluxos analíticos robustos, confira como manter um pipeline de dados inteligente. Cada motor atende a um perfil específico de trabalho:
- DuckDB: escolha ideal quando a equipe prioriza SQL analítico e consultas diretas em arquivos locais. O controle de buffer permite rodar cargas pesadas com previsibilidade no consumo de recursos.
- Polars: melhor opção para pipelines construídos puramente em Python com transformações encadeadas de alto desempenho. O motor de streaming com
sink_parquetgrava dados volumosos direto no disco sem sobrecarregar a memória.
Qual o próximo passo prático para testar hoje?
Valide o comportamento de cada ferramenta no seu hardware antes de subir instâncias externas. Para inspecionar uma amostra inicial sem ler o arquivo inteiro na RAM, execute pl.scan_parquet("pedidos_50gb.parquet").head(100).collect(). Em seguida, valide a consulta completa rodando uma agregação com collect(engine="streaming") ou descarregando o filtro com sink_parquet().
No DuckDB, reproduza a mesma lógica fixando max_memory na metade da memória física livre do seu sistema. Monitore a curva de RAM e a escrita em disco durante a execução. Esse teste prático revela com precisão qual motor oferece maior segurança operacional para sua rotina de dados.