DuckDB ou Polars: como processar 50GB sem estourar a RAM

Dois servidores em torre iluminados por luzes de neon azul e roxa em uma estacao de trabalho futurista escura.

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_parquet grava 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.