O MaxCompute cobra com base na quantidade de dados lidos e nos recursos de computação consumidos. Dois padrões causam a maioria dos aumentos inesperados na fatura: jobs SQL que leem mais dados do que o necessário e jobs MapReduce que alocam mais recursos do que a carga de trabalho exige. Este tópico explica como diagnosticar e corrigir ambos os casos.
Estime os custos antes de executar jobs
Use as ferramentas de TCO para estimar os custos de computação antes de enviar jobs. Para jobs SQL, use o CostSQL para visualizar o custo de uma consulta antes da execução. Configure alertas de consumo de recursos para identificar precocemente qualquer crescimento inesperado de custos.
Reduza os custos de computação SQL
Evite leituras completas de tabela
As leituras completas de tabela (full table scans) são a principal causa dos altos custos de computação SQL. O MaxCompute cobra com base nos dados lidos — ler uma tabela inteira quando você precisa apenas de um subconjunto multiplica sua fatura.
Desative as leituras completas de tabela no nível da sessão ou do projeto:
-- Disable for the current session
set odps.sql.allow.fullscan=false;
-- Disable for the entire project
SetProject odps.sql.allow.fullscan=false;
**Use poda de colunas — evite SELECT *:**
O comando SELECT * sempre aciona uma leitura completa da tabela. Selecione apenas as colunas necessárias:
-- Table T has columns a, b, c, d, e.
-- This query reads only a, b, and e — skipping c and d entirely.
SELECT a, b FROM T WHERE e < 10;
Use poda de partições — filtre pelas colunas de chave de partição:
Especificar uma chave de partição na cláusula WHERE instrui o MaxCompute a ignorar partições irrelevantes e ler apenas os dados correspondentes:
SELECT a, b FROM T WHERE partitiondate = '2017-10-01';
Sem um filtro de partição, um JOIN ou SELECT em uma tabela particionada recorre a uma leitura completa da tabela. Sempre faça a poda das partições antes de executar JOINs. Para casos em que a poda de partições não surte efeito, consulte Cenários em que a poda de partições não surte efeito.
Reescreva padrões SQL custosos
Algumas palavras-chave SQL acionam embaralhamento e ordenação extras, o que consome recursos adicionais de computação. A causa raiz é a movimentação de dados — quando o mecanismo precisa reorganizar os dados entre nós para atender a uma consulta, ele consome CPU e I/O proporcionalmente ao volume de dados movido. Reescrever esses padrões reduz ou elimina essa movimentação.
Substitua FULL OUTER JOIN por UNION ALL
O FULL OUTER JOIN exige que o mecanismo corresponda a cada linha em ambas as tabelas, produzindo um grande embaralhamento intermediário. O UNION ALL elimina completamente a junção ao preencher os valores ausentes com zeros:
-- Original: FULL OUTER JOIN
SELECT COALESCE(t1.id, t2.id) AS id, SUM(t1.col1) AS col1, SUM(t2.col2) AS col2
FROM (
SELECT id, col1 FROM table1
) t1
FULL OUTER JOIN (
SELECT id, col2 FROM table2
) t2
ON t1.id = t2.id
GROUP BY COALESCE(t1.id, t2.id);
-- Optimized: UNION ALL (no join, no data movement overhead)
SELECT t.id, SUM(t.col1) AS col1, SUM(t.col2) AS col2
FROM (
SELECT id, col1, 0 AS col2 FROM table1
UNION ALL
SELECT id, 0 AS col1, col2 FROM table2
) t
GROUP BY t.id;
Mova o GROUP BY para fora do UNION ALL
Colocar o GROUP BY dentro de cada ramificação de um UNION ALL faz com que o mecanismo agregue os dados duas vezes. Agregue os dados uma única vez após a união:
-- Original: GROUP BY inside each branch (double aggregation)
SELECT t.id, SUM(t.val) AS val
FROM (
SELECT id, SUM(col3) AS val FROM table3 GROUP BY id
UNION ALL
SELECT id, SUM(col4) AS val FROM table4 GROUP BY id
) t
GROUP BY t.id;
-- Optimized: single aggregation after union
SELECT t.id, SUM(t.val) AS val
FROM (
SELECT id, col3 AS val FROM table3
UNION ALL
SELECT id, col4 AS val FROM table4
) t
GROUP BY t.id;
Substitua DISTINCT por GROUP BY
O uso de DISTINCT em grandes conjuntos de dados exige uma ordenação completa. Já o GROUP BY atinge o mesmo resultado com menos sobrecarga:
-- Original: DISTINCT (full sort)
SELECT COUNT(DISTINCT id) AS cnt FROM table1;
-- Optimized: GROUP BY (more efficient deduplication)
SELECT COUNT(1) AS cnt
FROM (
SELECT id FROM table1 GROUP BY id
) t;
Outros padrões a evitar:
Prefira usar
INSERT INTOcom um campo de partição em vez de inserir sem particionamento. Essa prática reduz a complexidade do SQL e diminui o custo.Ordene dados exportados temporariamente usando ferramentas externas, como o Excel, em vez de usar ORDER BY. No MaxCompute, o ORDER BY aciona uma ordenação global em todo o conjunto de dados.
Controle a frequência de agendamento
O MaxCompute foi projetado para processamento de grandes lotes, não para consultas em tempo real. Agendar jobs SQL em intervalos curtos — a cada poucos segundos ou minutos — acumula filas de jobs na modalidade de pagamento conforme o uso, e a fatura do dia seguinte pode apresentar picos inesperados.
Execute o CostSQL para estimar o custo de jobs agendados frequentemente antes de definir sua cadência. Para cargas de trabalho que exigem resultados quase em tempo real, utilize um serviço dedicado de computação em tempo real em vez do MaxCompute.
Visualize dados da tabela sem executar SQL
Executar SELECT * FROM table LIMIT 10 consome recursos de computação. Utilize o recurso integrado de visualização de tabela — ele lê os dados diretamente do armazenamento sem acionar um job de computação:
DataWorks: Abra a página Data Map, localize a tabela e utilize a guia de visualização. Consulte Visualizar os detalhes de uma tabela.
MaxCompute Studio: Clique duas vezes em uma tabela na árvore de objetos para visualizar seus dados.
Escolha a ferramenta adequada para a carga de trabalho
O MaxCompute retorna resultados em minutos, não em milissegundos. É a ferramenta correta para análises de grandes lotes, mas inadequada para consultas de frontend que exigem respostas instantâneas.
Para consultas de frontend — painéis, resultados de pesquisa, buscas de linhas — utilize um banco de dados relacional como o ApsaraDB for RDS. Armazene os resultados agregados do MaxCompute nesse banco e atenda às consultas a partir dele. Consultas de frontend sem cláusula WHERE, sem agregação e sem junção com dicionários serão sempre lentas no MaxCompute.
Reduza os custos de computação MapReduce
Configure o tamanho do split e a quantidade de reducers
Duas configurações têm o maior impacto no consumo de recursos do MapReduce.
O tamanho do split controla quantos mappers são criados. O padrão é 256 MB por split. Um tamanho menor gera mais mappers, aumentando o paralelismo, mas também o uso de recursos. Use JobConf#setSplitSize para ajustar esse valor com base no custo computacional da sua lógica de map — se o processamento de cada registro for custoso, reduza o tamanho do split para criar mais mappers e distribuir o trabalho.
A quantidade de reducers tem como padrão um quarto do número de mappers e pode ser definida com qualquer valor entre 0 e 2.000. Mais reducers consomem mais recursos. Defina apenas a quantidade de reducers necessária para sua carga de trabalho de agregação e use jobconf.setNumReduceTasks(num) para configurar explicitamente esse número.
Una jobs sequenciais usando o modo pipeline
Quando vários jobs MapReduce são encadeados — em que a saída de um job alimenta o próximo — cada job intermediário grava os resultados no disco e os lê novamente. Esse I/O de disco se acumula ao longo da cadeia.
O modo pipeline une jobs MapReduce sequenciais em um único job, eliminando as leituras e gravações intermediárias no disco. Isso reduz tanto o custo quanto a sobrecarga de agendamento. Para exemplos de implementação, consulte Exemplos de pipeline.
Faça poda de colunas nas tabelas de entrada
Quando um mapper lê uma tabela de entrada, mas precisa apenas de algumas de suas colunas, ler a linha inteira desperdiça I/O. Especifique as colunas necessárias ao adicionar uma tabela de entrada:
InputUtils.addTable(TableInfo.builder().tableName("wc_in").cols(new String[]{"c1","c2"}).build(), job);
Após essa configuração, o mapper lê apenas as colunas c1 e c2. Os dados acessados pelo nome da coluna não são afetados; já os dados acessados por índice subscrito podem ter comportamento diferente.
Leia recursos na fase de setup
Cada chamada para ler um recurso gera sobrecarga, sendo possível ler recursos até 64 vezes. Ler o mesmo recurso dentro de uma função map ou reduce faz com que ele seja relido a cada registro. Em vez disso, leia os recursos uma única vez na fase de setup. Para exemplos de uso, consulte Exemplo de uso de recursos.
Evite construir objetos na função map ou reduce
Objetos Java construídos dentro de uma função map ou reduce são recriados a cada invocação de registro. Mova a construção de objetos para a fase de setup:
Record word;
Record one;
public void setup(TaskContext context) throws IOException {
// Construct once in setup — not on every map call
word = context.createMapOutputKeyRecord();
one = context.createMapOutputValueRecord();
one.set(new Object[]{1L});
}
Use um combiner quando a saída do map tiver chaves duplicadas
Um combiner pré-agrega a saída de uma tarefa de map antes que ela seja enviada aos reducers, reduzindo o volume de dados transferidos pela rede durante a fase de embaralhamento.
Utilize um combiner apenas quando a saída do map contiver vários registros com a mesma chave — por exemplo, em um job de contagem de palavras. Se as chaves de saída do map forem únicas, o combiner adiciona sobrecarga sem trazer benefícios.
O combiner abaixo soma os valores para chaves correspondentes:
/**
* A combiner class that combines map output by summing values.
*/
public static class SumCombiner extends ReducerBase {
private Record count;
@Override
public void setup(TaskContext context) throws IOException {
count = context.createMapOutputValueRecord();
}
@Override
public void reduce(Record key, Iterator<Record> values, TaskContext context)
throws IOException {
long c = 0;
while (values.hasNext()) {
Record val = values.next();
c += (Long) val.get(0);
}
count.set(0, c);
context.write(key, count);
}
}
Previna skew de dados com colunas de partição ou um partitioner personalizado
Por padrão, os reducers recebem dados com base no hash do esquema completo da chave. Se alguns valores de chave forem muito mais comuns que outros, certos reducers receberão significativamente mais dados — um problema de cauda longa em que alguns reducers terminam muito depois dos demais, atrasando todo o job.
Para distribuir os dados de forma mais uniforme entre os reducers, especifique colunas de chave de partição usando JobConf#setPartitionColumns. Os dados são roteados para os reducers com base no hash dessas colunas, e não na chave completa:
jobconf.setPartitionerClass(MyPartitioner.class)
jobconf.setNumReduceTasks(num)
Para um controle mais refinado, implemente um partitioner personalizado:
import com.aliyun.odps.mapred.Partitioner;
public static class MyPartitioner extends Partitioner {
@Override
public int getPartition(Record key, Record value, int numPartitions) {
// numPartitions is the total number of reducers.
// Route each key to a reducer based on key length.
String k = key.get(0).toString();
return k.length() % numPartitions;
}
}
Mantenha a memória da JVM dentro da proporção de 1:4 entre CPU e memória
A configuração padrão é 1 núcleo de CPU e 4 GB de memória, com odps.stage.reducer.jvm.mem definido como 4006. Alocar memória além da proporção de 1:4 em relação aos núcleos de CPU aumenta o faturamento. Ajuste odps.stage.reducer.jvm.mem para corresponder à sua carga de trabalho real, evitando provisionamento excessivo.
Próximos passos
Para otimizar os custos de armazenamento, consulte Otimizar custos de armazenamento.
Para reduzir os custos de upload e download de dados, consulte Otimizar os custos de uploads e downloads de dados.
Para analisar e resolver anomalias de faturamento, consulte Gerenciar custos.
Para gerar um plano de otimização de recursos, consulte Gerar um plano de otimização de recursos de computação.