--:--:--

Planilhas gigantes sem travar: 6 caminhos para comparar milhões de linhas

2026-08-02T10:00:00Z

Guia completo para tratar, comparar e exportar planilhas gigantes com DuckDB, Power Query, pandas, Polars, SQLite ou PostgreSQL, incluindo instalação, comandos e critérios de escolha.

Planilhas gigantes sem travar: 6 caminhos para comparar milhões de linhas

Quando uma planilha passa de algumas centenas de milhares de linhas, o problema deixa de ser apenas “o Excel está lento”. O arquivo demora para abrir, filtros engasgam, fórmulas precisam ser copiadas por colunas inteiras, o PROCV consome memória e uma exportação pode terminar com um arquivo que nem cabe em uma única aba. A solução não é comprar um computador indefinidamente mais forte nem dividir tudo em dezenas de arquivos manuais. É mudar a ferramenta usada para processar os dados.

Neste guia vamos construir uma solução local, gratuita e reproduzível usando DuckDB, Parquet, SQL e Python. O Excel continuará sendo útil para receber e apresentar informações, mas não será obrigado a funcionar como banco de dados. Ao final, você terá um fluxo capaz de importar planilhas grandes, eliminar colunas vazias, padronizar códigos e valores, comparar milhões de registros e exportar somente o resultado necessário.

Resumo da arquitetura
Excel é entrada e saída. Parquet é armazenamento analítico compacto. DuckDB é o motor que consulta e compara. SQL descreve as regras. Python automatiza importações e exportações quando há muitos arquivos.

Por que o Excel sofre com planilhas gigantes?

O Excel é excelente para análise visual, fórmulas, pequenos modelos e entrega de relatórios. Ele não foi projetado para ser o melhor mecanismo de varredura, junção e agregação de milhões de registros. Uma planilha XLSX também é um pacote de arquivos XML compactados. Para trabalhar com ela, o programa precisa descompactar estruturas, interpretar células, estilos, fórmulas e tipos.

Há ainda um limite físico: uma aba aceita 1.048.576 linhas. Mesmo antes de alcançar esse teto, fórmulas como PROCV, PROCX, SOMASES e combinações de ÍNDICE/CORRESP aplicadas em centenas de milhares de células podem consumir muita memória. Se duas tabelas têm um milhão de linhas, a tentativa de compará-las dentro da grade transforma cada célula calculada em trabalho adicional.

Um banco analítico opera de outro modo. O DuckDB lê dados em blocos, processa colunas vetorialmente e pode consultar arquivos Parquet sem carregá-los inteiros na memória. Em vez de criar um milhão de fórmulas, fazemos uma única operação SQL.

O que vamos instalar

A solução principal utiliza quatro componentes:

  • DuckDB: banco analítico embutido. Não exige servidor, usuário, porta nem serviço rodando em segundo plano
  • Python: usado para ler XLSX em blocos lógicos, automatizar pastas e criar arquivos de saída
  • Visual Studio Code: editor opcional, mas conveniente para guardar scripts e consultas
  • Parquet: formato colunar comprimido; não é um programa e não precisa ser instalado

Links oficiais: instalação do DuckDB, download do Python e download do VS Code.

Passo 1: instalar o Python corretamente no Windows

  1. Acesse o site oficial do Python e baixe a versão estável para Windows
  2. Execute o instalador
  3. Marque Add Python to PATH antes de continuar
  4. Escolha a instalação padrão
  5. Abra um novo PowerShell ou Prompt de Comando

Confira a instalação:

python --version
python -m pip --version

Em algumas instalações do Windows, o comando é py. Se python não funcionar, teste:

py --version
py -m pip --version

Não use o comando pip isoladamente se houver mais de uma versão do Python. O padrão python -m pip reduz o risco de instalar pacotes no interpretador errado.

Passo 2: criar uma pasta e um ambiente virtual

Crie uma pasta como C:\dados_gigantes. Abra essa pasta no terminal e execute:

cd C:\dados_gigantes
python -m venv .venv
.venv\Scripts\activate

Se o PowerShell bloquear a ativação, abra o Prompt de Comando tradicional e use o mesmo comando de ativação, ou execute temporariamente no PowerShell:

Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.venv\Scripts\Activate.ps1

A opção -Scope Process vale apenas para a janela atual. Depois, atualize o instalador de pacotes e instale as bibliotecas:

python -m pip install --upgrade pip
python -m pip install duckdb pandas openpyxl pyarrow xlsxwriter

Função de cada pacote: duckdb executa SQL; pandas ajuda na leitura e preparação; openpyxl lê XLSX; pyarrow oferece suporte eficiente a Parquet; e xlsxwriter cria arquivos Excel de saída.

Passo 3: instalar a linha de comando do DuckDB

O pacote Python já contém o motor, portanto esta etapa é opcional. A interface de linha de comando é útil para testar SQL sem escrever programa. Na página oficial de instalação, escolha Command line, Windows e a arquitetura do computador, normalmente amd64. Baixe o ZIP, extraia duckdb.exe e coloque-o na pasta do projeto.

Abra o terminal nessa pasta e crie o banco:

.\duckdb.exe dados.duckdb

Dentro do console, alguns comandos úteis:

.help
.tables
.schema
.mode box
SELECT version();
.quit

O arquivo dados.duckdb é o banco inteiro. Faça backup dele como faria com qualquer arquivo importante. Evite abri-lo simultaneamente para escrita em vários programas.

Estrutura recomendada das pastas

C:\dados_gigantes\
├── .venv\
├── entrada\
│   ├── base_antiga.xlsx
│   └── base_nova.xlsx
├── parquet\
├── saida\
├── converter.py
├── comparar.sql
└── dados.duckdb

Nomes simples evitam problemas de caminho. O DuckDB aceita barras normais mesmo no Windows, por exemplo C:/dados_gigantes/parquet/base_nova.parquet.

Por que converter XLSX para Parquet?

Parquet armazena dados por coluna, preserva tipos e aplica compressão. Se uma consulta precisa apenas de codigo e valor, o motor pode evitar a leitura das demais colunas. O formato também permite filtros e projeções eficientes. XLSX é ótimo para pessoas; Parquet é ótimo para motores analíticos.

Não existe promessa de que todo Parquet será sempre menor, mas tabelas repetitivas costumam comprimir muito bem. Mais importante que o tamanho é o ganho de leitura: depois da conversão inicial, consultas seguintes deixam de reinterpretar toda a estrutura do Excel.

Conversor completo: Excel para Parquet

Crie o arquivo converter.py com o código abaixo. Ele lê todas as abas, ignora colunas cujo cabeçalho esteja vazio, normaliza nomes e grava um Parquet por aba.

from pathlib import Path
import re
import unicodedata
import pandas as pd

ENTRADA = Path("entrada")
DESTINO = Path("parquet")
DESTINO.mkdir(exist_ok=True)

def nome_seguro(texto):
    texto = unicodedata.normalize("NFKD", str(texto))
    texto = "".join(c for c in texto if not unicodedata.combining(c))
    texto = re.sub(r"[^a-zA-Z0-9]+", "_", texto).strip("_").lower()
    return texto or "coluna"

def colunas_unicas(colunas):
    usadas = {}
    resultado = []
    for coluna in colunas:
        base = nome_seguro(coluna)
        usadas[base] = usadas.get(base, 0) + 1
        resultado.append(base if usadas[base] == 1 else f"{base}_{usadas[base]}")
    return resultado

for arquivo in ENTRADA.glob("*.xlsx"):
    planilha = pd.ExcelFile(arquivo, engine="openpyxl")
    for aba in planilha.sheet_names:
        df = pd.read_excel(arquivo, sheet_name=aba, dtype=object, engine="openpyxl")

        # Descarta cabeçalhos vazios ou criados automaticamente pelo pandas.
        manter = [
            c for c in df.columns
            if str(c).strip() and not str(c).startswith("Unnamed:")
        ]
        df = df.loc[:, manter]

        # Remove linhas completamente vazias.
        df = df.dropna(how="all")
        df.columns = colunas_unicas(df.columns)

        saida = DESTINO / f"{nome_seguro(arquivo.stem)}__{nome_seguro(aba)}.parquet"
        df.to_parquet(saida, index=False, compression="zstd")
        print(f"OK: {arquivo.name} / {aba} -> {saida.name} ({len(df):,} linhas)")

Execute:

python converter.py

Atenção: bibliotecas tradicionais podem consumir bastante memória ao ler XLSX, pois o formato não é naturalmente adequado a processamento em blocos. Se o arquivo não couber na memória, salve-o como CSV por aba antes da conversão ou use o modo de leitura read_only do openpyxl em uma rotina de streaming. Depois que os dados estiverem em Parquet, o trabalho pesado fica muito mais leve.

Alternativa direta: importar CSV com DuckDB

Se puder exportar a origem como CSV, o caminho é mais simples. No console do DuckDB:

CREATE OR REPLACE TABLE base_nova AS
SELECT *
FROM read_csv('entrada/base_nova.csv',
              header = true,
              auto_detect = true,
              sample_size = -1,
              all_varchar = true);

all_varchar = true é uma escolha conservadora para dados administrativos: impede que códigos com zeros à esquerda sejam convertidos em números. Depois, transformamos apenas as colunas que realmente devem ser numéricas. sample_size = -1 solicita inspeção completa para inferência, o que é mais seguro, embora mais lento na primeira leitura.

Consultar Parquet sem importar para o banco

O DuckDB consegue consultar o arquivo diretamente:

SELECT COUNT(*)
FROM read_parquet('parquet/base_nova__plan1.parquet');

SELECT codigo, descricao, valor
FROM read_parquet('parquet/base_nova__plan1.parquet')
WHERE valor IS NOT NULL
LIMIT 20;

Se vários arquivos têm o mesmo esquema, use um curinga:

SELECT COUNT(*)
FROM read_parquet('parquet/base_*.parquet');

Para consultas recorrentes, crie uma VIEW:

CREATE OR REPLACE VIEW base_nova AS
SELECT * FROM read_parquet('parquet/base_nova__plan1.parquet');

Primeiro diagnóstico da base

Antes de comparar, investigue quantidade de linhas, chaves vazias e duplicadas:

SELECT COUNT(*) AS linhas FROM base_nova;

SELECT COUNT(*) AS codigos_vazios
FROM base_nova
WHERE codigo IS NULL OR trim(CAST(codigo AS VARCHAR)) = '';

SELECT codigo, COUNT(*) AS ocorrencias
FROM base_nova
GROUP BY codigo
HAVING COUNT(*) > 1
ORDER BY ocorrencias DESC
LIMIT 50;

Essa etapa evita o erro mais perigoso em comparações: imaginar que a chave é única quando não é. Se um código aparece três vezes em cada base, um JOIN simples pode produzir nove combinações. Isso não é defeito do banco; é o resultado matemático de uma relação muitos-para-muitos.

Padronizar códigos sem perder zeros importantes

Defina primeiro a regra de negócio. Em alguns cadastros, 00052 e 52 representam o mesmo código; em outros, o zero faz parte do identificador. Nunca normalize por hábito.

Quando zeros à esquerda não importam:

CASE
  WHEN regexp_replace(trim(CAST(codigo AS VARCHAR)), '^0+', '') = '' THEN '0'
  ELSE regexp_replace(trim(CAST(codigo AS VARCHAR)), '^0+', '')
END AS codigo_normalizado

Quando o código deve ter oito posições:

lpad(trim(CAST(codigo AS VARCHAR)), 8, '0') AS codigo_normalizado

Também é comum remover espaços invisíveis e padronizar letras:

upper(trim(CAST(codigo AS VARCHAR))) AS codigo_normalizado

Tratar valores monetários brasileiros

Valores como 1.234,56 não devem ser convertidos diretamente para DOUBLE. Primeiro remova o ponto de milhar, troque a vírgula decimal e use DECIMAL:

TRY_CAST(
  replace(
    replace(trim(CAST(valor AS VARCHAR)), '.', ''),
    ',', '.'
  ) AS DECIMAL(18,2)
) AS valor_normalizado

TRY_CAST devolve nulo quando o texto é inválido, em vez de derrubar toda a consulta. Para encontrar problemas:

SELECT valor
FROM base_nova
WHERE valor IS NOT NULL
  AND TRY_CAST(replace(replace(trim(CAST(valor AS VARCHAR)), '.', ''), ',', '.')
      AS DECIMAL(18,2)) IS NULL
LIMIT 100;

Para dinheiro, prefira DECIMAL(18,2) a números de ponto flutuante. Assim, uma diferença de um centavo permanece uma diferença de um centavo.

Criar visões limpas para as duas bases

CREATE OR REPLACE VIEW antiga_limpa AS
SELECT
  upper(trim(CAST(codigo AS VARCHAR))) AS codigo,
  descricao,
  TRY_CAST(replace(replace(trim(CAST(valor AS VARCHAR)), '.', ''), ',', '.')
      AS DECIMAL(18,2)) AS valor
FROM read_parquet('parquet/base_antiga__plan1.parquet')
WHERE codigo IS NOT NULL;

CREATE OR REPLACE VIEW nova_limpa AS
SELECT
  upper(trim(CAST(codigo AS VARCHAR))) AS codigo,
  descricao,
  TRY_CAST(replace(replace(trim(CAST(valor AS VARCHAR)), '.', ''), ',', '.')
      AS DECIMAL(18,2)) AS valor
FROM read_parquet('parquet/base_nova__plan1.parquet')
WHERE codigo IS NOT NULL;

Substitua os nomes das colunas pelos nomes reais produzidos pelo conversor. A vantagem da visão é centralizar a regra. Todas as comparações passam a usar a mesma limpeza.

O equivalente ao PROCV: LEFT JOIN

Para trazer o valor da base de referência para cada código da base principal:

SELECT
  a.codigo,
  a.descricao,
  a.valor AS valor_antigo,
  n.valor AS valor_novo
FROM antiga_limpa a
LEFT JOIN nova_limpa n
  ON a.codigo = n.codigo;

O LEFT JOIN mantém todas as linhas da tabela à esquerda, mesmo quando não há correspondência. É o comportamento mais próximo do PROCV usado para enriquecer uma tabela principal.

Classificar incluídos, removidos, alterados e iguais

Para uma comparação completa, use FULL OUTER JOIN:

CREATE OR REPLACE TABLE resultado_comparacao AS
SELECT
  coalesce(a.codigo, n.codigo) AS codigo,
  a.descricao AS descricao_antiga,
  n.descricao AS descricao_nova,
  a.valor AS valor_antigo,
  n.valor AS valor_novo,
  n.valor - a.valor AS diferenca,
  CASE
    WHEN a.codigo IS NULL THEN 'INCLUIDO'
    WHEN n.codigo IS NULL THEN 'REMOVIDO'
    WHEN a.valor IS DISTINCT FROM n.valor THEN 'VALOR ALTERADO'
    WHEN a.descricao IS DISTINCT FROM n.descricao THEN 'DESCRICAO ALTERADA'
    ELSE 'SEM ALTERACAO'
  END AS status
FROM antiga_limpa a
FULL OUTER JOIN nova_limpa n
  ON a.codigo = n.codigo;

IS DISTINCT FROM é melhor que <> quando há nulos, porque trata a ausência de valor de forma previsível.

Resolver duplicidade antes do JOIN

Se a regra diz que deve valer o registro mais recente por código:

CREATE OR REPLACE VIEW nova_unica AS
SELECT * EXCLUDE (rn)
FROM (
  SELECT *,
         row_number() OVER (
           PARTITION BY codigo
           ORDER BY data_vigencia DESC NULLS LAST
         ) AS rn
  FROM nova_limpa
)
WHERE rn = 1;

Se a regra é somar todos os valores do código:

SELECT codigo, SUM(valor) AS valor_total
FROM nova_limpa
GROUP BY codigo;

Se não existe regra segura para escolher ou agregar, não esconda a duplicidade. Exporte-a para análise. Remover duplicados cegamente pode apagar registros legítimos.

Exportar o resultado para CSV

COPY (
  SELECT *
  FROM resultado_comparacao
  WHERE status <> 'SEM ALTERACAO'
  ORDER BY status, codigo
) TO 'saida/resultado.csv'
(HEADER, DELIMITER ';', ENCODING 'UTF-8');

O ponto e vírgula costuma abrir melhor no Excel configurado em português brasileiro. CSV não possui cores, múltiplas abas nem tipos de célula sofisticados, mas suporta muito mais linhas como arquivo de dados. O limite aparece quando ele é aberto em uma aba do Excel, não no CSV em si.

Exportar para XLSX com Python

Para resultados abaixo do limite de linhas do Excel:

import duckdb
import pandas as pd

con = duckdb.connect("dados.duckdb", read_only=True)

consulta = """
SELECT *
FROM resultado_comparacao
WHERE status <> 'SEM ALTERACAO'
ORDER BY status, codigo
"""

df = con.execute(consulta).df()

with pd.ExcelWriter("saida/resultado.xlsx", engine="xlsxwriter") as writer:
    df.to_excel(writer, sheet_name="Comparacao", index=False)
    aba = writer.sheets["Comparacao"]
    aba.freeze_panes(1, 0)
    aba.autofilter(0, 0, len(df), len(df.columns) - 1)
    aba.set_column(0, len(df.columns) - 1, 18)

con.close()

Se o resultado puder superar 1.048.576 linhas, não tente forçá-lo em uma aba. Exporte CSV ou Parquet, filtre o resultado, ou divida em abas controladas:

LIMITE = 1_000_000
with pd.ExcelWriter("saida/resultado_dividido.xlsx", engine="xlsxwriter") as writer:
    for inicio in range(0, len(df), LIMITE):
        parte = df.iloc[inicio:inicio + LIMITE]
        parte.to_excel(writer, sheet_name=f"Parte_{inicio // LIMITE + 1}", index=False)

Essa estratégia ainda exige memória para o DataFrame. Para saídas realmente gigantes, prefira COPY do DuckDB para CSV ou Parquet.

Exportar um novo Parquet tratado

COPY (
  SELECT * FROM resultado_comparacao
) TO 'saida/resultado.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD);

O Parquet é a melhor saída para continuar analisando dados, alimentar Power BI, reutilizar no DuckDB ou arquivar uma versão tratada. O XLSX é a melhor saída quando uma pessoa precisa abrir, filtrar e apresentar uma parcela manejável do resultado.

Configurações de desempenho do DuckDB

O DuckDB normalmente escolhe bons padrões, mas estas configurações são úteis:

SET threads = 8;
SET memory_limit = '8GB';
SET temp_directory = 'C:/dados_gigantes/temp_duckdb';
SET preserve_insertion_order = false;
  • threads: ajuste ao número de núcleos disponíveis; mais nem sempre significa melhor
  • memory_limit: reserve espaço para o Windows e outros programas
  • temp_directory: escolha um SSD com espaço livre; consultas podem descarregar dados temporários no disco
  • preserve_insertion_order = false: pode reduzir consumo em operações grandes quando a ordem original não importa

Não use todo o espaço do SSD nem toda a memória. Em um computador com 16 GB, começar com limite de 8 a 10 GB é mais prudente. Meça o comportamento com seus arquivos.

Como conferir se o resultado está correto

Velocidade sem validação apenas produz erros rapidamente. Faça quatro conferências:

  1. Contagem: quantas linhas existem antes e depois da limpeza?
  2. Unicidade: a chave que deveria ser única realmente é?
  3. Reconciliação: incluídos, removidos e correspondentes explicam o universo esperado?
  4. Amostragem: confira manualmente códigos conhecidos nas duas origens
SELECT status, COUNT(*) AS quantidade
FROM resultado_comparacao
GROUP BY status
ORDER BY status;

SELECT
  COUNT(*) AS total,
  COUNT(DISTINCT codigo) AS codigos_distintos
FROM resultado_comparacao;

Guarde também um log com nome do arquivo, data, quantidade de linhas e regra aplicada. Em processos corporativos, rastreabilidade vale tanto quanto desempenho.

Erros comuns e como resolver

“python não é reconhecido”

Reinstale marcando Add Python to PATH ou use o iniciador py.

“No module named duckdb”

Ative o ambiente virtual e execute python -m pip install duckdb. Confirme que o terminal do VS Code usa o mesmo interpretador.

Códigos perderam zeros

A coluna foi inferida como numérica. Leia códigos como texto desde a origem, use all_varchar no CSV ou dtype=object no pandas.

Valores viraram nulos

Há símbolos, espaços, formatos mistos ou textos como “N/A”. Liste as linhas em que TRY_CAST falhou antes de decidir como tratá-las.

O JOIN multiplicou linhas

Existem chaves repetidas em um ou nos dois lados. Conte ocorrências e aplique uma regra explícita de seleção, agregação ou relacionamento composto.

O XLSX não abre ou perdeu linhas

O resultado excedeu o limite do Excel ou a geração foi interrompida. Use CSV/Parquet, filtre a saída ou divida em abas com margem abaixo do limite.

O computador ficou sem espaço

Arquivos temporários, Parquets e exportações coexistem durante o processamento. Direcione temp_directory para um disco adequado e mantenha as origens até validar a saída; só depois faça uma limpeza controlada.

DuckDB UI e VS Code: opções de interface

Quem prefere uma interface pode usar o DuckDB UI disponibilizado pelo próprio projeto ou uma ferramenta SQL compatível. Ainda assim, guarde consultas importantes em arquivos .sql. Botões ajudam a explorar; arquivos versionados ajudam a repetir e auditar.

No VS Code, crie comparar.sql, cole as consultas e mantenha converter.py na mesma pasta. A extensão Python facilita executar o script. Extensões SQL podem melhorar realce e navegação, mas não são requisito para o motor funcionar.

Quando usar DuckDB e quando escolher outra solução

DuckDB é excelente para análise local, pipelines em lote, consultas sobre Parquet, preparação de relatórios e comparação de arquivos grandes. Ele é especialmente atraente quando uma pessoa ou processo controla a escrita e os dados cabem no computador ou em armazenamento acessível.

Escolha PostgreSQL, SQL Server ou outro banco servidor quando vários usuários precisam escrever simultaneamente, há uma aplicação web transacional, permissões complexas, alta disponibilidade ou operação contínua em rede. Use Spark quando o conjunto realmente exige processamento distribuído em várias máquinas. Use Excel quando a escala cabe nele e a interatividade da grade é mais valiosa que a automação.

Fluxo final para repetir todos os meses

  1. Coloque os novos XLSX na pasta entrada
  2. Ative o ambiente virtual
  3. Execute python converter.py
  4. Abra dados.duckdb e execute o SQL de limpeza e comparação
  5. Confira contagens, duplicidades e amostras
  6. Exporte o resultado completo para Parquet e a parcela humana para XLSX
  7. Registre data, arquivos usados e quantidades
  8. Arquive as entradas; não as apague antes de validar e aprovar o resultado

Checklist antes de confiar na comparação

  • Os cabeçalhos vazios foram descartados?
  • Códigos foram tratados como texto?
  • A regra de zeros à esquerda foi confirmada?
  • Valores brasileiros foram convertidos para DECIMAL?
  • Chaves duplicadas foram investigadas?
  • O tipo de JOIN corresponde ao objetivo?
  • Nulos foram comparados corretamente?
  • A saída respeita o limite do Excel?
  • Contagens e amostras foram reconciliadas?
  • As consultas e arquivos originais foram preservados?

Guia operacional completo: o que baixar e onde clicar

Se você quer apenas executar o processo, siga esta ordem. Todos os endereços abaixo são oficiais:

Não baixe executáveis de sites de terceiros. O DuckDB CLI é um executável independente: depois de extrair duckdb.exe, ele já pode ser usado, sem servidor e sem instalador tradicional.

Qual comando vai em qual lugar?

Este detalhe evita muitos erros. Use a tabela como referência:

TipoOnde executarExemplo
Comando do WindowsPowerShell ou Prompt de Comandopython --version
Comando do DuckDBDepois de abrir duckdb.exeSELECT version();
Script SQLArquivo comparar.sqlCREATE TABLE...
Script PythonArquivo terminado em .pyimport duckdb

Instalação automatizada no Windows com winget

Em Windows 10 ou 11 com o Gerenciador de Pacotes do Windows disponível, abra o PowerShell e confira:

winget --version
winget search DuckDB
winget search Python.Python
winget search Microsoft.VisualStudioCode

Depois, instale os componentes. O identificador retornado pela pesquisa é a referência mais confiável; no catálogo atual, os comandos normalmente são:

winget install --id DuckDB.cli --exact
winget install --id Python.Python.3.14 --exact
winget install --id Microsoft.VisualStudioCode --exact

Se o identificador do Python tiver mudado, copie exatamente o ID mostrado por winget search Python.Python. Feche e abra o terminal após a instalação. Valide tudo:

duckdb --version
python --version
python -m pip --version
code --version

Criação completa do projeto pelo PowerShell

Abra o PowerShell e execute uma linha de cada vez:

New-Item -ItemType Directory -Force C:\dados_gigantes
Set-Location C:\dados_gigantes
New-Item -ItemType Directory -Force entrada, parquet, saida, scripts, sql, logs, temp_duckdb
python -m venv .venv
Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass
.\.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install duckdb pandas openpyxl pyarrow xlsxwriter
python -m pip freeze > requirements.txt

O arquivo requirements.txt registra as versões instaladas. Para recriar o ambiente em outro computador:

python -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt

Os mesmos passos no Prompt de Comando

mkdir C:\dados_gigantes
cd /d C:\dados_gigantes
mkdir entrada parquet saida scripts sql logs temp_duckdb
python -m venv .venv
.venv\Scripts\activate.bat
python -m pip install --upgrade pip
python -m pip install duckdb pandas openpyxl pyarrow xlsxwriter
python -m pip freeze > requirements.txt

Rota 1: ler XLSX diretamente no DuckDB

Versões atuais do DuckDB oferecem a extensão oficial excel, que lê e grava .xlsx. Ela não lê o formato antigo .xls. Abra o banco no PowerShell:

Set-Location C:\dados_gigantes
duckdb dados.duckdb

Agora, dentro do DuckDB, instale a extensão uma vez e carregue-a:

INSTALL excel;
LOAD excel;

SELECT *
FROM read_xlsx('entrada/base_antiga.xlsx')
LIMIT 10;

Para descobrir os nomes das planilhas existentes no arquivo:

SELECT *
FROM excel_sheets('entrada/base_antiga.xlsx');

Para transformar uma aba em tabela persistente:

CREATE OR REPLACE TABLE base_antiga AS
SELECT *
FROM read_xlsx(
  'entrada/base_antiga.xlsx',
  sheet = 'Plan1',
  header = true
);

A leitura direta é excelente para inspeção e arquivos bem estruturados. Quando códigos com zeros à esquerda dependem apenas da formatação visual do Excel, valide uma amostra antes de importar: o valor armazenado na célula pode já ser numérico. Para controlar tipos desde a origem, prefira o conversor Python deste guia e force as colunas de código como texto.

Rota 2: converter primeiro para Parquet

Para rotinas repetidas e arquivos grandes, a conversão inicial para Parquet costuma ser mais prática. Depois de colocar os XLSX em entrada e salvar o conversor deste artigo como scripts/converter.py, execute:

Set-Location C:\dados_gigantes
.\.venv\Scripts\Activate.ps1
python .\scripts\converter.py

Confira se os arquivos foram criados:

Get-ChildItem .\parquet\ -Filter *.parquet
Get-ChildItem .\parquet\ -Filter *.parquet | Select-Object Name, Length, LastWriteTime

Faça uma consulta sem importar o Parquet para o banco:

duckdb dados.duckdb -c "SELECT COUNT(*) AS linhas FROM read_parquet('parquet/*.parquet');"

Configuração recomendada do banco

Crie sql/00_configuracao.sql:

SET threads = 8;
SET memory_limit = '8GB';
SET temp_directory = 'C:/dados_gigantes/temp_duckdb';
SET preserve_insertion_order = false;

SELECT version() AS versao_duckdb;
SELECT current_setting('threads') AS threads;
SELECT current_setting('memory_limit') AS limite_memoria;
SELECT current_setting('temp_directory') AS pasta_temporaria;

Execute o arquivo inteiro pelo terminal:

duckdb dados.duckdb -init sql/00_configuracao.sql

Ajuste memória e threads ao computador. Em uma máquina com 16 GB de RAM, começar com 8 GB deixa espaço para o Windows, Excel e antivírus. Em máquinas com 32 GB, 16 a 20 GB podem ser um ponto de partida. Não trate esses números como regra universal: monitore o uso real.

Script SQL completo de comparação

Crie sql/10_comparar.sql. Troque os nomes das colunas codigo, descricao e valor pelos cabeçalhos reais:

CREATE OR REPLACE VIEW antiga_limpa AS
SELECT
  TRIM(CAST(codigo AS VARCHAR)) AS codigo,
  TRIM(CAST(descricao AS VARCHAR)) AS descricao_antiga,
  TRY_CAST(
    REPLACE(REPLACE(REPLACE(TRIM(CAST(valor AS VARCHAR)), 'R$', ''), '.', ''), ',', '.')
    AS DECIMAL(18,2)
  ) AS valor_antigo
FROM read_parquet('parquet/base_antiga__plan1.parquet')
WHERE codigo IS NOT NULL
  AND TRIM(CAST(codigo AS VARCHAR)) <> '';

CREATE OR REPLACE VIEW nova_limpa AS
SELECT
  TRIM(CAST(codigo AS VARCHAR)) AS codigo,
  TRIM(CAST(descricao AS VARCHAR)) AS descricao_nova,
  TRY_CAST(
    REPLACE(REPLACE(REPLACE(TRIM(CAST(valor AS VARCHAR)), 'R$', ''), '.', ''), ',', '.')
    AS DECIMAL(18,2)
  ) AS valor_novo
FROM read_parquet('parquet/base_nova__plan1.parquet')
WHERE codigo IS NOT NULL
  AND TRIM(CAST(codigo AS VARCHAR)) <> '';

CREATE OR REPLACE TABLE resultado_comparacao AS
SELECT
  COALESCE(a.codigo, n.codigo) AS codigo,
  a.descricao_antiga,
  n.descricao_nova,
  a.valor_antigo,
  n.valor_novo,
  n.valor_novo - a.valor_antigo AS diferenca_reais,
  CASE
    WHEN a.codigo IS NULL THEN 'INCLUIDO'
    WHEN n.codigo IS NULL THEN 'REMOVIDO'
    WHEN a.valor_antigo IS DISTINCT FROM n.valor_novo THEN 'VALOR ALTERADO'
    WHEN a.descricao_antiga IS DISTINCT FROM n.descricao_nova THEN 'DESCRICAO ALTERADA'
    ELSE 'IGUAL'
  END AS status
FROM antiga_limpa a
FULL OUTER JOIN nova_limpa n USING (codigo);

SELECT status, COUNT(*) AS quantidade
FROM resultado_comparacao
GROUP BY status
ORDER BY status;

Execute e grave um log de texto:

duckdb dados.duckdb -init sql/00_configuracao.sql -c ".read sql/10_comparar.sql" > logs/comparacao.txt

Teste obrigatório antes do JOIN

Se codigo deveria ser único, rode:

SELECT codigo, COUNT(*) AS ocorrencias
FROM antiga_limpa
GROUP BY codigo
HAVING COUNT(*) > 1
ORDER BY ocorrencias DESC, codigo
LIMIT 100;

SELECT codigo, COUNT(*) AS ocorrencias
FROM nova_limpa
GROUP BY codigo
HAVING COUNT(*) > 1
ORDER BY ocorrencias DESC, codigo
LIMIT 100;

Se houver duplicidade em ambos os lados, um código repetido três vezes na antiga e quatro vezes na nova pode gerar doze linhas. Não use DISTINCT apenas para esconder isso. Defina a regra: escolher a vigência mais recente, somar, preservar todos os registros ou usar uma chave composta.

Exemplo com vigência: escolher a linha mais recente

CREATE OR REPLACE VIEW nova_unica AS
SELECT * EXCLUDE (ordem)
FROM (
  SELECT
    *,
    ROW_NUMBER() OVER (
      PARTITION BY codigo
      ORDER BY TRY_CAST(data_vigencia AS DATE) DESC NULLS LAST
    ) AS ordem
  FROM nova_limpa
)
WHERE ordem = 1;

Antes de aplicar essa regra, confirme o significado da data e o critério de desempate. Se duas linhas tiverem a mesma vigência, inclua uma segunda coluna no ORDER BY.

Exportação completa: Parquet, CSV e XLSX

O Parquet preserva o resultado completo com boa eficiência:

COPY resultado_comparacao
TO 'saida/resultado_completo.parquet'
(FORMAT PARQUET, COMPRESSION ZSTD, ROW_GROUP_SIZE 250000);

Para CSV compatível com configurações brasileiras do Excel:

COPY resultado_comparacao
TO 'saida/resultado_completo.csv'
(FORMAT CSV, HEADER true, DELIMITER ';');

Para XLSX usando a extensão oficial:

INSTALL excel;
LOAD excel;

COPY (
  SELECT *
  FROM resultado_comparacao
  WHERE status <> 'IGUAL'
  ORDER BY status, codigo
) TO 'saida/resultado_alteracoes.xlsx'
(FORMAT XLSX, HEADER true, SHEET 'Alteracoes');

O Excel limita cada aba a 1.048.576 linhas. Se o resultado ultrapassar isso, mantenha Parquet/CSV como saída principal ou divida a exportação. Nunca descarte linhas silenciosamente para fazer o arquivo caber.

Automação em um clique com arquivo BAT

Crie executar_processamento.bat na raiz:

@echo off
setlocal
cd /d C:\dados_gigantes

call .venv\Scripts\activate.bat
if errorlevel 1 goto :erro

python scripts\converter.py
if errorlevel 1 goto :erro

duckdb dados.duckdb -init sql\00_configuracao.sql -c ".read sql/10_comparar.sql"
if errorlevel 1 goto :erro

echo Processamento concluido.
pause
exit /b 0

:erro
echo O processamento terminou com erro. Leia as mensagens acima.
pause
exit /b 1

Esse arquivo para imediatamente se a conversão ou o SQL falhar. Isso é melhor do que exibir “concluído” depois de uma etapa incompleta.

Comandos de diagnóstico e manutenção

-- Listar tabelas e visões
SHOW ALL TABLES;

-- Ver colunas e tipos
DESCRIBE resultado_comparacao;

-- Tamanho aproximado do banco
PRAGMA database_size;

-- Ver plano de execução
EXPLAIN ANALYZE
SELECT status, COUNT(*)
FROM resultado_comparacao
GROUP BY status;

-- Conferir metadados do Parquet
SELECT file_name, row_group_id, row_group_num_rows
FROM parquet_metadata('saida/resultado_completo.parquet');

-- Reorganizar e reduzir espaço reaproveitável do banco
CHECKPOINT;

Links oficiais organizados por etapa

Não existe apenas uma linha de resolução

DuckDB com Parquet é a rota principal deste guia porque combina velocidade, baixo consumo de recursos, SQL e instalação simples. Mas não é a única solução correta. A melhor escolha depende do volume, da frequência, do conhecimento da equipe e da necessidade de vários usuários trabalharem ao mesmo tempo.

RotaMelhor cenárioPonto forteLimite principal
Power QueryUsuário de Excel, processo visual e volume moderadoQuase sem códigoPode consumir muita memória e continuar preso aos limites da planilha
DuckDB + ParquetMilhões de linhas em um computadorAlta velocidade sem servidorNão é a melhor opção para muitos usuários escrevendo simultaneamente
Python + pandasAutomação flexível e regras personalizadasEcossistema enormeDataFrames grandes precisam caber na memória
Python + PolarsPipeline local muito grande e desempenho críticoExecução lazy, paralela e eficienteEcossistema menor e curva de aprendizado adicional
SQLiteBase local persistente, consultas simples e uso individualArquivo único e estabilidadeMenos otimizado para análise colunar pesada
PostgreSQLEquipe, sistema permanente, permissões e concorrênciaServidor completo e multiusuárioInstalação, administração e manutenção maiores

Alternativa 1: Power Query, sem programar

É a rota mais acessível para quem deseja permanecer no Excel. O Power Query importa, limpa, combina e atualiza dados por etapas reproduzíveis. No Excel, abra Dados > Obter Dados > De Arquivo > De Pasta de Trabalho, selecione a primeira planilha e clique em Transformar Dados. Repita com a segunda base.

  1. Defina a coluna de código como Texto, preservando zeros à esquerda
  2. Defina valores como Número decimal fixo ou aplique a localidade Português (Brasil)
  3. Remova colunas e linhas vazias antes da junção
  4. Use Página Inicial > Mesclar Consultas como Novas
  5. Selecione o código nas duas tabelas e escolha Externa completa para enxergar incluídos, removidos e correspondências
  6. Expanda as colunas de descrição e valor da segunda consulta
  7. Crie colunas condicionais para classificar o resultado
  8. Use Fechar e Carregar Para... e, se o resultado for grande, carregue apenas como conexão ou no Modelo de Dados

Exemplo de coluna personalizada em linguagem M:

if [valor_antigo] = null then "INCLUIDO"
else if [valor_novo] = null then "REMOVIDO"
else if [valor_antigo] <> [valor_novo] then "VALOR ALTERADO"
else "IGUAL"

Use Power Query quando a equipe precisa de uma interface visual e o processo cabe confortavelmente na máquina. Se a atualização demora demais, estoura memória ou o resultado ultrapassa o limite de linhas do Excel, migre o processamento para DuckDB, Polars ou um banco servidor. Documentação: Power Query e mesclar consultas.

Alternativa 2: Python com pandas

pandas é uma boa escolha quando o tratamento exige regras específicas, várias abas, nomes irregulares ou integração com outras rotinas Python. Instale:

python -m pip install pandas openpyxl pyarrow xlsxwriter

Crie comparar_pandas.py:

import pandas as pd

a = pd.read_excel("entrada/base_antiga.xlsx", dtype={"codigo": "string"})
n = pd.read_excel("entrada/base_nova.xlsx", dtype={"codigo": "string"})

for df in (a, n):
    df["codigo"] = df["codigo"].str.strip()
    df["valor"] = (df["valor"].astype("string")
        .str.replace("R$", "", regex=False)
        .str.replace(".", "", regex=False)
        .str.replace(",", ".", regex=False))
    df["valor"] = pd.to_numeric(df["valor"], errors="coerce")

if a["codigo"].duplicated().any() or n["codigo"].duplicated().any():
    raise ValueError("Há códigos duplicados; defina a regra antes de comparar")

r = a.merge(n, on="codigo", how="outer", suffixes=("_antigo", "_novo"), validate="one_to_one", indicator=True)
r["status"] = "IGUAL"
r.loc[r["_merge"] == "left_only", "status"] = "REMOVIDO"
r.loc[r["_merge"] == "right_only", "status"] = "INCLUIDO"
r.loc[(r["_merge"] == "both") & (r["valor_antigo"] != r["valor_novo"]), "status"] = "VALOR ALTERADO"

r.to_parquet("saida/resultado.parquet", index=False)
r.to_csv("saida/resultado.csv", sep=";", index=False, encoding="utf-8-sig")
r[r["status"] != "IGUAL"].to_excel("saida/alteracoes.xlsx", index=False)

Execute com python comparar_pandas.py. A opção validate="one_to_one" é uma proteção importante contra multiplicação acidental de linhas. Para arquivos que não cabem na memória, pandas puro deixa de ser a melhor rota; prefira DuckDB ou Polars lazy. Consulte read_excel, merge e to_excel.

Alternativa 3: Polars para alto desempenho em Python

Polars é interessante quando se deseja continuar em Python, mas com processamento colunar, paralelo e lazy. O ideal é converter o XLSX uma vez e trabalhar em Parquet. Instale:

python -m pip install polars fastexcel xlsxwriter

Exemplo de comparação lazy:

import polars as pl

a = (pl.scan_parquet("parquet/base_antiga.parquet")
    .select(
        pl.col("codigo").cast(pl.String).str.strip_chars(),
        pl.col("descricao").alias("descricao_antiga"),
        pl.col("valor").cast(pl.Decimal(18, 2)).alias("valor_antigo")
    ))

n = (pl.scan_parquet("parquet/base_nova.parquet")
    .select(
        pl.col("codigo").cast(pl.String).str.strip_chars(),
        pl.col("descricao").alias("descricao_nova"),
        pl.col("valor").cast(pl.Decimal(18, 2)).alias("valor_novo")
    ))

r = (a.join(n, on="codigo", how="full", coalesce=True)
    .with_columns(
        pl.when(pl.col("valor_antigo").is_null()).then(pl.lit("INCLUIDO"))
        .when(pl.col("valor_novo").is_null()).then(pl.lit("REMOVIDO"))
        .when(pl.col("valor_antigo") != pl.col("valor_novo")).then(pl.lit("VALOR ALTERADO"))
        .otherwise(pl.lit("IGUAL")).alias("status")
    ))

r.sink_parquet("saida/resultado_polars.parquet")

scan_parquet permite ao otimizador evitar colunas e linhas desnecessárias antes de materializar o resultado. Valide duplicidades separadamente antes do join. Documentação: instalação, scan_parquet e exportação Excel.

Alternativa 4: SQLite para um banco local persistente

SQLite também guarda tudo em um único arquivo e é excelente para aplicativos locais, cadastros e consultas transacionais simples. Baixe o pacote de ferramentas em sqlite.org/download.html, extraia sqlite3.exe e abra:

sqlite3 dados.sqlite

No console, importe CSVs e crie índices:

.mode csv
.separator ;
.import --skip 1 entrada/base_antiga.csv base_antiga
.import --skip 1 entrada/base_nova.csv base_nova

CREATE INDEX idx_antiga_codigo ON base_antiga(codigo);
CREATE INDEX idx_nova_codigo ON base_nova(codigo);

.headers on
.once saida/resultado_sqlite.csv
SELECT a.codigo, a.valor AS valor_antigo, n.valor AS valor_novo
FROM base_antiga a
LEFT JOIN base_nova n ON n.codigo = a.codigo;

SQLite é confiável e simples, porém DuckDB costuma ser mais adequado para varreduras analíticas, Parquet e agregações sobre milhões de linhas. Escolha SQLite quando o banco local persistente e o comportamento transacional forem mais importantes que análise colunar. Consulte a documentação oficial do CLI.

Alternativa 5: PostgreSQL para equipe e produção

Quando várias pessoas ou sistemas precisam consultar e alterar dados ao mesmo tempo, a solução deixa de ser apenas um arquivo local. PostgreSQL oferece usuários, permissões, transações, índices, backups e acesso em rede. Baixe pelo instalador oficial para Windows, defina uma senha administrativa e instale também o pgAdmin.

Crie o banco e as tabelas pelo psql:

createdb -U postgres comparacao
psql -U postgres -d comparacao
CREATE TABLE base_antiga (
  codigo text,
  descricao text,
  valor numeric(18,2)
);

CREATE TABLE base_nova (LIKE base_antiga INCLUDING ALL);

\copy base_antiga FROM 'C:/dados_gigantes/entrada/base_antiga.csv' WITH (FORMAT csv, HEADER true, DELIMITER ';', ENCODING 'UTF8');
\copy base_nova FROM 'C:/dados_gigantes/entrada/base_nova.csv' WITH (FORMAT csv, HEADER true, DELIMITER ';', ENCODING 'UTF8');

CREATE INDEX ON base_antiga(codigo);
CREATE INDEX ON base_nova(codigo);
ANALYZE base_antiga;
ANALYZE base_nova;

A comparação usa o mesmo conceito de FULL OUTER JOIN:

CREATE TABLE resultado AS
SELECT
  COALESCE(a.codigo, n.codigo) AS codigo,
  a.valor AS valor_antigo,
  n.valor AS valor_novo,
  CASE
    WHEN a.codigo IS NULL THEN 'INCLUIDO'
    WHEN n.codigo IS NULL THEN 'REMOVIDO'
    WHEN a.valor IS DISTINCT FROM n.valor THEN 'VALOR ALTERADO'
    ELSE 'IGUAL'
  END AS status
FROM base_antiga a
FULL OUTER JOIN base_nova n USING (codigo);

PostgreSQL é mais trabalhoso de manter: exige política de backup, atualizações, controle de acesso e monitoramento. Ele se paga quando há concorrência, integração com sistemas ou necessidade de uma fonte central oficial. Consulte COPY, psql e backup.

Qual caminho escolher na prática?

  • Até algumas centenas de milhares de linhas e equipe sem programação: comece com Power Query
  • Milhões de linhas, uso local e consultas analíticas: DuckDB + Parquet é a recomendação principal
  • Regras muito personalizadas e volume que cabe na memória: pandas
  • Python com volume maior e desempenho crítico: Polars
  • Aplicativo local com banco transacional simples: SQLite
  • Vários usuários, permissões e operação permanente: PostgreSQL

Também é possível combinar rotas. Um desenho forte é usar Power Query apenas para consumo no Excel, DuckDB ou Polars para preparar dados, Parquet como camada intermediária e PostgreSQL apenas quando os resultados precisam ser compartilhados como serviço. Alternativas não significam refazer tudo: significam escolher o motor certo para cada etapa.

Conclusão: o Excel não precisa deixar de existir

A melhor solução para planilhas gigantes não é declarar guerra ao Excel. É colocá-lo na posição em que funciona melhor. O Excel recebe arquivos, permite conferências visuais e entrega relatórios. DuckDB e Parquet assumem leitura, junção, agregação e comparação em grande escala.

Essa separação muda o trabalho: fórmulas copiadas por um milhão de linhas viram uma consulta SQL; arquivos pesados viram Parquets reaproveitáveis; o PROCV vira um JOIN auditável; e o resultado pode ser exportado no formato certo para cada público. Mais importante, o processo deixa de depender de cliques manuais difíceis de repetir.

Comece com duas bases e três colunas: código, descrição e valor. Valide as regras, meça a diferença e só então amplie. Em dados grandes, uma solução confiável não é a que termina primeiro. É a que termina rápido, explica o que fez e permite chegar ao mesmo resultado novamente.

Documentação oficial para continuar

#duckdb #excel #planilhas #parquet #big data #sql #tutorial #Power Query #pandas #Polars #SQLite #PostgreSQL