Tous les produits
Search
Centre de documentation

PolarDB:Exécuter l'instruction EXPLAIN pour consulter les plans d'exécution des requêtes parallèles élastiques

Dernière mise à jour :Aug 11, 2026

Utilisez l'instruction EXPLAIN pour inspecter les plans d'exécution et vérifier si une requête s'exécute en parallèle sur PolarDB for MySQL.

Fonctionnement

Lorsque la requête parallèle élastique est active, PolarDB divise une requête en sous-tâches et les répartit entre plusieurs workers sur un ou plusieurs nœuds. Le plan d'exécution reflète ce comportement en introduisant des opérateurs spécifiques au parallélisme, en plus des champs de sortie EXPLAIN standard.

Champs de sortie EXPLAIN standard :

Champ Description
Id Identifiant de l'étape dans le plan d'exécution
Select_type Type de SELECT (par exemple, SIMPLE, PRIMARY, DEPENDENT SUBQUERY)
Table Table accessible par cette étape, ou nœud virtuel tel que <gather2>
Type Type de jointure ou d'accès (par exemple, index, range, ALL)
Key Index utilisé
Rows Nombre estimé de lignes examinées
Filtered Pourcentage estimé de lignes satisfaisant la condition de filtre
Extra Informations d'exécution supplémentaires, y compris les opérateurs parallèles

Opérateurs spécifiques au parallélisme :

Opérateur Emplacement Fonction
Gather Niveau supérieur ou sous-arbre Collecte les résultats partiels de tous les workers et les fusionne en un seul jeu de résultats
Parallel scan (N workers) Champ Extra Indique une analyse parallèle de table ou d'index avec N workers
Parallel pushdown Champ Extra Délègue une sous-requête dépendante aux workers pour une exécution parallèle
Broadcast Sortie EXPLAIN FORMAT=TREE Envoie une copie d'un jeu de données à tous les workers
Shared access Sortie EXPLAIN FORMAT=TREE Exécute une sous-requête parallèle une seule fois et partage le résultat entre les workers

Le nœud Gather dans l'arborescence du plan marque la frontière entre l'exécution parallèle et sérielle. Tout ce qui se trouve sous un nœud Gather s'exécute en parallèle ; tout ce qui se trouve au-dessus s'exécute de manière sérielle sur le coordinateur.

Table d'exemple

Tous les exemples de cette rubrique utilisent la table pq_test.

Schéma :

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)

Taille de la table :

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)

Requête de test :

SELECT COUNT(*) FROM pq_test;

Consulter un plan de requête non parallèle

Définissez max_parallel_degree sur 0 pour désactiver la requête parallèle, puis exécutez 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)

Le plan affiche une analyse d'index standard. Aucun nœud Gather n'est présent et le champ Extra ne contient aucun marqueur Parallel scan, ce qui confirme que la requête s'exécute de manière sérielle.

Consulter un plan de requête parallèle

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)

Deux lignes apparaissent au lieu d'une :

  • La ligne 1 (<gather2>) correspond à l'opération Gather qui collecte et fusionne les résultats de tous les workers. Cette ligne marque la limite du parallélisme : tout ce qui se trouve en dessous s'exécute en parallèle.

  • La ligne 2 montre l'analyse effective sur pq_test avec Parallel scan (2 workers) dans le champ Extra, confirmant que deux workers exécutent l'analyse d'index en parallèle.

PolarDB for MySQL 8.0.2 : EXPLAIN FORMAT=TREE

EXPLAIN FORMAT=TREE retourne un plan arborescent offrant des détails plus riches sur la structure d'exécution parallèle :

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)

Lecture de haut en bas :

  • Count rows in gather : le coordinateur agrège le décompte final à partir des résultats des workers.

  • Gather (slice: 1; workers: 2, nodes: 2) : deux workers s'exécutent sur deux nœuds. Il s'agit de la limite du parallélisme.

  • Parallel index scan on pq_test using PRIMARY, with parallel partitions: 8 : l'index est divisé en huit partitions analysées en parallèle.

Consulter un plan de requête parallèle avec sous-requêtes

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)

La ligne 3 présente select_type: DEPENDENT SUBQUERY avec Extra: Parallel pushdown. Cela signifie que la sous-requête est déléguée à chaque worker pour une exécution parallèle, plutôt que d'être évaluée une seule fois sur le coordinateur.

PolarDB for MySQL 8.0.2 : EXPLAIN FORMAT=TREE

Exemple 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)

Le nœud Gather (slice: 1; workers: 2) sépare le sous-arbre parallèle (en dessous) de l'agrégation et du tri sériels (au-dessus). La sous-requête (Select #2) est marquée comme dependent, ce qui signifie qu'elle s'exécute dans chaque worker en tant que Parallel pushdown.

Exemple 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)

Points clés de ce plan :

  • Gather (merge sort; slice: 1; workers: 256; actual workers: 32) : le modificateur merge sort indique que le nœud Gather fusionne des résultats pré-triés provenant des workers, évitant ainsi un tri complet au niveau supérieur. workers: 256 correspond au degré de parallélisme configuré ; actual workers: 32 est le nombre de workers effectivement exécutés.

  • Select #2 (subquery in condition; run only once; shared access) : la sous-requête HAVING s'exécute une seule fois en parallèle et le résultat est partagé entre tous les workers externes. Il s'agit de l'optimisation Shared access.

  • Broadcast (slice: 2; workers: 256; nodes: 16) : envoie le résultat de la jointure entre supplier et nation aux 16 nœuds afin que chacun puisse effectuer la jointure par hachage localement, sans transfert de données entre nœuds.

  • Le bloc de requête externe ne peut pas utiliser la requête parallèle élastique multi-nœuds en raison de restrictions de politique. Les workers de la requête externe exécutent une requête parallèle élastique mono-nœud sur le nœud où la requête est distribuée.

Consulter le temps d'exécution par worker avec EXPLAIN ANALYZE

Disponible dans PolarDB for MySQL 8.0.2, EXPLAIN ANALYZE ajoute les chronométrages réels au coût estimé. Pour les requêtes parallèles, cette commande enregistre le temps d'exécution de chaque worker par opérateur, y compris les valeurs maximale, minimale et moyenne sur l'ensemble des 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)

Le format actual time diffère selon que l'opérateur s'exécute au-dessus ou en dessous du nœud Gather :

Emplacement de l'opérateur Format Exemple
Au-dessus de Gather (coordinateur, sériel) actual time=start..end rows=N loops=N actual time=1.250..1.250 rows=0 loops=1
En dessous de Gather (workers, parallèle) 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

Pour les opérateurs situés dans le sous-arbre parallèle, chaque valeur séparée par une virgule correspond à un worker. Cela permet d'identifier quel opérateur constitue le goulot d'étranglement parmi les workers et de vérifier si le temps d'exécution est uniformément réparti.