Quando lidamos com grandes volumes de dados em ambientes de Lakehouse, a eficiência na escrita e leitura faz toda a diferença. Usar o Delta Lake em conjunto com o Spark e armazenamentos como o MinIO abre possibilidades poderosas, mas também traz desafios: fragmentação de arquivos, lentidão em consultas e joins custosos. Neste artigo, compartilho estratégias práticas para lidar com esses problemas no dia a dia.
1. Otimizando escrita no Delta Lake com OPTIMIZE e Z-ORDER
Ao longo do tempo, tabelas Delta tendem a acumular muitos arquivos pequenos, o que degrada a performance das consultas. Para mitigar isso, o Delta Lake oferece o comando OPTIMIZE, que faz a compactação de arquivos em unidades maiores e mais eficientes para leitura.
OPTIMIZE:
Consolida vários arquivos pequenos em arquivos de tamanho ideal (normalmente entre 256MB e 1GB). Isso reduz o overhead de leitura e melhora a performance de scans completos.
Z-ORDER:
Quando combinado ao optimize, o Z-ORDER organiza fisicamente os dados em disco de acordo com colunas de alto filtro nas consultas. Exemplo típico:
*OPTIMIZE **ecommerce.transacoes **ZORDER BY *(cliente_id, data_compra);
Isso melhora bastante consultas seletivas, já que os dados relacionados ficam próximos fisicamente, reduzindo I/O desnecessário.
Boas práticas:
Usar Z-ORDER em colunas que mais aparecem nos filtros (WHERE, JOIN, GROUP BY).
Automatizar a execução de OPTIMIZE em janelas de baixa carga, já que o processo consome recursos.
- Reduzindo o problema de arquivos pequenos no MinIO
Ao escrever dados no MinIO (ou S3), o Spark pode gerar muitos arquivos pequenos devido ao paralelismo natural da execução. Isso não apenas ocupa espaço, mas também prejudica a performance de leitura quando usamo o Delta Lake.
Estratégias para lidar com o problema:
Reparticionamento antes da escrita: Ajuste o número de partições antes do write para controlar quantos arquivos serão gerados:
df.repartition(50).write.format(“delta”).mode(“append”).save(caminho)
Aqui, 50 partições significa que a saída terá ~50 arquivos.
Coalesce para dados menores: Quando o volume for reduzido após um filtro, use coalesce() para gerar menos arquivos.
df.coalesce(1).write.format(“delta”).mode(“overwrite”).save(caminho)
Auto Optimize e Auto Compact (quando disponíveis): No Databricks, recursos automáticos podem gerenciar esse problema, mas em ambientes com MinIO puro, vale mais a pena automatizar um job de OPTIMIZE periódico.
Compactação pós-escrita:
Criar um job dedicado apenas para consolidar os arquivos pequenos, usando OPTIMIZE ou mesmo um processo customizado de merge.
3. Particionamento e paralelismo no Spark para joins grandes
Outra dor comum em grandes volumes são os joins custosos, que muitas vezes resultam em shuffles pesados. Existem boas práticas para aliviar esse cenário:
Particionamento estratégico:
Particionar tabelas do Delta Lake por colunas usadas com frequência em filtros e joins. Exemplo:
df.write.format(“delta”).partitionBy(“ano”, “mes”).save(caminho)
Isso ajuda o Spark a ler apenas os arquivos relevantes.
Broadcast join: Se uma das tabelas for pequena o suficiente, é muito mais eficiente transmitir a tabela inteira para os executores:
df_grande.join(broadcast(df_pequeno), “id”)
Configuração de paralelismo:
Ajustar o número de partições para o tamanho real do cluster:
spark.config(“spark.sql.shuffle.partitions”, 400)
Valores muito baixos causam gargalo.
Valores muito altos geram overhead de arquivos pequenos. A ideia é sempre calibrar com base no cluster e no volume processado.
Skew join handling: Em dados desbalanceados (ex.: muitos registros para um mesmo valor de chave), é comum que alguns executores fiquem sobrecarregados. O Spark possui configurações como:
spark.conf.set(“spark.sql.adaptive.skewJoin.enabled”, “true”)
Isso divide dinamicamente partições muito grandes, melhorando a distribuição de carga.
Conclusão
O trio Delta Lake + Spark + MinIO é extremamente poderoso, mas para alcançar alta performance é essencial cuidar da forma como os dados são gravados e lidos.
Use OPTIMIZE e Z-ORDER para melhorar consultas seletivas.
Lute contra arquivos pequenos ajustando particionamento e aplicando compactações periódicas.
Planeje particionamento e paralelismo para reduzir o custo de joins e operações de shuffle.
Com essas práticas aplicadas de forma contínua, a diferença de performance é notável: menos tempo de processamento, custos menores e consultas mais ágeis para os consumidores de dados.
Este artigo é um trecho adaptado do livro
Guia prático para construir um Data Lakehouse open source usando Minio , Delta Lake e Spark — Do Conceito à Prática com Ferramentas Open Source
Disponível em breve na Amazon.
