A instrução EXPLAIN exibe o plano de execução de uma consulta SELECT no MaxCompute SQL. Esse recurso ajuda a identificar gargalos de desempenho nas instruções de consulta ou nas estruturas das tabelas.
Uma consulta é mapeada para um ou mais jobs, cada um contendo uma ou mais tarefas. O comando EXPLAIN revela como jobs, tarefas e operadores se relacionam, permitindo que você compreenda e otimize seu SQL.
Sintaxe
EXPLAIN <query>;
query: Obrigatório. Uma instrução SELECT. Para obter mais informações, consulte SELECT syntax.
Estrutura da saída
A saída do EXPLAIN possui três seções:
Dependências de jobs — Lista todos os jobs e sua ordem de execução. A mensagem
job0 is root jobindica que a consulta requer um único job raiz.-
Dependências de tarefas — Apresenta as tarefas dentro de cada job e suas dependências. A saída abaixo significa que
job0contém três tarefas —M1,M2eJ3_1_2_Stg1— e que o MaxCompute executaJ3_1_2_Stg1somente após a conclusão deM1eM2.In Job job0: root Tasks: M1, M2 J3_1_2_Stg1 depends on: M1, M2 -
Detalhes dos operadores — Descreve os operadores dentro de cada tarefa e suas semânticas de execução. A linha
Data sourceidentifica a entrada da tarefa. Cada linha subsequente representa um operador e seus parâmetros, com recuo para mostrar o pipeline de operadores.In Task M2: Data source: mf_mc_bj.sale_detail_jt/sale_date=2013/region=china TS: mf_mc_bj.sale_detail_jt/sale_date=2013/region=china FIL: ISNOTNULL(customer_id) RS: order: + nullDirection: * optimizeOrderBy: False valueDestLimit: 0 dist: HASH keys: customer_id values: customer_id (string) total_price (double) partitions: customer_id
Convenções de nomenclatura de tarefas
|
Componente |
Significado |
Exemplo |
|
Primeira letra |
Tipo de tarefa: |
|
|
Dígito após a primeira letra |
ID da tarefa, exclusivo dentro da consulta |
|
|
Dígitos separados por sublinhados |
Dependências diretas da tarefa |
|
Operadores
|
Operador |
Abreviação |
Cláusula SQL |
Descrição |
|
TableScanOperator |
TS |
|
Varre a tabela de entrada. A saída mostra o alias da tabela de entrada. |
|
SelectOperator |
SEL |
|
Projeta colunas para o próximo operador. Uma coluna é exibida como |
|
FilterOperator |
FIL |
|
Filtra linhas com base em uma expressão |
|
JoinOperator |
JOIN |
|
Une tabelas. A saída indica quais tabelas foram unidas e o método de junção utilizado. |
|
GroupByOperator |
AGGREGATE |
Funções de agregação |
Executa agregações. Aparece quando a consulta contém funções de agregação. A saída mostra o conteúdo da função de agregação. |
|
ReduceSinkOperator |
RS |
-- |
Distribui dados entre tarefas. Surge no final de uma tarefa quando sua saída alimenta outra tarefa. A saída exibe a ordem de classificação, chaves de distribuição, valores e colunas de hash. |
|
FileSinkOperator |
FS |
|
Grava os resultados finais no armazenamento. Para instruções |
|
LimitOperator |
LIM |
|
Limita o número de linhas retornadas. |
|
MapjoinOperator |
HASHJOIN |
|
Executa uma junção no lado do map para tabelas grandes. Funciona de maneira similar ao JoinOperator. |
Cada linha de operador também emite uma linha Statistics que mostra a contagem estimada de linhas e o tamanho dos dados naquele ponto do pipeline. Por exemplo, Statistics: Num rows: 3.0, Data size: 324.0 informa quantas linhas o otimizador espera que o operador produza. Utilize essas estimativas para identificar operadores que processam muito mais dados do que o esperado — um sinal comum de filtro ausente ou chave de junção desbalanceada.
A saída do ReduceSinkOperator (RS) inclui os seguintes campos:
|
Campo |
Descrição |
|
|
Direção de ordenação para cada chave: |
|
|
Define como valores nulos são ordenados em relação aos não nulos: |
|
|
Indica se o otimizador pode ignorar uma ordenação completa porque os dados já estão ordenados. O valor |
|
|
Número máximo de linhas enviadas para a tarefa subsequente. O valor |
|
|
Método de distribuição: |
|
|
Colunas usadas para particionar e ordenar dados entre tarefas. |
|
|
Colunas passadas para a tarefa subsequente. |
|
|
Colunas usadas para determinar qual tarefa recebe cada linha. |
Limitações
Se uma consulta for complexa e a saída do EXPLAIN ultrapassar 4 MB, o limite da API da aplicação de camada superior será atingido e a saída será truncada. Para contornar isso, divida a consulta em subconsultas menores e execute EXPLAIN em cada uma separadamente.
Exemplos
Preparar dados de amostra
Crie duas tabelas particionadas, sale_detail e sale_detail_jt, e insira dados de amostra.
-- Create two partitioned tables named sale_detail and sale_detail_jt.
CREATE TABLE if NOT EXISTS sale_detail
(
shop_name STRING,
customer_id STRING,
total_price DOUBLE
)
PARTITIONED BY (sale_date STRING, region STRING);
CREATE TABLE if NOT EXISTS sale_detail_jt
(
shop_name STRING,
customer_id STRING,
total_price DOUBLE
)
PARTITIONED BY (sale_date STRING, region STRING);
-- Add partitions to the two tables.
ALTER TABLE sale_detail ADD PARTITION (sale_date='2013', region='china') PARTITION (sale_date='2014', region='shanghai');
ALTER TABLE sale_detail_jt ADD PARTITION (sale_date='2013', region='china');
-- Insert data into the tables.
INSERT INTO sale_detail PARTITION (sale_date='2013', region='china') VALUES ('s1','c1',100.1),('s2','c2',100.2),('s3','c3',100.3);
INSERT INTO sale_detail PARTITION (sale_date='2014', region='shanghai') VALUES ('null','c5',null),('s6','c6',100.4),('s7','c7',100.5);
INSERT INTO sale_detail_jt PARTITION (sale_date='2013', region='china') VALUES ('s1','c1',100.1),('s2','c2',100.2),('s5','c2',100.2);
Verifique os dados:
SET odps.sql.allow.fullscan=true;
SELECT * FROM sale_detail;
+------------+-------------+-------------+------------+------------+
| shop_name | customer_id | total_price | sale_date | region |
+------------+-------------+-------------+------------+------------+
| s1 | c1 | 100.1 | 2013 | china |
| s2 | c2 | 100.2 | 2013 | china |
| s3 | c3 | 100.3 | 2013 | china |
| null | c5 | NULL | 2014 | shanghai |
| s6 | c6 | 100.4 | 2014 | shanghai |
| s7 | c7 | 100.5 | 2014 | shanghai |
+------------+-------------+-------------+------------+------------+
SET odps.sql.allow.fullscan=true;
SELECT * FROM sale_detail_jt;
+------------+-------------+-------------+------------+------------+
| shop_name | customer_id | total_price | sale_date | region |
+------------+-------------+-------------+------------+------------+
| s1 | c1 | 100.1 | 2013 | china |
| s2 | c2 | 100.2 | 2013 | china |
| s5 | c2 | 100.2 | 2013 | china |
+------------+-------------+-------------+------------+------------+
Crie uma tabela não particionada para a operação de JOIN:
SET odps.sql.allow.fullscan=true;
CREATE TABLE shop AS SELECT shop_name, customer_id, total_price FROM sale_detail;
Exemplo 1: JOIN padrão com agregação
Este exemplo executa EXPLAIN em uma consulta que realiza um INNER JOIN entre sale_detail_jt e sale_detail, agrupa os resultados e aplica ordenação com limite.
Consulta:
SELECT a.customer_id AS ashop, SUM(a.total_price) AS ap,COUNT(b.total_price) AS bp
FROM (SELECT * FROM sale_detail_jt WHERE sale_date='2013' AND region='china') a
INNER JOIN (SELECT * FROM sale_detail WHERE sale_date='2013' AND region='china') b
ON a.customer_id=b.customer_id
GROUP BY a.customer_id
ORDER BY a.customer_id
LIMIT 10;
Execute EXPLAIN:
EXPLAIN
SELECT a.customer_id AS ashop, SUM(a.total_price) AS ap,COUNT(b.total_price) AS bp
FROM (SELECT * FROM sale_detail_jt WHERE sale_date='2013' AND region='china') a
INNER JOIN (SELECT * FROM sale_detail WHERE sale_date='2013' AND region='china') b
ON a.customer_id=b.customer_id
GROUP BY a.customer_id
ORDER BY a.customer_id
LIMIT 10;
Saída:
job0 is root job
In Job job0:
root Tasks: M1
M2_1 depends on: M1
R3_2 depends on: M2_1
R4_3 depends on: R3_2
In Task M1:
Data source: doc_****.default.sale_detail/sale_date=2013/region=china
TS: doc_****.default.sale_detail/sale_date=2013/region=china
Statistics: Num rows: 3.0, Data size: 324.0
FIL: ISNOTNULL(customer_id)
Statistics: Num rows: 2.7, Data size: 291.6
RS: valueDestLimit: 0
dist: BROADCAST
keys:
values:
customer_id (string)
total_price (double)
partitions:
Statistics: Num rows: 2.7, Data size: 291.6
In Task M2_1:
Data source: doc_****.default.sale_detail_jt/sale_date=2013/region=china
TS: doc_****.default.sale_detail_jt/sale_date=2013/region=china
Statistics: Num rows: 3.0, Data size: 324.0
FIL: ISNOTNULL(customer_id)
Statistics: Num rows: 2.7, Data size: 291.6
HASHJOIN:
Filter1 INNERJOIN StreamLineRead1
keys:
0:customer_id
1:customer_id
non-equals:
0:
1:
bigTable: Filter1
Statistics: Num rows: 3.6450000000000005, Data size: 787.32
RS: order: +
nullDirection: *
optimizeOrderBy: False
valueDestLimit: 0
dist: HASH
keys:
customer_id
values:
customer_id (string)
total_price (double)
total_price (double)
partitions:
customer_id
Statistics: Num rows: 3.6450000000000005, Data size: 422.82000000000005
In Task R3_2:
AGGREGATE: group by:customer_id
UDAF: SUM(total_price) (__agg_0_sum)[Complete],COUNT(total_price) (__agg_1_count)[Complete]
Statistics: Num rows: 1.0, Data size: 116.0
RS: order: +
nullDirection: *
optimizeOrderBy: True
valueDestLimit: 10
dist: HASH
keys:
customer_id
values:
customer_id (string)
__agg_0 (double)
__agg_1 (bigint)
partitions:
Statistics: Num rows: 1.0, Data size: 116.0
In Task R4_3:
SEL: customer_id,__agg_0,__agg_1
Statistics: Num rows: 1.0, Data size: 116.0
SEL: customer_id ashop, __agg_0 ap, __agg_1 bp, customer_id
Statistics: Num rows: 1.0, Data size: 216.0
FS: output: Screen
schema:
ashop (string)
ap (double)
bp (bigint)
Statistics: Num rows: 1.0, Data size: 116.0
OK
O plano de execução mostra quatro tarefas:
M1 — Varre
sale_detail, filtra valores nulos decustomer_ide transmite os dados para todas as tarefas subsequentes (dist: BROADCAST). Os camposkeysepartitionsvazios confirmam que os dados não são particionados — cada linha vai para todos os lugares.M2_1 — Lê
sale_detail_jt, filtra valores nulos decustomer_ide executa uma junção hash no lado do map (HASHJOIN) com os dados transmitidos de M1. Os resultados são particionados por hash usandocustomer_idpara a tarefa de reduce subsequente.R3_2 — Agrega com
GROUP BY customer_id, calculandoSUM(total_price)eCOUNT(total_price). Ambas as operações rodam no modoComplete, o que significa que a agregação completa ocorre em uma única fase, sem pré-agregação parcial.R4_3 — Seleciona as colunas finais, aplica aliases (
ashop,ap,bp) e grava a saída na tela.
Exemplo 2: Junção no lado do map (hint MAPJOIN)
Neste exemplo, utiliza-se o hint /*+ mapjoin(a) */ para forçar uma junção no lado do map com uma condição de junção não equitativa (a.total_price < b.total_price).
Consulta:
SELECT /*+ mapjoin(a) */
a.customer_id AS ashop, SUM(a.total_price) AS ap,COUNT(b.total_price) AS bp
FROM (SELECT * FROM sale_detail_jt
WHERE sale_date='2013' AND region='china') a
INNER JOIN (SELECT * FROM sale_detail WHERE sale_date='2013' AND region='china') b
ON a.total_price<b.total_price
GROUP BY a.customer_id
ORDER BY a.customer_id
LIMIT 10;
Execute EXPLAIN:
EXPLAIN
SELECT /*+ mapjoin(a) */
a.customer_id AS ashop, SUM(a.total_price) AS ap,COUNT(b.total_price) AS bp
FROM (SELECT * FROM sale_detail_jt
WHERE sale_date='2013' AND region='china') a
INNER JOIN (SELECT * FROM sale_detail WHERE sale_date='2013' AND region='china') b
ON a.total_price<b.total_price
GROUP BY a.customer_id
ORDER BY a.customer_id
LIMIT 10;
Saída:
job0 is root job
In Job job0:
root Tasks: M1
M2_1 depends on: M1
R3_2 depends on: M2_1
R4_3 depends on: R3_2
In Task M1:
Data source: doc_****.sale_detail_jt/sale_date=2013/region=china
TS: doc_****.sale_detail_jt/sale_date=2013/region=china
Statistics: Num rows: 3.0, Data size: 324.0
RS: valueDestLimit: 0
dist: BROADCAST
keys:
values:
customer_id (string)
total_price (double)
partitions:
Statistics: Num rows: 3.0, Data size: 324.0
In Task M2_1:
Data source: doc_****.sale_detail/sale_date=2013/region=china
TS: doc_****.sale_detail/sale_date=2013/region=china
Statistics: Num rows: 3.0, Data size: 24.0
HASHJOIN:
StreamLineRead1 INNERJOIN TableScan2
keys:
0:
1:
non-equals:
0:
1:
bigTable: TableScan2
Statistics: Num rows: 9.0, Data size: 1044.0
FIL: LT(total_price,total_price)
Statistics: Num rows: 6.75, Data size: 783.0
AGGREGATE: group by:customer_id
UDAF: SUM(total_price) (__agg_0_sum)[Partial_1],COUNT(total_price) (__agg_1_count)[Partial_1]
Statistics: Num rows: 2.3116438356164384, Data size: 268.1506849315069
RS: order: +
nullDirection: *
optimizeOrderBy: False
valueDestLimit: 0
dist: HASH
keys:
customer_id
values:
customer_id (string)
__agg_0_sum (double)
__agg_1_count (bigint)
partitions:
customer_id
Statistics: Num rows: 2.3116438356164384, Data size: 268.1506849315069
In Task R3_2:
AGGREGATE: group by:customer_id
UDAF: SUM(__agg_0_sum)[Final] __agg_0,COUNT(__agg_1_count)[Final] __agg_1
Statistics: Num rows: 1.6875, Data size: 195.75
RS: order: +
nullDirection: *
optimizeOrderBy: True
valueDestLimit: 10
dist: HASH
keys:
customer_id
values:
customer_id (string)
__agg_0 (double)
__agg_1 (bigint)
partitions:
Statistics: Num rows: 1.6875, Data size: 195.75
In Task R4_3:
SEL: customer_id,__agg_0,__agg_1
Statistics: Num rows: 1.6875, Data size: 195.75
SEL: customer_id ashop, __agg_0 ap, __agg_1 bp, customer_id
Statistics: Num rows: 1.6875, Data size: 364.5
FS: output: Screen
schema:
ashop (string)
ap (double)
bp (bigint)
Statistics: Num rows: 1.6875, Data size: 195.75
OK
Principais diferenças em relação ao Exemplo 1:
M1 transmite
sale_detail_jt(a tabela especificada pelo hint mapjoin) sem filtro, pois a condição de junção não equitativa é avaliada após a junção, e não antes.M2_1 executa o HASHJOIN, aplica o filtro
LT(total_price, total_price)(representandoa.total_price < b.total_price) e roda uma agregação parcial (fasePartial_1). A fasePartial_1pré-agrega os dados no lado do map, reduzindo o volume enviado para a tarefa de reduce.R3_2 conclui a agregação na fase
Final, mesclando os resultados parciais de M2_1. A abordagem em duas fases (dePartial_1paraFinal) é mais eficiente do que o modoCompletede fase única do Exemplo 1 quando o volume de dados no lado do map é grande.R4_3 seleciona e aplica aliases às colunas de saída finais, da mesma forma que no Exemplo 1.