Les instructions EXPLAIN permettent de consulter le plan d'exécution de toute requête SQL dans ApsaraDB for SelectDB. Trois variantes sont disponibles, chacune offrant un niveau de détail différent :
Astuce :EXPLAIN GRAPH<EXPLAIN<EXPLAIN VERBOSE— chaque variante retourne plus de détails que la précédente.
| Variante | Sortie |
|---|---|
EXPLAIN GRAPH SELECT ou DESC GRAPH SELECT |
Représentation graphique de l'arborescence du plan d'exécution et de ses fragments |
EXPLAIN SELECT |
Plan au format texte avec des détails par nœud, tels que les conditions de filtre descendues et les statistiques de scan |
EXPLAIN VERBOSE SELECT |
Détail complet : tout le contenu d'EXPLAIN SELECT ainsi que les descripteurs de tuple, les descripteurs de slot et les affectations de filtres d'exécution |
EXPLAIN compile l'instruction SQL sans l'exécuter. Cette opération ne consomme aucune ressource de calcul et ne retourne aucun résultat de requête.
Fonctionnement
SQL est un langage déclaratif qui décrit quelles données récupérer, et non comment les récupérer. Le planificateur de requêtes détermine la méthode d'exécution, notamment l'algorithme de jointure à utiliser (Hash Join, Sort Merge Join, Shuffle Join ou Broadcast Join), l'ordre optimal des jointures pour éviter un produit cartésien, ainsi que les nœuds chargés d'exécuter chaque opération.
Le planificateur de requêtes génère d'abord une arborescence de plan d'exécution autonome, puis la convertit en un plan d'exécution distribué composé de plusieurs fragments de plan. Chaque fragment traite une partie du plan. Les données transitent entre les fragments via l'opérateur ExchangeNode. Chaque fragment se divise ensuite en plusieurs instances qui s'exécutent en parallèle afin de maximiser l'utilisation des ressources et la concurrence des requêtes.
Le diagramme suivant illustre une arborescence de plan d'exécution autonome simple :
┌────┐
│Sort│
└────┘
│
┌───────────┐
│Aggregation│
└───────────┘
│
┌────┐
│Join│
└────┘
┌───┴────┐
┌──────┐ ┌──────┐
│Scan-1│ │Scan-2│
└──────┘ └──────┘
Une fois cette arborescence convertie en plan distribué, le planificateur de requêtes la divise en fragments. Le diagramme suivant présente le même plan scindé en deux fragments (F1 et F2), reliés par un ExchangeNode pour le transfert des données :
┌────┐
│Sort│
│F1 │
└────┘
│
┌───────────┐
│Aggregation│
│F1 │
└───────────┘
│
┌────┐
│Join│
│F1 │
└────┘
┌──────┴────┐
┌──────┐ ┌────────────┐
│Scan-1│ │ExchangeNode│
│F1 │ │F1 │
└──────┘ └────────────┘
│
┌──────────────┐
│DataStreamSink│
│F2 │
└──────────────┘
│
┌──────┐
│Scan-2│
│F2 │
└──────┘
EXPLAIN GRAPH SELECT ou DESC GRAPH SELECT
EXPLAIN GRAPH SELECT et DESC GRAPH SELECT sont équivalents. Tous deux affichent le plan d'exécution sous forme d'arborescence visuelle, ce qui facilite l'identification du fragment auquel appartient chaque nœud et la manière dont les fragments sont connectés entre eux.
EXPLAIN GRAPH SELECT tbl1.k1, SUM(tbl1.k2)
FROM tbl1 JOIN tbl2 ON tbl1.k1 = tbl2.k1
GROUP BY tbl1.k1
ORDER BY tbl1.k1;
Sortie :
+---------------------------------------------------------------------------------------------------------------------------------+
| Explain String |
+---------------------------------------------------------------------------------------------------------------------------------+
| |
| ┌───────────────┐ |
| │[9: ResultSink]│ |
| │[Fragment: 4] │ |
| │RESULT SINK │ |
| └───────────────┘ |
| │ |
| ┌─────────────────────┐ |
| │[9: MERGING-EXCHANGE]│ |
| │[Fragment: 4] │ |
| └─────────────────────┘ |
| │ |
| ┌───────────────────┐ |
| │[9: DataStreamSink]│ |
| │[Fragment: 3] │ |
| │STREAM DATA SINK │ |
| │ EXCHANGE ID: 09 │ |
| │ UNPARTITIONED │ |
| └───────────────────┘ |
| │ |
| ┌─────────────┐ |
| │[4: TOP-N] │ |
| │[Fragment: 3]│ |
| └─────────────┘ |
| │ |
| ┌───────────────────────────────┐ |
| │[8: AGGREGATE (merge finalize)]│ |
| │[Fragment: 3] │ |
| └───────────────────────────────┘ |
| │ |
| ┌─────────────┐ |
| │[7: EXCHANGE]│ |
| │[Fragment: 3]│ |
| └─────────────┘ |
| │ |
| ┌───────────────────┐ |
| │[7: DataStreamSink]│ |
| │[Fragment: 2] │ |
| │STREAM DATA SINK │ |
| │ EXCHANGE ID: 07 │ |
| │ HASH_PARTITIONED │ |
| └───────────────────┘ |
| │ |
| ┌─────────────────────────────────┐ |
| │[3: AGGREGATE (update serialize)]│ |
| │[Fragment: 2] │ |
| │STREAMING │ |
| └─────────────────────────────────┘ |
| │ |
| ┌─────────────────────────────────┐ |
| │[2: HASH JOIN] │ |
| │[Fragment: 2] │ |
| │join op: INNER JOIN (PARTITIONED)│ |
| └─────────────────────────────────┘ |
| ┌──────────┴──────────┐ |
| ┌─────────────┐ ┌─────────────┐ |
| │[5: EXCHANGE]│ │[6: EXCHANGE]│ |
| │[Fragment: 2]│ │[Fragment: 2]│ |
| └─────────────┘ └─────────────┘ |
| │ │ |
| ┌───────────────────┐ ┌───────────────────┐ |
| │[5: DataStreamSink]│ │[6: DataStreamSink]│ |
| │[Fragment: 0] │ │[Fragment: 1] │ |
| │STREAM DATA SINK │ │STREAM DATA SINK │ |
| │ EXCHANGE ID: 05 │ │ EXCHANGE ID: 06 │ |
| │ HASH_PARTITIONED │ │ HASH_PARTITIONED │ |
| └───────────────────┘ └───────────────────┘ |
| │ │ |
| ┌─────────────────┐ ┌─────────────────┐ |
| │[0: OlapScanNode]│ │[1: OlapScanNode]│ |
| │[Fragment: 0] │ │[Fragment: 1] │ |
| │TABLE: tbl1 │ │TABLE: tbl2 │ |
| └─────────────────┘ └─────────────────┘ |
+---------------------------------------------------------------------------------------------------------------------------------+
Ce plan se divise en cinq fragments (du Fragment 0 au Fragment 4). L'étiquette [Fragment: N] sur chaque nœud indique son fragment d'appartenance. Les nœuds DataStreamSink et ExchangeNode gèrent la transmission des données entre les fragments.
EXPLAIN SELECT
EXPLAIN SELECT retourne un plan au format texte contenant des détails par nœud invisibles dans la vue graphique, notamment les conditions de filtre descendues et les statistiques par scan.
EXPLAIN SELECT tbl1.k1, sum(tbl1.k2)
FROM tbl1 JOIN tbl2 ON tbl1.k1 = tbl2.k1
GROUP BY tbl1.k1
ORDER BY tbl1.k1;
Sortie :
+----------------------------------------------------------------------------------+
| EXPLAIN String |
+----------------------------------------------------------------------------------+
| PLAN FRAGMENT 0 |
| OUTPUT EXPRS:<slot 5> <slot 3> `tbl1`.`k1` | <slot 6> <slot 4> sum(`tbl1`.`k2`) |
| PARTITION: UNPARTITIONED |
| |
| RESULT SINK |
| |
| 9:MERGING-EXCHANGE |
| limit: 65535 |
| |
| PLAN FRAGMENT 1 |
| OUTPUT EXPRS: |
| PARTITION: HASH_PARTITIONED: <slot 3> `tbl1`.`k1` |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 09 |
| UNPARTITIONED |
| |
| 4:TOP-N |
| | order by: <slot 5> <slot 3> `tbl1`.`k1` ASC |
| | offset: 0 |
| | limit: 65535 |
| | |
| 8:AGGREGATE (merge finalize) |
| | output: sum(<slot 4> sum(`tbl1`.`k2`)) |
| | group by: <slot 3> `tbl1`.`k1` |
| | cardinality=-1 |
| | |
| 7:EXCHANGE |
| |
| PLAN FRAGMENT 2 |
| OUTPUT EXPRS: |
| PARTITION: HASH_PARTITIONED: `tbl1`.`k1` |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 07 |
| HASH_PARTITIONED: <slot 3> `tbl1`.`k1` |
| |
| 3:AGGREGATE (update serialize) |
| | STREAMING |
| | output: sum(`tbl1`.`k2`) |
| | group by: `tbl1`.`k1` |
| | cardinality=-1 |
| | |
| 2:HASH JOIN |
| | join op: INNER JOIN (PARTITIONED) |
| | runtime filter: false |
| | hash predicates: |
| | colocate: false, reason: table not in the same group |
| | equal join conjunct: `tbl1`.`k1` = `tbl2`.`k1` |
| | cardinality=2 |
| | |
| |----6:EXCHANGE |
| | |
| 5:EXCHANGE |
| |
| PLAN FRAGMENT 3 |
| OUTPUT EXPRS: |
| PARTITION: RANDOM |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 06 |
| HASH_PARTITIONED: `tbl2`.`k1` |
| |
| 1:OlapScanNode |
| TABLE: tbl2 |
| PREAGGREGATION: ON |
| partitions=1/1 |
| rollup: tbl2 |
| tabletRatio=3/3 |
| tabletList=105104776,105104780,105104784 |
| cardinality=1 |
| avgRowSize=4.0 |
| numNodes=6 |
| |
| PLAN FRAGMENT 4 |
| OUTPUT EXPRS: |
| PARTITION: RANDOM |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 05 |
| HASH_PARTITIONED: `tbl1`.`k1` |
| |
| 0:OlapScanNode |
| TABLE: tbl1 |
| PREAGGREGATION: ON |
| partitions=1/1 |
| rollup: tbl1 |
| tabletRatio=3/3 |
| tabletList=105104752,105104763,105104767 |
| cardinality=2 |
| avgRowSize=8.0 |
| numNodes=6 |
+----------------------------------------------------------------------------------+
Champs de sortie
Les champs suivants apparaissent dans la sortie d'EXPLAIN SELECT :
| Champ | Description |
|---|---|
PARTITION |
Stratégie de partitionnement des données pour le fragment : RANDOM, HASH_PARTITIONED ou UNPARTITIONED |
EXCHANGE ID |
Identifiant reliant un DataStreamSink à son ExchangeNode correspondant |
cardinality |
Nombre estimé de lignes pour le nœud. La valeur -1 indique que les statistiques sont indisponibles |
avgRowSize |
Taille moyenne d'une ligne scannée, en octets |
numNodes |
Nombre de nœuds exécutant le scan |
tabletRatio |
Ratio de tablets scannés par rapport au total (scanned/total) |
tabletList |
Liste séparée par des virgules des ID de tablets à scanner |
PREAGGREGATION |
Indique si la préagrégation au niveau du stockage est activée (ON ou OFF) |
rollup |
Index rollup utilisé pour le scan |
colocate |
Indique si la jointure utilise le mode colocate. Si la valeur est false, la raison est affichée |
runtime filter |
Indique si un filtre d'exécution est appliqué à cette jointure |
EXPLAIN VERBOSE SELECT
EXPLAIN VERBOSE SELECT fournit la sortie la plus détaillée. En plus de toutes les informations d'EXPLAIN SELECT, elle inclut les descripteurs de tuple, les descripteurs de slot et les affectations de filtres d'exécution. Les opérateurs utilisent le préfixe V (par exemple, VHASH JOIN, VOlapScanNode) pour signaler l'utilisation du moteur d'exécution vectorisé.
EXPLAIN VERBOSE SELECT tbl1.k1, sum(tbl1.k2)
FROM tbl1 JOIN tbl2 ON tbl1.k1 = tbl2.k1
GROUP BY tbl1.k1
ORDER BY tbl1.k1;
Sortie :
+---------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN String |
+---------------------------------------------------------------------------------------------------------------------------------------------------------+
| PLAN FRAGMENT 0 |
| OUTPUT EXPRS:<slot 5> <slot 3> `tbl1`.`k1` | <slot 6> <slot 4> sum(`tbl1`.`k2`) |
| PARTITION: UNPARTITIONED |
| |
| VRESULT SINK |
| |
| 6:VMERGING-EXCHANGE |
| limit: 65535 |
| tuple ids: 3 |
| |
| PLAN FRAGMENT 1 |
| |
| PARTITION: HASH_PARTITIONED: `default_cluster:test`.`tbl1`.`k2` |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 06 |
| UNPARTITIONED |
| |
| 4:VTOP-N |
| | order by: <slot 5> <slot 3> `tbl1`.`k1` ASC |
| | offset: 0 |
| | limit: 65535 |
| | tuple ids: 3 |
| | |
| 3:VAGGREGATE (update finalize) |
| | output: sum(<slot 8>) |
| | group by: <slot 7> |
| | cardinality=-1 |
| | tuple ids: 2 |
| | |
| 2:VHASH JOIN |
| | join op: INNER JOIN(BROADCAST)[Tables are not in the same group] |
| | equal join conjunct: CAST(`tbl1`.`k1` AS DATETIME) = `tbl2`.`k1` |
| | runtime filters: RF000[in_or_bloom] <- `tbl2`.`k1` |
| | cardinality=0 |
| | vec output tuple id: 4 | tuple ids: 0 1 |
| | |
| |----5:VEXCHANGE |
| | tuple ids: 1 |
| | |
| 0:VOlapScanNode |
| TABLE: tbl1(null), PREAGGREGATION: OFF. Reason: the type of agg on StorageEngine's Key column should only be MAX or MIN.agg expr: sum(`tbl1`.`k2`) |
| runtime filters: RF000[in_or_bloom] -> CAST(`tbl1`.`k1` AS DATETIME) |
| partitions=0/1, tablets=0/0, tabletList= |
| cardinality=0, avgRowSize=20.0, numNodes=1 |
| tuple ids: 0 |
| |
| PLAN FRAGMENT 2 |
| |
| PARTITION: HASH_PARTITIONED: `default_cluster:test`.`tbl2`.`k2` |
| |
| STREAM DATA SINK |
| EXCHANGE ID: 05 |
| UNPARTITIONED |
| |
| 1:VOlapScanNode |
| TABLE: tbl2(null), PREAGGREGATION: OFF. Reason: null |
| partitions=0/1, tablets=0/0, tabletList= |
| cardinality=0, avgRowSize=16.0, numNodes=1 |
| tuple ids: 1 |
| |
| Tuples: |
| TupleDescriptor{id=0, tbl=tbl1, byteSize=32, materialized=true} |
| SlotDescriptor{id=0, col=k1, type=DATE} |
| parent=0 |
| materialized=true |
| byteSize=16 |
| byteOffset=16 |
| nullIndicatorByte=0 |
| nullIndicatorBit=-1 |
| slotIdx=1 |
| |
| SlotDescriptor{id=2, col=k2, type=INT} |
| parent=0 |
| materialized=true |
| byteSize=4 |
| byteOffset=0 |
| nullIndicatorByte=0 |
| nullIndicatorBit=-1 |
| slotIdx=0 |
| |
| |
| TupleDescriptor{id=1, tbl=tbl2, byteSize=16, materialized=true} |
| SlotDescriptor{id=1, col=k1, type=DATETIME} |
| parent=1 |
| materialized=true |
| byteSize=16 |
| byteOffset=0 |
| nullIndicatorByte=0 |
| nullIndicatorBit=-1 |
| slotIdx=0 |
| |
| |
| TupleDescriptor{id=2, tbl=null, byteSize=32, materialized=true} |
| SlotDescriptor{id=3, col=null, type=DATE} |
| parent=2 |
| materialized=true |
| byteSize=16 |
| byteOffset=16 |
| nullIndicatorByte=0 |
| nullIndicatorBit=-1 |
| slotIdx=1 |
| |
| SlotDescriptor{id=4, col=null, type=BIGINT} |
| parent=2 |
| materialized=true |
| byteSize=8 |
| byteOffset=0 |
| nullIndicatorByte=0 |
| nullIndicatorBit=-1 |
| slotIdx=0 |
| |
| |
| TupleDescriptor{id=3, tbl=null, byteSize=32, materialized=true} |
| SlotDescriptor{id=5, col=null, type=DATE} |
| parent=3 |
| materialized=true |
| byteSize=16 |
| byteOffset=16 |
| nullIndicatorByte=0 |
| nullIndicatorBit=-1 |
| slotIdx=1 |
| |
| SlotDescriptor{id=6, col=null, type=BIGINT} |
| parent=3 |
| materialized=true |
| byteSize=8 |
| byteOffset=0 |
| nullIndicatorByte=0 |
| nullIndicatorBit=-1 |
| slotIdx=0 |
| |
| |
| TupleDescriptor{id=4, tbl=null, byteSize=48, materialized=true} |
| SlotDescriptor{id=7, col=k1, type=DATE} |
| parent=4 |
| materialized=true |
| byteSize=16 |
| byteOffset=16 |
| nullIndicatorByte=0 |
| nullIndicatorBit=-1 |
| slotIdx=1 |
| |
| SlotDescriptor{id=8, col=k2, type=INT} |
| parent=4 |
| materialized=true |
| byteSize=4 |
| byteOffset=0 |
| nullIndicatorByte=0 |
| nullIndicatorBit=-1 |
| slotIdx=0 |
| |
| SlotDescriptor{id=9, col=k1, type=DATETIME} |
| parent=4 |
| materialized=true |
| byteSize=16 |
| byteOffset=32 |
| nullIndicatorByte=0 |
| nullIndicatorBit=-1 |
| slotIdx=2 |
+---------------------------------------------------------------------------------------------------------------------------------------------------------+
160 rows in set (0.00 sec)
La section Tuples située à la fin répertorie chaque TupleDescriptor ainsi que ses entrées SlotDescriptor. Chaque descripteur de slot décrit une colonne ou une expression du plan, en précisant son type de données, sa taille en octets, son décalage mémoire et la position de son indicateur de valeur nulle.
Champs de sortie supplémentaires dans EXPLAIN VERBOSE
| Champ | Description |
|---|---|
tuple ids |
ID des tuples produits ou consommés par un nœud |
vec output tuple id |
ID du tuple de sortie vectorisée produit par un nœud de jointure |
runtime filters |
Affectations des filtres d'exécution : <- indique où un filtre est construit ; -> indique où il est appliqué |
TupleDescriptor |
Décrit la structure d'une ligne (table, taille totale en octets, état de matérialisation) |
SlotDescriptor |
Décrit une colonne ou une expression au sein d'un tuple (type de données, taille en octets, décalage mémoire, indicateur de valeur nulle) |
Remarques d'utilisation
EXPLAIN affiche le plan d'exécution logique. L'ordre d'exécution réel peut différer en raison d'optimisations telles que l'élagage par filtre d'exécution.
Les valeurs de
cardinalitysont des estimations basées sur les statistiques des tables. Si ces statistiques sont obsolètes ou absentes, la mentioncardinality=-1s'affiche.Les valeurs
tabletRatioettabletListdansEXPLAIN SELECTcorrespondent à des estimations maximales. Des optimisations d'exécution, comme l'élagage de partitions, peuvent réduire le nombre de tablets effectivement scannés.Dans la sortie d'
EXPLAIN VERBOSE, les opérateurs préfixés parV(VHASH JOIN,VOlapScanNode, etc.) signalent l'utilisation du moteur d'exécution vectorisé. À l'inverse, les opérateurs sans préfixe dans la sortie d'EXPLAIN SELECTindiquent le moteur non vectorisé.