Todos os produtos
Search
Central de documentação

PolarDB:Execute a instrução EXPLAIN para visualizar planos de execução de consultas paralelas elásticas

Última atualização: Jun 28, 2026

Use a instrução EXPLAIN para inspecionar planos de execução e confirme se uma consulta é executada em paralelo no PolarDB for MySQL.

Como funciona

Quando a consulta paralela elástica está ativa, o PolarDB divide a consulta em subtarefas e as distribui para vários workers em um ou mais nós. O plano de execução reflete esse comportamento ao introduzir operadores específicos de paralelismo, além dos campos padrão de saída do EXPLAIN.

Campos padrão de saída do EXPLAIN:

Campo

Descrição

Id

Identificador da etapa no plano de execução

Select_type

Tipo de SELECT (por exemplo, SIMPLE, PRIMARY, DEPENDENT SUBQUERY)

Table

Tabela acessada nesta etapa ou um nó virtual como <gather2>

Type

Tipo de junção ou acesso (por exemplo, index, range, ALL)

Key

Índice utilizado

Rows

Número estimado de linhas examinadas

Filtered

Percentual estimado de linhas que atendem à condição de filtro

Extra

Informações adicionais de execução, incluindo operadores paralelos

Operadores específicos de paralelismo:

Operador

Onde aparece

Função

Gather

Nível superior ou subárvore

Coleta resultados parciais de todos os workers e os mescla em um único conjunto de resultados

Parallel scan (N workers)

Campo Extra

Indica uma varredura paralela de tabela ou índice com N workers

Parallel pushdown

Campo Extra

Envia uma subconsulta dependente aos workers para execução paralela

Broadcast

Saída do EXPLAIN FORMAT=TREE

Envia uma cópia de um conjunto de dados para todos os workers

Shared access

Saída do EXPLAIN FORMAT=TREE

Executa uma subconsulta paralela uma única vez e compartilha o resultado entre os workers

O nó Gather na árvore do plano marca o limite entre a execução paralela e a serial. Tudo abaixo de um nó Gather é executado em paralelo; tudo acima é executado serialmente no coordenador.

Tabela de exemplo

Todos os exemplos neste tópico usam a tabela pq_test.

Schema:

SHOW CREATE TABLE pq_test\G
*************************** 1. row ***************************
       Table: pq_test
Create Table: CREATE TABLE `pq_test` (
  `id` BIGINT(20) NOT NULL AUTO_INCREMENT,
  `help_topic_id` INT(10) UNSIGNED NOT NULL,
  `name` CHAR(64) NOT NULL,
  `help_category_id` SMALLINT(5) UNSIGNED NOT NULL,
  `description` TEXT NOT NULL,
  `example` TEXT NOT NULL,
  `url` TEXT NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=21495809 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)

Tamanho da tabela:

SHOW TABLE STATUS\G
*************************** 1. row ***************************
           Name: pq_test
         Engine: InnoDB
        Version: 10
     Row_format: Dynamic
           Rows: 20064988
 Avg_row_length: 1898
    Data_length: 38085328896
Max_data_length: 0
   Index_length: 0
      Data_free: 4194304
 Auto_increment: 21495809
    Create_time: 2019-07-30 01:35:27
     Update_time: NULL
      Check_time: NULL
       Collation: utf8_general_ci
        Checksum: NULL
 Create_options:
         Comment:
1 row in set (0.02 sec)

Consulta de teste:

SELECT COUNT(*) FROM pq_test;

Visualize um plano de consulta não paralela

Defina max_parallel_degree como 0 para desativar a consulta paralela e execute o EXPLAIN:

SET max_parallel_degree=0; EXPLAIN SELECT COUNT(*) FROM pq_test\G
*************************** 1. row ***************************
           Id: 1
  Select_type: SIMPLE
        Table: pq_test
   Partitions: NULL
         Type: index
Possible_keys: NULL
          Key: PRIMARY
      Key_len: 8
          Ref: NULL
         Rows: 20064988
     Filtered: 100.00
        Extra: Using index
1 row in set, 1 warning (0.03 sec)

O plano mostra uma varredura de índice padrão. Não há nó Gather e o campo Extra não contém o marcador Parallel scan, o que confirma que a consulta é executada serialmente.

Visualize um plano de consulta paralela

PolarDB for MySQL 8.0.1: EXPLAIN

EXPLAIN SELECT COUNT(*) FROM pq_test\G
*************************** 1. row ***************************
           Id: 1
  Select_type: SIMPLE
        Table: <gather2>
   Partitions: NULL
         Type: ALL
Possible_keys: NULL
          Key: NULL
      Key_len: NULL
          Ref: NULL
         Rows: 20064988
     Filtered: 100.00
        Extra: NULL
*************************** 2. row ***************************
           Id: 2
  Select_type: SIMPLE
        Table: pq_test
   Partitions: NULL
         Type: index
Possible_keys: NULL
          Key: PRIMARY
      Key_len: 8
          Ref: NULL
         Rows: 10032494
     Filtered: 100.00
        Extra: Parallel scan (2 workers); Using index
2 rows in set, 1 warning (0.00 sec)

Duas linhas aparecem em vez de uma:

  • A linha 1 (<gather2>) representa a operação Gather, que coleta e mescla os resultados de todos os workers. Esta linha marca o limite do paralelismo — tudo abaixo dela é executado em paralelo.

  • A linha 2 exibe a varredura real em pq_test com Parallel scan (2 workers) no campo Extra, confirmando que 2 workers executam a varredura do índice em paralelo.

PolarDB for MySQL 8.0.2: EXPLAIN FORMAT=TREE

O comando EXPLAIN FORMAT=TREE retorna um plano em formato de árvore com detalhes mais ricos sobre a estrutura de execução paralela:

EXPLAIN FORMAT=TREE SELECT COUNT(*) FROM pq_test\G
*************************** 1. row ***************************
EXPLAIN: -> Count rows in gather  (cost=... rows=1)
    -> Gather (slice: 1; workers: 2, nodes: 2)
        -> Parallel index scan on pq_test using PRIMARY, with parallel partitions: 8  (cost=... rows=20064988)

Leitura de cima para baixo:

  • Count rows in gather — o coordenador agrega a contagem final a partir dos resultados dos workers.

  • Gather (slice: 1; workers: 2, nodes: 2) — 2 workers são executados em 2 nós. Este é o limite do paralelismo.

  • Parallel index scan on pq_test using PRIMARY, with parallel partitions: 8 — o índice é dividido em 8 partições varridas em paralelo.

Visualize um plano de consulta paralela com subconsultas

PolarDB for MySQL 8.0.1: EXPLAIN

EXPLAIN SELECT
    o_orderpriority,
    COUNT(*) as order_count
FROM
    orders
WHERE
    o_orderdate >= '1994-04-01'
    AND o_orderdate < date_add('1994-04-01', interval '3' month)
    AND exists (
        SELECT
            *
        FROM
            lineitem
        WHERE
            l_orderkey = o_orderkey
            and l_commitdate < l_receiptdate
    )
GROUP BY
    o_orderpriority
ORDER BY
    o_orderpriority\G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: <gather1.1>
   partitions: NULL
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1489068
     filtered: 100.00
        Extra: Using temporary; Using filesort
*************************** 2. row ***************************
           id: 1
  select_type: PRIMARY
        table: orders
   partitions: NULL
         type: range
possible_keys: i_o_orderdate
          key: i_o_orderdate
      key_len: 3
          ref: NULL
         rows: 568369
     filtered: 100.00
        Extra: Parallel scan (2 workers); Using index condition; Using where; Using temporary
*************************** 3. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: lineitem
   partitions: NULL
         type: ref
possible_keys: PRIMARY,i_l_orderkey
          key: PRIMARY
      key_len: 4
          ref: tpch_10.orders.O_ORDERKEY
         rows: 4
     filtered: 33.3
        Extra: Parallel pushdown; Using where
3 rows in set, 2 warnings (0.01 sec)

A linha 3 apresenta select_type: DEPENDENT SUBQUERY com Extra: Parallel pushdown. Isso significa que a subconsulta é delegada a cada worker para execução paralela, em vez de ser avaliada apenas uma vez no coordenador.

PolarDB for MySQL 8.0.2: EXPLAIN FORMAT=TREE

Exemplo 1

EXPLAIN FORMAT=TREE SELECT
    o_orderpriority,
    COUNT(*) as order_count
FROM
    orders
WHERE
    o_orderdate >= '1994-04-01'
    AND o_orderdate < date_add('1994-04-01', interval '3' month)
    AND exists (
        SELECT
            *
        FROM
            lineitem
        WHERE
            l_orderkey = o_orderkey
            and l_commitdate < l_receiptdate
    )
GROUP BY
    o_orderpriority
ORDER BY
    o_orderpriority\G
*************************** 1. row ***************************
EXPLAIN: -> Sort: <temporary>.o_orderpriority
    -> Table scan on <temporary>
        -> Aggregate using temporary table  (cost=1746887.76 rows=1489068)
            -> Gather (slice: 1; workers: 2)  (cost=1597980.96 rows=1489068)
                -> Table scan on <temporary>
                    -> Aggregate using temporary table  (cost=1486290.85 rows=744534)
                        -> Filter: exists(select #2)  (cost=772982.43 rows=568369)
                            -> Parallel index range scan on orders using i_o_orderdate, with index condition: ((orders.O_ORDERDATE >= DATE'1994-04-01') and (orders.O_ORDERDATE < <cache>(('1994-04-01' + interval '3' month)))), with parallel partitions: 89  (cost=772982.43 rows=568369)
                            -> Select #2 (subquery in condition; dependent)
                                -> Limit: 1 row(s)
                                    -> Filter: (lineitem.L_COMMITDATE < lineitem.L_RECEIPTDATE)  (cost=1.14 rows=1)
                                        -> Index lookup on lineitem using PRIMARY (L_ORDERKEY=orders.O_ORDERKEY)  (cost=1.14 rows=4)

1 row in set, 1 warning (0.02 sec)

O nó Gather (slice: 1; workers: 2) separa a subárvore paralela (abaixo) da agregação e ordenação seriais (acima). A subconsulta (Select #2) está marcada como dependent, indicando que ela é executada dentro de cada worker como Parallel pushdown.

Exemplo 2

EXPLAIN FORMAT=TREE
select
    ps_partkey,
    sum(ps_supplycost * ps_availqty) as value
from
    partsupp,
    supplier,
    nation
where
    ps_suppkey = s_suppkey
    and s_nationkey = n_nationkey
    and n_name = 'IRAN'
group by
    ps_partkey having
        sum(ps_supplycost * ps_availqty) > (
            select
                sum(ps_supplycost * ps_availqty) * 0.0000010000
            from
                partsupp,
                supplier,
                nation
            where
                ps_suppkey = s_suppkey
                and s_nationkey = n_nationkey
                and n_name = 'IRAN'
        )
order by
    value desc limit 1;
| -> Limit: 1 row(s)  (cost=1408498.03 rows=1)
    -> Gather (merge sort; slice: 1; workers: 256; actual workers: 32)  (cost=1408498.03 rows=256)
        -> Limit: 1 row(s)  (cost=1408404.20 rows=1)
            -> Sort: <temporary>.value DESC, limit input to 1 row(s) per chunk  (cost=1408404.20 rows=803182)
                -> Filter: (sum((partsupp.PS_SUPPLYCOST * partsupp.PS_AVAILQTY)) > (select #2))
                    -> Table scan on <temporary>
                        -> Aggregate using temporary table  (cost=1408404.20 rows=803182)
                            -> Inner hash join (partsupp.PS_SUPPKEY = supplier.S_SUPPKEY)  (cost=829770.18 rows=327820)
                                -> Parallel table scan on partsupp, with parallel partitions: 2882, partition_keys: 1  (cost=6347528.15 rows=3176912)
                                -> Hash
                                    -> Broadcast (slice: 2; workers: 256; nodes: 16)  (cost=103382.56 rows=1029632)
                                        -> Nested loop inner join  (cost=409.36 rows=4022)
                                            -> Filter: (nation.N_NAME = 'IRAN')  (cost=2.29 rows=3)
                                                -> Table scan on nation  (cost=2.29 rows=25)
                                            -> Parallel index lookup on supplier using SUPPLIER_FK1 (S_NATIONKEY=nation.N_NATIONKEY), with parallel partitions: 9243  (cost=65.94 rows=1609)
                    -> Select #2 (subquery in condition; run only once; shared access)
                        -> Aggregate: sum(`<collector>`.tmp_field_0)  (cost=825576.85 rows=1)
                            -> Gather (slice: 1; workers: 256; nodes: 16)  (cost=825564.05 rows=256)
                                -> Aggregate: sum((partsupp.PS_SUPPLYCOST * partsupp.PS_AVAILQTY))  (cost=825541.20 rows=1)
                                    -> Inner hash join (partsupp.PS_SUPPKEY = supplier.S_SUPPKEY)  (cost=809150.20 rows=327820)
                                        -> Parallel table scan on partsupp, with parallel partitions: 14405  (cost=6147699.35 rows=3176912)
                                        -> Hash
                                            -> Broadcast (slice: 2; workers: 256; nodes: 16)  (cost=103382.56 rows=1029632)
                                                -> Nested loop inner join  (cost=409.36 rows=4022)
                                                    -> Filter: (nation.N_NAME = 'IRAN')  (cost=2.29 rows=3)
                                                        -> Table scan on nation  (cost=2.29 rows=25)
                                                    -> Parallel index lookup on supplier using SUPPLIER_FK1 (S_NATIONKEY=nation.N_NATIONKEY), with parallel partitions: 9243  (cost=65.94 rows=1609)

Pontos principais neste plano:

  • Gather (merge sort; slice: 1; workers: 256; actual workers: 32) — o modificador merge sort indica que o nó Gather mescla resultados pré-ordenados dos workers, evitando uma reordenação completa no nível superior. workers: 256 é o grau de paralelismo configurado; actual workers: 32 é a quantidade que realmente executou a tarefa.

  • Select #2 (subquery in condition; run only once; shared access) — a subconsulta do HAVING é executada uma vez em paralelo e o resultado é compartilhado entre todos os workers externos. Trata-se da otimização Shared access.

  • Broadcast (slice: 2; workers: 256; nodes: 16) — envia o resultado da junção de supplier e nation para todos os 16 nós, permitindo que cada nó realize o hash join localmente sem movimentação de dados entre nós.

  • O bloco de consulta externa não pode usar consulta paralela elástica multinó devido a restrições de política. Os workers da consulta externa executam consulta paralela elástica de nó único no nó onde a consulta foi entregue.

Visualize tempo de execução por worker com EXPLAIN ANALYZE

Disponível no PolarDB for MySQL 8.0.2, o comando EXPLAIN ANALYZE adiciona tempos reais ao lado do custo estimado. Para consultas paralelas, ele registra o tempo de execução de cada worker por operador, incluindo os valores máximo, mínimo e médio entre todos os workers.

EXPLAIN ANALYZE select
    c_name,
    c_custkey,
    o_orderkey,
    o_orderdate,
    o_totalprice,
    sum(l_quantity)
from
    customer,
    orders,
    lineitem
where
    o_orderkey in (
        select
            l_orderkey
        from
            lineitem
        group by
            l_orderkey having
                sum(l_quantity) > 313
    )
    and c_custkey = o_custkey
    and o_orderkey = l_orderkey
group by
    c_name,
    c_custkey,
    o_orderkey,
    o_orderdate,
    o_totalprice
order by
    o_totalprice desc,
    o_orderdate
LIMIT 100;
| -> Limit: 100 row(s)  (cost=14.62 rows=1) (actual time=1.252..1.252 rows=0 loops=1)
    -> Gather (merge sort; slice: 1; workers: 1; nodes: 2)  (cost=14.62 rows=1) (actual time=1.250..1.250 rows=0 loops=1)
        -> Limit: 100 row(s)  (cost=4.52 rows=1) (actual time=0.084,0.084,0.084..0.084,0.084,0.084 rows=0,0,0 loops=1,1,1)
            -> Sort: <temporary>.O_TOTALPRICE DESC, <temporary>.O_ORDERDATE, limit input to 100 row(s) per chunk  (cost=4.52 rows=1) (actual time=0.083,0.083,0.083..0.083,0.083,0.083 rows=0,0,0 loops=1,1,1)
                -> Table scan on <temporary> (actual time=0.070,0.070,0.070..0.070,0.070,0.070 rows=0,0,0 loops=1,1,1)
                    -> Aggregate using temporary table  (cost=4.52 rows=1) (actual time=0.001,0.001,0.001..0.001,0.001,0.001 rows=0,0,0 loops=1,1,1)
                        -> Nested loop inner join  (cost=2.86 rows=4) (actual time=0.039,0.039,0.039..0.039,0.039,0.039 rows=0,0,0 loops=1,1,1)
                            -> Nested loop inner join  (cost=1.45 rows=1) (actual time=0.037,0.037,0.037..0.037,0.037,0.037 rows=0,0,0 loops=1,1,1)
                                -> Parallel table scan on customer, with parallel partitions: 1, partition_keys: 1  (cost=0.35 rows=1) (actual time=0.036,0.036,0.036..0.036,0.036,0.036 rows=0,0,0 loops=1,1,1)
                                -> Filter: <in_optimizer>(orders.O_ORDERKEY,<exists>(select #2))  (cost=1.10 rows=1)
                                    -> Index lookup on orders using ORDERS_FK1 (O_CUSTKEY=customer.C_CUSTKEY)  (cost=1.10 rows=1)
                                    -> Select #2 (subquery in condition; dependent)
                                        -> Limit: 1 row(s)
                                            -> Filter: ((sum(lineitem.L_QUANTITY) > 313) and (<cache>(orders.O_ORDERKEY) = <ref_null_helper>(lineitem.L_ORDERKEY)))
                                                -> Group aggregate: sum(lineitem.L_QUANTITY)
                                                    -> Index scan on lineitem using PRIMARY  (cost=41554048.20 rows=380071042)
                            -> Index lookup on lineitem using PRIMARY (L_ORDERKEY=orders.O_ORDERKEY)  (cost=1.41 rows=4)

O formato de actual time varia dependendo se um operador é executado acima ou abaixo do nó Gather:

Localização do operador

Formato

Exemplo

Acima do Gather (coordenador, serial)

actual time=start..end rows=N loops=N

actual time=1.250..1.250 rows=0 loops=1

Abaixo do Gather (workers, paralelo)

actual time=v1,v2,v3..v1,v2,v3 rows=N1,N2,N3 loops=N1,N2,N3

actual time=0.084,0.084,0.084..0.084,0.084,0.084 rows=0,0,0 loops=1,1,1

Para operadores dentro da subárvore paralela, cada valor separado por vírgula corresponde a um worker. Isso permite identificar qual operador é o gargalo entre os workers e se o tempo de execução está distribuído uniformemente.