No artigo anterior, vimos que existe uma relação fundamental no Apache Spark:
Dados
↓
Partitions
↓
Tasks
↓
Parallelism
↓
Performance
Também vimos que nem sempre os dados permanecem nas mesmas partições durante todo o processamento.
Operações como:
df.repartition(...)
df.groupBy(...)
df.join(...)
podem exigir uma redistribuição dos dados.
E é nesse momento que aparece um dos conceitos mais importantes para entender performance no Spark:
Shuffle
Quando comecei a entender o que realmente acontecia durante um shuffle, vários problemas de performance que antes pareciam aleatórios começaram a fazer sentido.
Porque shuffle não significa apenas:
"Spark reorganizou algumas linhas."
Em um ambiente distribuído, reorganizar dados pode significar:
serializar
↓
escrever
↓
transferir pela rede
↓
ler
↓
desserializar
↓
agrupar
↓
continuar processando
E tudo isso tem custo.
1. O que é Shuffle?
Vamos começar com um exemplo simples.
Imagine que temos dados distribuídos entre três partições:
Partition 1
customer=A
customer=B
customer=C
Partition 2
customer=A
customer=D
customer=B
Partition 3
customer=C
customer=A
customer=D
Agora queremos contar quantos registros existem para cada cliente:
resultado = (
df
.groupBy("customer")
.count()
)
Para calcular:
A → 3
B → 2
C → 2
D → 2
Spark precisa reunir registros relacionados à mesma chave.
Mas os registros de customer=A, por exemplo, estão espalhados entre diferentes partições.
Temos:
P1 → A
P2 → A
P3 → A
Em algum momento esses dados precisam ser redistribuídos.
Conceitualmente:
ANTES
Partition 1 Partition 2 Partition 3
A A C
B D A
C B D
↓
SHUFFLE
↓
DEPOIS
Partition A Partition B Partition C
A B C
A B C
A D D
A distribuição exata depende da estratégia e do particionamento utilizados, mas a ideia central é esta:
Shuffle redistribui dados entre partições para permitir que uma etapa posterior da computação seja executada.
2. Por que Shuffle é caro?
Se todos os dados estivessem dentro de um único processo, reorganizar registros seria relativamente simples.
Mas Spark é distribuído.
Imagine:
Machine A
Executor 1
Machine B
Executor 2
Machine C
Executor 3
Agora precisamos mover dados de uma máquina para outra:
Executor 1 ──────────► Executor 2
│ ▲
│ │
▼ │
Executor 3 ─────────────────┘
Isso pode envolver:
- CPU;
- serialização;
- memória;
- disco;
- rede;
- desserialização;
- gerenciamento de buffers;
- sincronização entre etapas.
Portanto, shuffle pode transformar uma operação aparentemente simples em uma operação distribuída relativamente cara.
3. Narrow vs. Wide Dependencies
Na Parte 3 vimos rapidamente a diferença entre operações narrow e wide.
Agora ela se torna essencial.
Narrow dependency
Imagine:
df.filter("valor > 100")
Cada partição pode executar o filtro sobre seus próprios registros.
Partition 1 ───► Filter ───► Partition 1'
Partition 2 ───► Filter ───► Partition 2'
Partition 3 ───► Filter ───► Partition 3'
Não precisamos necessariamente enviar registros de uma partição para outra.
Outro exemplo:
rdd.map(lambda x: x * 2)
Conceitualmente:
P1 ───► P1'
P2 ───► P2'
P3 ───► P3'
Isso é muito diferente de uma operação que precisa reorganizar os dados globalmente.
4. Wide dependency
Agora considere:
df.groupBy("customer_id").count()
Os registros do mesmo cliente podem estar em qualquer partição.
Temos algo como:
P1 ──┐ ┌──► P1'
├───────┤
P2 ──┼───────┼──► P2'
├───────┤
P3 ──┘ └──► P3'
Uma partição de saída pode depender de dados provenientes de várias partições anteriores.
Isso caracteriza uma dependência ampla — wide dependency.
E frequentemente significa:
Wide Dependency
↓
Shuffle
↓
Stage Boundary
Esse relacionamento é uma das chaves para entender o DAG de uma aplicação Spark.
5. Shuffle cria fronteiras entre Stages
Na Parte 2 vimos:
Action
↓
Job
↓
Stages
↓
Tasks
Agora conseguimos entender melhor como Spark pode separar determinados Stages.
Considere:
resultado = (
df
.filter("status = 'ACTIVE'")
.groupBy("country")
.count()
)
resultado.show()
Podemos imaginar:
STAGE 1
Read
↓
Filter
↓
Partial Processing
↓
========= SHUFFLE =========
↓
STAGE 2
Aggregation
↓
Result
O shuffle cria uma dependência entre conjuntos de Tasks.
As Tasks da etapa posterior precisam dos dados produzidos pela etapa anterior.
Isso forma uma fronteira natural na execução.
6. Shuffle Write e Shuffle Read
Esse processo pode ser dividido conceitualmente em duas partes:
Stage anterior
│
▼
SHUFFLE WRITE
│
▼
Network
│
▼
SHUFFLE READ
│
▼
Próximo Stage
Shuffle Write
As Tasks do Stage anterior produzem dados que serão consumidos posteriormente.
Esses dados precisam ser organizados para as partições de destino.
Podemos imaginar:
Task 1
│
├── dados para Partition A
├── dados para Partition B
└── dados para Partition C
Task 2
│
├── dados para Partition A
├── dados para Partition B
└── dados para Partition C
Esses resultados formam a saída do shuffle.
Shuffle Read
No Stage seguinte, as Tasks precisam buscar os blocos correspondentes à sua partição.
Por exemplo:
Task A
Task 1 output ──────┐
Task 2 output ──────┼──► Partition A
Task 3 output ──────┘
Agora começamos a entender métricas encontradas no Spark UI:
Shuffle Read
Shuffle Write
Elas não são apenas números abstratos.
Representam movimentação real de dados durante a computação distribuída.
7. Um exemplo com groupBy
Considere:
resultado = (
df
.groupBy("estado")
.count()
)
Antes da agregação:
Executor 1
SP
RJ
SP
MG
Executor 2
MG
SP
BA
RJ
Executor 3
SP
BA
SP
MG
Para calcular os totais por estado, os registros precisam ser reorganizados de acordo com a chave.
Algo conceitualmente semelhante a:
SHUFFLE
Executor 1 ─────────┐
Executor 2 ─────────┼──► Partition SP
Executor 3 ─────────┘
Executor 1 ─────────┐
Executor 2 ─────────┼──► Partition RJ
Executor 3 ─────────┘
Executor 1 ─────────┐
Executor 2 ─────────┼──► Partition MG
Executor 3 ─────────┘
Depois disso, cada partição pode executar sua parte da agregação.
8. groupByKey() vs. reduceByKey()
No mundo dos RDDs, um exemplo clássico para entender shuffle é comparar:
rdd.groupByKey()
com:
rdd.reduceByKey(...)
Imagine:
("spark", 1)
("spark", 1)
("spark", 1)
("python", 1)
Com groupByKey(), os valores associados às chaves precisam ser agrupados para então serem processados.
Conceitualmente:
spark → [1, 1, 1]
python → [1]
Agora considere:
rdd.reduceByKey(lambda a, b: a + b)
Spark pode realizar agregações locais antes de transferir os dados entre partições.
Imagine:
PARTITION 1
spark → 1
spark → 1
python → 1
Antes de enviar:
spark → 2
python → 1
Outra partição:
PARTITION 2
spark → 1
spark → 1
pode produzir:
spark → 2
Então, em vez de mover todos os registros individualmente, podemos movimentar resultados parcialmente agregados.
Partition 1 → spark:2
Partition 2 → spark:2
↓
spark:4
Essa é uma otimização extremamente importante.
A lição é:
Quanto menos dados precisarmos movimentar, melhor.
9. Map-side aggregation
O conceito anterior é frequentemente associado à ideia de realizar parte da agregação antes do shuffle.
Visualmente:
SEM AGREGAÇÃO LOCAL
1
1
1
1
1
1
1
1
↓
NETWORK
↓
sum()
versus:
COM AGREGAÇÃO LOCAL
Partition A
1 + 1 + 1 + 1
↓
4
Partition B
1 + 1 + 1 + 1
↓
4
4 ───┐
├──► NETWORK ───► 8
4 ───┘
Mover:
4
4
é muito mais eficiente do que mover todos os registros originais.
Essa ideia aparece de diferentes formas nas otimizações do Spark.
10. Joins também podem gerar Shuffle
Agora chegamos a um caso extremamente comum em Engenharia de Dados.
Imagine:
pedidos.join(
clientes,
"customer_id"
)
Temos:
PEDIDOS
customer_id=1
customer_id=5
customer_id=3
customer_id=1
e:
CLIENTES
customer_id=1
customer_id=2
customer_id=3
customer_id=5
Para fazer o join, registros com a mesma chave precisam se encontrar.
Conceitualmente:
Pedidos ──────┐
│
▼
SHUFFLE
│
▼
Clientes ─────┘
customer_id=1 ──► mesma partition
customer_id=2 ──► mesma partition
customer_id=3 ──► mesma partition
...
Dependendo da estratégia de join, isso pode significar mover grandes volumes de dados pela rede.
E aqui surge uma pergunta importante:
E se uma das tabelas for muito pequena?
Talvez não faça sentido redistribuir os dois lados.
É aí que entra uma técnica que veremos em detalhes mais adiante:
Broadcast Join
Em vez de movimentar duas tabelas grandes:
Large Table ──► Shuffle
Large Table ──► Shuffle
podemos, em determinados cenários, distribuir uma tabela pequena para os Executors:
Small Table
/ | \
▼ ▼ ▼
Executor Executor Executor
Large Dataset
Assim podemos evitar um shuffle grande de um dos lados.
Essa é uma das otimizações mais importantes em joins Spark.
11. Shuffle não usa apenas rede
É comum resumir:
"Shuffle é caro porque usa rede."
Isso é verdade, mas incompleto.
Shuffle pode envolver:
SHUFFLE
│
┌──────────────┼──────────────┐
▼ ▼ ▼
CPU Memory Disk
│ │ │
serialization buffers spill
│ │ │
└──────────────┼──────────────┘
▼
Network
Ou seja, um shuffle pesado pode pressionar vários recursos simultaneamente.
12. Quando memória não é suficiente: Spill
Imagine que uma Task precisa organizar uma quantidade significativa de dados durante uma agregação ou ordenação.
Idealmente, parte desse trabalho acontece em memória.
Mas a memória é finita.
Quando Spark não consegue manter todo o estado necessário em memória, pode precisar utilizar disco.
Esse comportamento é conhecido como:
Spill
Conceitualmente:
Dados
↓
Memory
↓
Memory insuficiente
↓
Disk Spill
Depois, esses dados podem precisar ser lidos novamente.
Temos:
Memory
↓
Disk Write
↓
Disk Read
↓
Continue Processing
Agora o custo aumentou.
Além de CPU e rede, temos I/O em disco.
13. Memory Spill vs. Disk Spill
Ao analisar métricas de execução, podemos encontrar indicadores relacionados a spill.
A ideia principal é perceber que:
Uma Task precisou usar disco porque o processamento intermediário ultrapassou o que podia ser tratado eficientemente em memória.
Isso pode acontecer em operações como:
- agregações;
- joins;
- sorts;
- shuffles;
- processamento de partições muito grandes.
Se encontramos grandes volumes de spill, precisamos investigar.
Não significa automaticamente:
"adicione mais memória."
Pode significar:
- partições grandes demais;
- skew;
- estratégia de join inadequada;
- volume excessivo de shuffle;
- configuração inadequada;
- algoritmo inadequado.
Performance tuning exige descobrir a causa.
14. Shuffle + Data Skew = combinação perigosa
Na Parte 3 vimos Data Skew.
Agora podemos entender por que ele é especialmente problemático durante shuffle.
Imagine:
customer_id
A → 100.000.000 registros
B → 500.000
C → 400.000
D → 300.000
Depois do shuffle por customer_id:
Partition A █████████████████████████████████████████
Partition B █
Partition C █
Partition D █
Agora temos Tasks como:
Task A → 25 minutos
Task B → 20 segundos
Task C → 18 segundos
Task D → 15 segundos
Depois de alguns segundos:
Executor 1 → █████████████████ trabalhando
Executor 2 → idle
Executor 3 → idle
Executor 4 → idle
O cluster possui recursos.
Mas uma única partição concentra grande parte do trabalho.
Esse é um dos padrões mais importantes para reconhecer no Spark UI.
15. Stragglers
Tasks que demoram muito mais que as demais são frequentemente chamadas de stragglers.
Imagine:
Stage 17
Task 001 → 21 s
Task 002 → 19 s
Task 003 → 22 s
Task 004 → 20 s
Task 005 → 18 s
...
Task 199 → 20 s
Task 200 → 14 min
O Stage precisa esperar:
Task 200
Então:
199 Tasks concluídas
+
1 Task extremamente lenta
=
Stage ainda executando
O problema pode ser skew.
Mas também pode ser:
- hardware;
- I/O;
- garbage collection;
- spill;
- rede;
- tamanho da partição;
- executor problemático.
Por isso precisamos analisar métricas antes de concluir.
16. repartition() é um Shuffle explícito
Agora podemos revisitar a Parte 3.
Quando executamos:
df.repartition(200)
Spark precisa redistribuir os registros entre novas partições.
Conceitualmente:
ANTES
P1 ─────┐
P2 ─────┤
P3 ─────┼────► SHUFFLE
P4 ─────┤
P5 ─────┘
↓
DEPOIS
P1'
P2'
P3'
...
P200'
Isso explica por que adicionar repartition() arbitrariamente pode prejudicar performance.
Talvez tenhamos:
Read
↓
repartition()
↓
SHUFFLE
↓
groupBy()
↓
SHUFFLE
↓
join()
↓
SHUFFLE
Agora imagine isso sobre vários terabytes.
Estamos movimentando uma quantidade enorme de dados entre máquinas.
17. Shuffle e serialização
Dados não simplesmente "viajam" pela rede como objetos Python ou JVM abstratos.
Eles precisam ser representados de uma forma adequada para transferência e armazenamento.
Simplificando:
Object
↓
Serialization
↓
Bytes
↓
Network / Disk
↓
Deserialization
↓
Object
Isso também custa CPU.
Em workloads grandes, detalhes aparentemente pequenos começam a se multiplicar por:
milhões
bilhões
trilhões
de registros.
É por isso que otimizações distribuídas frequentemente tratam não apenas de algoritmos, mas também de:
- representação;
- serialização;
- compressão;
- layout;
- movimentação de dados.
18. Shuffle e compressão
Existe um trade-off interessante.
Podemos comprimir dados antes de movimentá-los.
Conceitualmente:
100 GB
↓
Compression
↓
30 GB
↓
Network
Reduzimos o volume transferido.
Mas agora precisamos gastar CPU:
CPU → compress
Network → transfer
CPU → decompress
Temos um trade-off:
CPU
↕
Network
Em sistemas distribuídos, otimização frequentemente significa escolher onde queremos pagar o custo.
19. Como identificar Shuffle no Spark UI
Agora o Spark UI começa a ficar ainda mais interessante.
Podemos observar métricas como:
Shuffle Read
Shuffle Write
Input
Output
Task Duration
GC Time
Spill
Imagine um Stage:
Input: 200 GB
Shuffle Write: 180 GB
E o próximo:
Shuffle Read: 180 GB
Output: 20 GB
Isso nos conta uma história.
Talvez tenhamos movimentado:
180 GB
para produzir:
20 GB
Então surge uma pergunta:
Poderíamos reduzir os dados antes do shuffle?
Talvez um filtro pudesse acontecer antes.
Talvez uma projeção pudesse eliminar colunas desnecessárias.
Talvez a estratégia de join pudesse mudar.
Esse é o tipo de pergunta que transforma métricas em otimização.
20. Filter Early
Considere:
resultado = (
df
.groupBy("customer_id")
.count()
)
sobre:
1 TB
Agora imagine que só precisamos de registros:
status = 'ACTIVE'
Se filtrarmos antes:
resultado = (
df
.filter("status = 'ACTIVE'")
.groupBy("customer_id")
.count()
)
e apenas 20% dos registros forem ativos, conceitualmente podemos passar de:
1 TB
↓
SHUFFLE
para algo mais próximo de:
1 TB
↓
FILTER
↓
200 GB
↓
SHUFFLE
Reduzimos drasticamente o volume que precisa atravessar a operação cara.
Essa é uma ideia simples e poderosa:
Reduza os dados antes de movimentá-los, quando semanticamente possível.
21. Project Early
O mesmo raciocínio vale para colunas.
Imagine um dataset com:
100 colunas
mas nosso processamento precisa apenas de:
customer_id
valor
estado
Podemos selecionar cedo:
df = df.select(
"customer_id",
"valor",
"estado"
)
Conceitualmente:
100 columns
↓
SELECT
↓
3 columns
↓
SHUFFLE
Dependendo do plano de execução e do formato da fonte, isso também pode reduzir bastante a quantidade de dados manipulada.
A ideia continua:
Não mova dados que você não precisa.
22. Aggregate Early
Quando possível, também podemos reduzir cardinalidade antes de determinadas movimentações.
Imagine:
1 bilhão de registros
que podem ser parcialmente agregados localmente antes de uma redistribuição.
Se conseguirmos transformar:
1.000.000.000 rows
em:
10.000.000 partial results
antes da transferência, temos uma diferença enorme.
Essa é a lógica por trás de várias estratégias de agregação distribuída.
23. O melhor Shuffle é o Shuffle que não precisamos fazer
Essa frase resume boa parte do tuning de sistemas distribuídos:
Movimentação de dados é cara.
Isso não significa que devemos eliminar todos os shuffles.
Muitos são necessários.
Um:
groupBy()
pode exigir redistribuição.
Um:
join()
pode exigir redistribuição.
Um:
repartition()
existe justamente para redistribuir.
A pergunta correta não é:
"Como elimino todos os shuffles?"
É:
"Esse shuffle é necessário e, se for, estou movimentando apenas os dados necessários?"
24. Um exemplo de pipeline ruim
Imagine:
df = spark.read.parquet("/data/events")
df = df.repartition(5000)
df = df.select("*")
resultado = (
df
.groupBy("customer_id")
.count()
)
Podemos ter:
READ
↓
REPARTITION
↓
SHUFFLE
↓
GROUP BY
↓
SHUFFLE
↓
RESULT
Agora suponha que só precisamos de eventos de compra.
Podemos reconsiderar:
df = (
spark.read.parquet("/data/events")
.filter("event_type = 'purchase'")
.select("customer_id")
)
resultado = (
df
.groupBy("customer_id")
.count()
)
Conceitualmente:
READ
↓
FILTER
↓
PROJECT
↓
menos dados
↓
SHUFFLE necessário
↓
AGGREGATION
Não estamos "fazendo Spark ficar rápido".
Estamos simplesmente reduzindo trabalho desnecessário.
Essa distinção é importante.
25. Mais memória nem sempre resolve Shuffle
Um padrão comum de troubleshooting é:
Job lento
↓
aumentar memória
Depois:
continua lento
↓
aumentar memória novamente
Mas talvez o problema real seja:
2 TB de Shuffle
Adicionar memória pode ajudar em alguns aspectos.
Mas não elimina necessariamente:
- movimentação de rede;
- serialização;
- skew;
- quantidade de dados;
- algoritmo inadequado.
Talvez o problema precise ser atacado no plano da computação.
26. Mais Executors também não resolvem tudo
Outro pensamento comum:
"Vou dobrar o cluster."
Podemos passar de:
10 Executors
para:
20 Executors
Mas se o problema for uma chave extremamente skewed:
customer_id = 123
80% dos registros
podemos continuar tendo:
1 Task gigantesca
e vários Executors ociosos.
Escalar infraestrutura sem entender o gargalo pode simplesmente aumentar custo.
27. Performance e custo caminham juntos
Em ambientes cloud, essa relação é ainda mais importante.
Imagine:
Job A
Cluster: 20 workers
Tempo: 2 horas
Depois de reduzir shuffles desnecessários:
Job A
Cluster: 20 workers
Tempo: 35 minutos
Não ganhamos apenas velocidade.
Reduzimos consumo de:
- CPU;
- memória;
- disco;
- rede;
- tempo de cluster.
Portanto:
Performance
↓
Efficiency
↓
Cost
Performance tuning também é FinOps.
28. O Spark moderno consegue ajudar
Nas versões modernas do Spark, mecanismos de otimização conseguem melhorar várias decisões automaticamente.
Um dos mais importantes é:
Adaptive Query Execution — AQE
Durante a execução, Spark pode utilizar estatísticas reais para adaptar partes do plano.
Isso pode ajudar em cenários relacionados a:
- shuffle partitions;
- estratégias de join;
- skew;
- coalescing de partições.
Conceitualmente:
Plano inicial
↓
Execução começa
↓
Estatísticas reais
↓
AQE
↓
Plano adaptado
Isso é uma evolução enorme em relação aos primeiros anos do Spark.
Mas existe uma lição importante:
Automação não elimina a necessidade de entender o sistema.
Quando sabemos como shuffle funciona, conseguimos interpretar melhor o que o Spark está tentando otimizar.
29. Como eu investigaria um Shuffle caro
Imagine um Job demorando muito.
Meu raciocínio seria algo parecido com:
JOB LENTO
↓
Qual Stage?
↓
Existe Shuffle?
↓
Quanto Shuffle Read/Write?
↓
As Tasks têm duração semelhante?
↓
Existe Skew?
↓
Existe Spill?
↓
Qual operação criou o Shuffle?
Depois:
groupBy?
join?
repartition?
distinct?
orderBy?
aggregation?
Então perguntaria:
Posso filtrar antes?
Posso selecionar menos colunas?
Posso agregar antes?
O repartition é necessário?
A chave está skewed?
Existe uma estratégia melhor de join?
Uma tabela poderia ser broadcast?
Isso é muito mais eficiente do que ajustar parâmetros aleatoriamente.
30. Checklist de Shuffle
Quando analiso um pipeline Spark, algumas perguntas são especialmente úteis:
- Quanto de Shuffle Write existe?
- Quanto de Shuffle Read existe?
- Qual operação gerou o shuffle?
- O shuffle é realmente necessário?
- Estamos movimentando colunas desnecessárias?
- Podemos filtrar dados antes?
- Existe agregação local possível?
- Existe data skew?
- Existem Tasks muito maiores que as outras?
- Há spill para disco?
-
repartition()está sendo usado sem necessidade? - O número de shuffle partitions faz sentido?
- O join está usando uma estratégia adequada?
- Uma das tabelas é pequena o suficiente para outra estratégia?
- O AQE está ajudando?
- O problema é computação ou movimentação de dados?
Essas perguntas normalmente são mais valiosas do que procurar:
"Qual configuração deixa Spark mais rápido?"
31. A mudança de mentalidade
Quando comecei a aprender Spark, era fácil olhar para:
df.groupBy("customer_id").count()
e enxergar apenas:
agrupar
+
contar
Depois de entender arquitetura distribuída, começo a enxergar:
Dataset
↓
Partitions
↓
Local Processing
↓
Shuffle Write
↓
Network
↓
Shuffle Read
↓
New Partitions
↓
Aggregation
↓
Result
Essa segunda visão é muito mais próxima do que realmente importa para Engenharia de Dados em escala.
32. O princípio que levo para outros sistemas
Existe uma ideia aqui que vai muito além do Spark:
Em sistemas distribuídos, mover dados costuma ser caro.
Esse princípio aparece em:
- bancos distribuídos;
- Data Warehouses;
- Lakehouses;
- sistemas de streaming;
- microserviços;
- Machine Learning distribuído;
- HPC;
- arquiteturas cloud.
Quanto maior o volume, mais importante fica pensar:
Onde os dados estão?
Onde a computação está?
Quanto precisa atravessar a rede?
Muitas decisões de arquitetura nascem dessas três perguntas.
33. A principal lição deste capítulo
Se eu pudesse resumir shuffle em uma única sequência, seria:
Dados distribuídos
↓
Operação precisa reorganizá-los
↓
Shuffle Write
↓
Disk / Memory
↓
Network
↓
Shuffle Read
↓
Novas Partitions
↓
Próximo Stage
E resumiria a otimização em uma pergunta:
Quanto dado realmente precisa atravessar esse caminho?
Quanto menos dados desnecessários movimentarmos, melhor.
Próximo artigo
Agora chegamos naturalmente a um dos assuntos que mais causam diferença entre um pipeline eficiente e um pipeline caro:
Joins
No próximo artigo vamos entender por que isto:
pedidos.join(
clientes,
"customer_id"
)
pode resultar em planos de execução completamente diferentes dependendo dos dados.
Vamos explorar:
- joins distribuídos;
- shuffle joins;
- Broadcast Hash Join;
- Sort Merge Join;
- estratégias de join;
- tabelas grandes vs. pequenas;
- estatísticas;
- broadcast;
- data skew;
- joins com chaves problemáticas;
- leitura do plano com
explain(); - Catalyst Optimizer;
- AQE;
- e como escolher uma estratégia baseada nos dados, não em receitas prontas.
Vamos evoluir deste modelo:
Table A ───┐
├──► SHUFFLE ───► JOIN
Table B ───┘
para entender quando podemos fazer algo próximo de:
Small Table
/ | \
▼ ▼ ▼
Large ───► Executor Executor Executor
│ │ │
▼ ▼ ▼
JOIN JOIN JOIN
É aí que Spark começa a mostrar de verdade o poder do seu otimizador.
← Parte 3: Partições, paralelismo e performance
Próximo: Parte 5 — Joins no Apache Spark: Shuffle, Broadcast e estratégias de execução













