Affiche le plan d’exécution d’une instruction.
Syntaxe :
EXPLAIN [AST | SYNTAX | QUERY TREE | PLAN | PIPELINE | ANALYZE | ESTIMATE | TABLE OVERRIDE | WHATIF] [setting = value, ...]
[
SELECT ... |
tableFunction(...) [COLUMNS (...)] [ORDER BY ...] [PARTITION BY ...] [PRIMARY KEY] [SAMPLE BY ...] [TTL ...]
]
[FORMAT ...]Exemple :
EXPLAIN SELECT sum(number) FROM numbers(10) UNION ALL SELECT sum(number) FROM numbers(10) ORDER BY sum(number) ASC FORMAT TSV;Output: sum(number)
Union
├Types d’EXPLAIN
AST— Arbre syntaxique abstrait.SYNTAX— Texte de la requête après les optimisations au niveau de l’AST.QUERY TREE— Arbre de requête après les optimisations au niveau du Query Tree.PLAN— Plan d’exécution de la requête.PIPELINE— Pipeline d’exécution de la requête.ANALYZE— Exécute la requête et annote le plan d’exécution avec les runtime metrics mesurées.ESTIMATE— Nombre estimé de lignes, de marks et de parts à lire depuis les tables pendant le traitement de la requête.TABLE OVERRIDE— Résultat validé d’une surcharge de table sur le schéma d’une fonction de table.
EXPLAIN AST
Affiche l’AST de la requête. Prend en charge tous les types de requêtes, pas uniquement SELECT.
Paramètres :
graph– Affiche l’AST sous forme de graphe, décrit dans le langage de description de graphes DOT. Par défaut : 0.
Exemples :
EXPLAIN AST SELECT 1;SelectWithUnionQuery (children 1)
ExpressionList (children 1)
SelectQuery (children 1)
ExpressionList (children 1)
Literal UInt64_1EXPLAIN AST ALTER TABLE t1 DELETE WHERE date = today(); explain
AlterQuery t1 (children 1)
ExpressionList (children 1)
AlterCommand 27 (children 1)
Function equals (children 1)
ExpressionList (children 2)
Identifier date
Function today (children 1)
ExpressionListEXPLAIN SYNTAX
Affiche l'arbre syntaxique abstrait (AST) d'une requête après l'analyse syntaxique.
Cela consiste à analyser la requête, à construire l'AST et l'arbre de requête, à exécuter éventuellement l'analyseur de requêtes et les passes d'optimisation, puis à reconvertir l'arbre de requête en AST.
Paramètres :
oneline– Affiche la requête sur une seule ligne. Valeur par défaut :0.run_query_tree_passes– Exécute les passes de l'arbre de requête avant d'en produire le dump. Valeur par défaut :0.query_tree_passes– Sirun_query_tree_passesest défini, indique combien de passes exécuter. Siquery_tree_passesn'est pas spécifié, toutes les passes sont exécutées.single_record– Renvoie la requête reformatée sous la forme d'un seul enregistrement sur plusieurs lignes au lieu d'un enregistrement par ligne. Valeur par défaut :1(contrôlée par le paramètreexplain_syntax_single_record). Définissez cette valeur sur0pour restaurer la sortie historique avec un enregistrement par ligne, ou définissezexplain_syntax_single_record = 0(globalement ou dansSETTINGSpar requête), ou définissezcompatibilitysur toute version antérieure à26.8.
Exemples :
EXPLAIN SYNTAX SELECT * FROM system.numbers AS a, system.numbers AS b, system.numbers AS c WHERE a.number = b.number AND b.number = c.number;SELECT *
FROM system.numbers AS a, system.numbers AS b, system.numbers AS c
WHERE (a.number = b.number) AND (b.number = c.number)Avec run_query_tree_passes :
EXPLAIN SYNTAX run_query_tree_passes = 1 SELECT * FROM system.numbers AS a, system.numbers AS b, system.numbers AS c WHERE a.number = b.number AND b.number = c.number;SELECT
__table1.number AS `a.number`,
__table2.number AS `b.number`,
__table3.number AS `c.number`
FROM system.numbers AS __table1
ALL INNER JOIN system.numbers AS __table2 ON __table1.number = __table2.number
ALL INNER JOIN system.numbers AS __table3 ON __table2.number = __table3.numberEXPLAIN QUERY TREE
Paramètres :
run_passes— Exécute toutes les passes de l'arbre de requête avant d'en afficher le contenu. Valeur par défaut :1.dump_passes— Affiche des informations sur les passes utilisées avant d'afficher l'arbre de requête. Valeur par défaut :0.passes— Indique le nombre de passes à exécuter. S'il est défini sur-1, exécute toutes les passes. Valeur par défaut :-1.dump_tree— Affiche l'arbre de requête. Valeur par défaut :1.dump_ast— Affiche l'AST de la requête généré à partir de l'arbre de requête. Valeur par défaut :0.
Exemple :
EXPLAIN QUERY TREE SELECT id, value FROM test_table;QUERY id: 0
PROJECTION COLUMNS
id UInt64
value String
PROJECTION
LIST id: 1, nodes: 2
COLUMN id: 2, column_name: id, result_type: UInt64, source_id: 3
COLUMN id: 4, column_name: value, result_type: String, source_id: 3
JOIN TREE
TABLE id: 3, table_name: default.test_tableEXPLAIN PLAN
Affiche les étapes du plan de requête.
Paramètres :
optimize— Détermine si les optimisations du plan de requête sont appliquées avant l’affichage du plan. Par défaut : 1.header— Affiche l’en-tête de sortie pour l’étape. Par défaut : 0.description— Affiche la description de l’étape. Par défaut : 1.indexes— Affiche les index utilisés, le nombre de parts filtrées et le nombre de granules filtrés pour chaque index appliqué. Par défaut : 0. Pris en charge pour les tables MergeTree. À partir de ClickHouse >= v25.9, cette instruction ne produit un résultat pertinent que lorsqu’elle est utilisée avecSETTINGS use_query_condition_cache = 0, use_skip_indexes_on_data_read = 0.projections— Affiche toutes les projections analysées et leur effet sur le filtrage au niveau des parts en fonction des conditions sur la clé primaire de la projection. Pour chaque projection, cette section inclut des statistiques comme le nombre de parts, de lignes, de marks et de plages évaluées à l’aide de la clé primaire de la projection. Elle indique également combien de parts de données ont été ignorées grâce à ce filtrage, sans lecture depuis la projection elle-même. Le champdescriptionpermet de déterminer si une projection a réellement été utilisée pour la lecture ou seulement analysée pour le filtrage. Par défaut : 0. Pris en charge pour les tables MergeTree.actions— Affiche des informations détaillées sur les actions de l’étape. Par défaut : 1.sorting— Affiche la description du tri pour chaque étape du plan produisant une sortie triée. Par défaut : 0.keep_logical_steps— Conserve les étapes logiques du plan pour les jointures au lieu de les convertir en implémentations physiques de jointure. Par défaut : 0.json— Affiche les étapes du plan de requête sous la forme d’une ligne au format JSON. Par défaut : 0. Il est recommandé d’utiliser le format TabSeparatedRaw (TSVRaw) pour éviter les séquences d’échappement inutiles.input_headers— Affiche les en-têtes d’entrée pour l’étape. Par défaut : 0. Utile principalement aux développeurs pour le Débogage des problèmes liés à une incompatibilité entre les en-têtes d’entrée et de sortie.column_structure— Affiche également la structure des colonnes dans les en-têtes, en plus de leur nom et de leur type. Par défaut : 0. Utile principalement aux développeurs pour le Débogage des problèmes liés à une incompatibilité entre les en-têtes d’entrée et de sortie.distributed— Affiche les plans de requête exécutés sur des nœuds distants pour les tables distribuées ou les répliques parallèles. N’est pas pris en charge avecjson. Par défaut : 0.compact— Lorsqu’il est activé, masque dans le plan les étapes d’expression et les informations détaillées sur les actions (entrées, fonctions, alias et positions de sortie). N’a d’effet que lorsqueactions = 1. Par défaut : 1.pretty— Affiche l’arborescence du plan à l’aide de caractères de dessin de lignes (├──, └──, │) au lieu de l’indentation, afin de visualiser la hiérarchie. Formate également les propriétés des étapes de jointure de manière intégrée. Par défaut : 1.
Exemple :
EXPLAIN SELECT sum(number) FROM numbers(10) GROUP BY number % 4 LIMIT 1;Output: sum(number)
Limit (preliminary LIMIT)
│Lorsque json = 1, le plan de requête est représenté au format JSON. Chaque nœud est un dictionnaire qui possède toujours les clés Node Type, Node Id et Plans. Node Type est une chaîne contenant le nom de l’étape, et Node Id est un identifiant d’étape unique (le nom de l’étape avec un suffixe numérique, par ex. Union_10). Plans est un tableau contenant les descriptions des étapes enfants. D’autres clés facultatives peuvent être ajoutées selon le type de nœud et les paramètres.
Exemple :
EXPLAIN json = 1, description = 0 SELECT 1 UNION ALL SELECT 2 FORMAT TSVRaw;[
{
"Plan": {
"Node Type": "Union",
"Node Id": "Union_10",
"Plans": [
{
"Node Type": "Expression",
"Node Id": "Expression_13",
"Plans": [
{
"Node Type": "ReadFromStorage",
"Node Id": "ReadFromStorage_0"
}
]
},
{
"Node Type": "Expression",
"Node Id": "Expression_16",
"Plans": [
{
"Node Type": "ReadFromStorage",
"Node Id": "ReadFromStorage_4"
}
]
}
]
}
}
]Avec description = 1, la clé Description est ajoutée à l’étape :
{
"Node Type": "ReadFromStorage",
"Description": "SystemOne"
}Avec header = 1, la clé Header est ajoutée à l’étape sous forme d’un tableau de colonnes.
Exemple :
EXPLAIN json = 1, description = 0, header = 1 SELECT 1, 2 + dummy;[
{
"Plan": {
"Node Type": "Expression",
"Node Id": "Expression_5",
"Header": [
{
"Name": "1",
"Type": "UInt8"
},
{
"Name": "plus(2, dummy)",
"Type": "UInt16"
}
],
"Plans": [
{
"Node Type": "ReadFromStorage",
"Node Id": "ReadFromStorage_0",
"Header": [
{
"Name": "dummy",
"Type": "UInt8"
}
]
}
]
}
}
]Avec indexes = 1, la clé Indexes est ajoutée. Elle contient un tableau des index utilisés. Chaque index est décrit en JSON avec la clé Type (une chaîne Partition Min-Max, Partition, Statistics, PrimaryKey ou Skip) et, éventuellement, les clés suivantes :
Name— Le nom de l’index (actuellement utilisé uniquement pour les indexSkip).Keys— Le tableau des colonnes utilisées par l’index.Condition— La condition utilisée.Description— La description de l’index (actuellement utilisée uniquement pour les indexSkip).Parts— Le nombre de parts après/avant l’application de l’index.Granules— Le nombre de granules après/avant l’application de l’index.Ranges— Le nombre de plages de granules après l’application de l’index.
Exemple :
"Node Type": "ReadFromMergeTree",
"Indexes": [
{
"Type": "Partition Min-Max",
"Keys": ["y"],
"Condition": "(y in [1, +inf))",
"Parts": 4/5,
"Granules": 11/12
},
{
"Type": "Partition",
"Keys": ["y", "bitAnd(z, 3)"],
"Condition": "and((bitAnd(z, 3) not in [1, 1]), and((y in [1, +inf)), (bitAnd(z, 3) not in [1, 1])))",
"Parts": 3/4,
"Granules": 10/11
},
{
"Type": "PrimaryKey",
"Keys": ["x", "y"],
"Condition": "and((x in [11, +inf)), (y in [1, +inf)))",
"Parts": 2/3,
"Granules": 6/10,
"Search Algorithm": "generic exclusion search"
},
{
"Type": "Skip",
"Name": "t_minmax",
"Description": "minmax GRANULARITY 2",
"Parts": 1/2,
"Granules": 2/6
},
{
"Type": "Skip",
"Name": "t_set",
"Description": "set GRANULARITY 2",
"": 1/1,
"Granules": 1/2
}
]Avec projections = 1, la clé Projections est ajoutée. Elle contient un tableau de projections analysées. Chaque projection est décrite en JSON avec les clés suivantes :
Name— Le nom de la projection.Condition— La condition de clé primaire utilisée pour la projection.Description— La description de l’utilisation de la projection (par ex. filtrage au niveau des parties).Selected Parts— Nombre de parties sélectionnées par la projection.Selected Marks— Nombre de marques sélectionnées.Selected Ranges— Nombre de plages sélectionnées.Selected Rows— Nombre de lignes sélectionnées.Filtered Parts— Nombre de parties ignorées en raison du filtrage au niveau des parties.
Exemple :
"Node Type": "ReadFromMergeTree",
"Projections": [
{
"Name": "region_proj",
"Description": "Projection has been analyzed and is used for part-level filtering",
"Condition": "(region in ['us_west', 'us_west'])",
"Search Algorithm": "binary search",
"Selected Parts": 3,
"Selected Marks": 3,
"Selected Ranges": 3,
"Selected Rows": 3,
"Filtered Parts": 2
},
{
"Name": "user_id_proj",
"Description": "Projection has been analyzed and is used for part-level filtering",
"Condition": "(user_id in [107, 107])",
"Search Algorithm": "binary search",
"Selected Parts": 1,
"Selected Marks": 1,
"Selected Ranges": 1,
"Selected Rows": 1,
"Filtered Parts": 2
}
]Avec actions = 1, les clés ajoutées dépendent du type d'étape.
Exemple :
EXPLAIN json = 1, actions = 1, description = 0 SELECT 1 FORMAT TSVRaw;[
{
"Plan": {
"Node Type": "Expression",
"Node Id": "Expression_5",
"Expression": {
"Inputs": [
{
"Name": "dummy",
"Type": "UInt8"
}
],
"Actions": [
{
"Node Type": "INPUT",
"Result Type": "UInt8",
"Result Name": "dummy",
"Arguments": [0],
"Removed Arguments": [0],
"Result": 0
},
{
"Node Type": "COLUMN",
"Result Type": "UInt8",
"Result Name": "1",
"Column": "Const(UInt8)",
"Arguments": [],
"Removed Arguments": [],
"Result": 1
}
],
"Outputs": [
{
"Name": "1",
"Type": "UInt8"
}
],
"Positions": [1]
},
"Plans": [
{
"Node Type": "ReadFromStorage",
"Node Id": "ReadFromStorage_0"
}
]
}
}
]Avec compact = 0 et actions = 1, les étapes Expression sont visibles, accompagnées d'informations détaillées sur les expressions :
EXPLAIN actions = 1, compact = 0 SELECT sum(number) FROM numbers(10) GROUP BY number % 4;Output: sum(number)
Expression ((Project names + Projection))
│ Actions: INPUT : 0 -> sum(__table1.number) UInt64 : 0
│ INPUT :: 1 -> modulo(__table1.number, 4_UInt8) UInt8 : 1
│ ALIAS sum(__table1.number) :: 0 -> sum(number) UInt64 : 2
│ Positions: 2
└──Aggregating
│ Keys: number MOD 4
│ Aggregates: sum(number)
│ Skip merging: 0
└──Expression ((Before GROUP BY + Change column names to column identifiers))
│ Actions: INPUT : 0 -> number UInt64 : 0
│ COLUMN Const(UInt8) -> 4_UInt8 UInt8 : 1
│ ALIAS number :: 0 -> __table1.number UInt64 : 2
│ FUNCTION modulo(__table1.number : 2, 4_UInt8 :: 1) -> modulo(__table1.number, 4_UInt8) UInt8 : 0
│ Positions: 0 2
└──ReadFromSystemNumbers
Output: numberAvec distributed = 1, la sortie inclut non seulement le plan de requête local, mais également les plans de requête qui seront exécutés sur les nœuds distants. Cela s'avère utile pour analyser et déboguer les requêtes distribuées.
Exemple avec une table distribuée :
EXPLAIN distributed=1 SELECT * FROM remote('127.0.0.{1,2}', numbers(2)) WHERE number = 1;Union
Expression ((Project names + (Projection + (Change column names to column identifiers + (Project names + Projection)))))
Filter ((WHERE + Change column names to column identifiers))
ReadFromSystemNumbers
Expression ((Project names + (Projection + Change column names to column identifiers)))
ReadFromRemote (Read from remote replica)
Expression ((Project names + Projection))
Filter ((WHERE + Change column names to column identifiers))
ReadFromSystemNumbersExemple avec des répliques parallèles :
SET enable_parallel_replicas = 2, max_parallel_replicas = 2, cluster_for_parallel_replicas = 'default';
EXPLAIN distributed=1 SELECT sum(number) FROM test_table GROUP BY number % 4;Expression ((Project names + Projection))
MergingAggregated
Union
Aggregating
Expression ((Before GROUP BY + Change column names to column identifiers))
ReadFromMergeTree (default.test_table)
ReadFromRemoteParallelReplicas
BlocksMarshalling
Aggregating
Expression ((Before GROUP BY + Change column names to column identifiers))
ReadFromMergeTree (default.test_table)Dans les deux exemples, le plan de requête affiche le flux d'exécution complet, y compris les étapes locales et distantes.
Avec pretty = 1, l'arbre du plan est affiché à l'aide de caractères de tracé plutôt que par indentation, et des informations supplémentaires sont affichées pour les étapes clés :
- Les colonnes de sortie de la requête sont affichées en haut du plan.
- Les expressions dans les filtres, les clés d’agrégation, les descriptions de tri et les fonctions de fenêtre sont affichées dans une notation SQL lisible par l’humain (par ex.
a + 1 > 5au lieu degreater(plus(a, 1), 5)). Les préfixes internes des identifiants de colonnes (tels que__table1.) sont supprimés pour plus de clarté. - Les étapes source (telles que
ReadFromMergeTree) affichent leurs colonnes de sortie. - Les étapes de filtre affichent la condition de filtre en notation SQL. Lorsque des filtres de jointure à l’exécution sont présents, ils sont affichés séparément.
- Les étapes d’agrégation affichent les clés et les fonctions d’agrégation avec leurs arguments (par ex.
sum(c),count()). - Les ensembles IN issus de littéraux
Tupleaffichent leurs valeurs (tronquées pour les grands ensembles), les ensembles basés sur des sous-requêtes sont libelléssubquery1,subquery2, etc., et les ensembles provenant de tables utilisant le moteurSetaffichent le nom de la table. - Les étapes de jointure affichent la relation de jointure à l’aide d’une notation mathématique, le nombre estimé de lignes du résultat, et quelles colonnes de sortie proviennent du côté gauche ou du côté droit. Les symboles suivants sont utilisés pour représenter les différents types de JOIN :
| Symbole | Type de jointure |
|---|---|
⋈ |
Jointure interne |
⟕ |
Jointure gauche |
⟖ |
Jointure droite |
⟗ |
Jointure complète |
⋉ |
Semi-jointure gauche |
⋊ |
Semi-jointure droite |
⋉ with strikethrough |
Anti-jointure gauche |
⋊ with strikethrough |
Anti-jointure droite |
× |
Jointure croisée |
Par exemple, t1 ⟕ t2 signifie une jointure gauche entre les tables t1 et t2.
Le nombre entre crochets après le nom de la table (par ex. t1[100]) indique le nombre estimé de lignes
lorsque les statistiques de table sont disponibles.
L’option pretty fonctionne bien avec compact = 1, qui masque les étapes Expression et les informations détaillées sur les actions, ce qui rend le plan plus lisible.
Un exemple détaillé avec des jointures :
CREATE TABLE t1 (id UInt64, value String) ENGINE = MergeTree ORDER BY id;
CREATE TABLE t2 (id UInt64, value String) ENGINE = MergeTree ORDER BY id;
INSERT INTO t1 SELECT number, toString(number) FROM numbers(100);
INSERT INTO t2 SELECT number, toString(number) FROM numbers(100);
EXPLAIN actions = 1, compact = 1, pretty = 1
SELECT * FROM t1 INNER JOIN t2 ON t1.id = t2.id FORMAT Raw;Output: id, value, id, value
Join (JOIN FillRightFirst)
│ t1[100] ⋈ t2[100]
│ Type: inner | Strictness: all | Algorithm: SpillingHashJoin(HashJoin)
│ Result rows: 100
│ Join conditions: id = id
│ Output:
│ Left: id, value
│ Right: id, value
├──ReadFromMergeTree (default.t1)
│ Read type: Default
│ Parts: 1 | Granules: 1
│ Output: id, value
│ Runtime filters: RF1(id, id from default.t2)
└──BuildRuntimeFilter (Build runtime join filter on id)
│ Filter id: RF1
│ Source table: default.t2
└──ReadFromMergeTree (default.t2)
Read type: Default
Parts: 1 | Granules: 1
Output: id, valueEXPLAIN PIPELINE
Paramètres :
header— Affiche l’en-tête de chaque port de sortie. Valeur par défaut : 0.graph— Affiche un graphe décrit dans le langage de description de graphes DOT. Valeur par défaut : 0.compact— Affiche le graphe en mode compact si le paramètregraphest activé. Valeur par défaut : 1.compact_repeated_processor_chains— Regroupe les chaînes de processeurs répétées et adjacentes dans la sortie texte en n’affichant qu’une seule occurrence de la chaîne avec un nombre de répétitions. Cela peut faciliter la lecture des pipelines parallèles lorsque la même chaîne apparaît de nombreuses fois, par exemple dans des jointures. Cela n’affecte pas la sortie du graphe. Valeur par défaut : 0.
Resize 16 → 1
FillingRightJoinSide │
SimpleSquashingTransform │ × 16
Resize 1 → 16Lorsque compact=0 et graph=1, les noms des processeurs contiendront un suffixe supplémentaire indiquant un identifiant de processeur unique.
Exemple :
EXPLAIN PIPELINE SELECT sum(number) FROM numbers_mt(100000) GROUP BY number % 4;(Union)
(Expression)
ExpressionTransform
(Expression)
ExpressionTransform
(Aggregating)
Resize 2 →EXPLAIN ANALYZE
EXPLAIN ANALYZE exécute effectivement la requête, ignore les lignes de résultat et affiche la même arborescence du plan que EXPLAIN PLAN, avec pour chaque étape des annotations indiquant ce qui s’est réellement produit à l’exécution.
Paramètres :
EXPLAIN ANALYZE accepte les mêmes options d’affichage que EXPLAIN PLAN (documentées dans la section EXPLAIN PLAN).
header— voir la section EXPLAIN PLAN.description— voir la section EXPLAIN PLAN.projections— voir la section EXPLAIN PLAN.sorting— voir la section EXPLAIN PLAN.input_headers— voir la section EXPLAIN PLAN.column_structure— voir la section EXPLAIN PLAN.actions— voir la section EXPLAIN PLAN. Par défaut : 1.indexes— voir la section EXPLAIN PLAN. Par défaut : 1.compact— voir la section EXPLAIN PLAN. Par défaut : 1.pretty— voir la section EXPLAIN PLAN. Par défaut : 1.processors— PourEXPLAIN ANALYZE, affiche une ligne supplémentaire par étape avec la répartition du temps écoulé par processeur :min,median,maxetsum. Utile pour repérer un déséquilibre de charge entre les processeurs parallèles. Par défaut : 0.matches— PourEXPLAIN ANALYZE, permet aux étapes de jointure d’effectuer le suivi supplémentaire nécessaire aux métriquesmatched,match rateetfanoutlorsque ces valeurs ne peuvent pas être déduites de ce que produit la jointure. Lorsqu’elles le peuvent, elles sont signalées sans cette option. Voir Étapes de jointure. Par défaut : 0.
Exemple :
EXPLAIN ANALYZE SELECT number % 10 AS k, count() FROM numbers_mt(1000000) GROUP BY k;Query summary:
Time: 10.72 ms (planning 6.45 ms · execution 4.26 ms)
Read: 1.00 million rows, 8.00 MB (234.49 million rows/s., 1.88 GB/s.)
Peak memory: 28.98 KiB
Output: number MOD 10, count()
Expression ((Project names + Projection))
│ I/O: rows 10 → 10 · 90 B → 90 B
│ time 21.82 us (0.5%) · parallelism 0.98/1
└──Aggregating
│ Keys: number MOD 10
│ Aggregates: count()
│ Skip merging: 0
│ I/O: rows 1.00 million → 10 (0.00%) · 1.00 MB → 90 B
│ Stage (partial aggregation): time 868.45 us (20.4%) · parallelism 3.80/15
│ Stage (final aggregation): time 445.27 us (10.4%) · parallelism 1.11/16
└──Expression ((Before GROUP BY + Change column names to column identifiers))
│ I/O: rows 1.00 million → 1.00 million · 8.00 MB → 1.00 MB
│ time 677.07 us (15.9%) · parallelism 4.31/15
└──ReadFromSystemNumbers
Output: number
I/O: rows 0 → 1.00 million · 0 B → 8.00 MB
time 993.94 us (23.3%) · parallelism 7.52/15Examinons la sortie. Commençons par l’en-tête.
Query summary:
Time: <total> (planning <planning> · execution <execution>)
Read: <rows> rows, <bytes> (<rows/s>, <bytes/s>)
Peak memory: <peak>Time— temps total, réparti entre les phases de planification (c.-à-d. création du plan + optimisation du plan + construction du pipeline) et d’exécution (exécution du pipeline).Read— lignes et octets non compressés lus dans les tables, avec leur débit — les mêmes valeurs que celles indiquées dans le pied de page d’une requête classique sous la forme "Processed".Peak memory— mémoire maximale utilisée par la requête.
Examinons maintenant les nouvelles lignes qui apparaissent dans le plan de requête.
I/O: rows <in> → <out> (<selectivity>%) · <bytes_in> → <bytes_out>
[Stage (<stage>): ]time <t> (<share>%) · parallelism <avg>/<max>Le nombre de lignes et d’octets est indiqué une seule fois pour l’ensemble de l’étape (la ligne I/O). Le temps et le parallélisme sont indiqués pour chaque étape de l’étape sur la ou les lignes en retrait qui suivent.
rows <in> → <out>— lignes entrées dans l’étape et qui en sont sorties ; (<selectivity>%) indique dans quelle mesure l’étape a filtré (out/in) ou étendu les données. Cette information est masquée lorsque le nombre de lignes en entrée est égal au nombre de lignes en sortie, ainsi que lorsque le nombre de lignes en entrée est égal à0.<bytes_in> → <bytes_out>— octets en mémoire non compressés traversant l’étape (omis lorsque les deux valeurs sont nulles).time <t> (<share>%)— temps d’horloge pendant lequel l’étape a été active, ainsi que sa part du temps d’exécution de la requête (c.-à-d. hors temps de build). Notez que les parts peuvent totaliser plus de 100 % car les étapes et les étapes s’exécutent en parallèle.parallelism <avg>/<max>— nombre moyen de threads CPU travaillant simultanément dans cette étape, par rapport au maximum qu’elle pouvait utiliser. Une valeur proche du maximum signifie que l’étape a été bien parallélisée ; une valeur proche de 1 signifie qu’elle s’est exécutée principalement en série.Stage (<stage>)— le nom de l’étape. Une étape avec une seule étape affiche directement la ligne de temps, sans libelléStage (...). Les étapes comportant plusieurs étapes affichent une ligne libellée par étape ; par exemple,AggregatingafficheStage (partial aggregation)etStage (final aggregation), et un hash join afficheStage (build)etStage (probe).
Étapes de jointure
Pour une étape de jointure, EXPLAIN ANALYZE affiche, pour chaque côté, des lignes de participation — Left et Right — suivies des éventuelles lignes propres à l’implémentation de la jointure. Left et Right correspondent aux côtés logiques de l’instruction SQL. Dans la plupart des cas, Left correspond également au côté probe de la jointure et Right au côté build. Ce n’est toutefois pas systématique, en raison de l’échange qui peut intervenir lors de l’exécution de la jointure. Toutes les valeurs de join_algorithm sont prises en charge (hash, parallel_hash, grace_hash, partial_merge, full_sorting_merge, parallel_full_sorting_merge, direct), de même que les deux implémentations que ce paramètre ne peut pas sélectionner : les jointures CROSS ou COMMA, ainsi que toute section ON sans égalité de clés, et le moteur de table Join. La plupart indiquent les deux côtés ; certaines n’indiquent que le côté qu’elles matérialisent (par exemple, direct affiche uniquement Left:).
Les lignes associées à chaque côté ont la même structure :
Left: rows <left_rows> · matched <matched_left_rows> · match rate <match_rate>% · fanout <fanout>
Right: rows <right_rows> · matched <matched_right_rows> · match rate <match_rate>% · fanout <fanout>Pour chaque côté, EXPLAIN ANALYZE indique :
rows <rows>— le nombre total de lignes de ce côté qui ont été traitées par la jointure.matched <matched_rows>— le nombre de lignes de ce côté qui ont trouvé au moins une ligne correspondante de l’autre côté. Il s’agit bien de lignes, et non de clés : si une clé apparaît trois fois à droite et établit une correspondance, les trois lignes de droite sont comptées comme correspondantes.match rate <match_rate>%— le pourcentage de lignes de ce côté ayant établi une correspondance, calculé comme suit :100 * <matched_rows> / <rows>.fanout <fanout>— le nombre de lignes de sortie produites en moyenne par une ligne correspondante de ce côté.
Toute valeur qui ne peut pas être calculée exactement est indiquée par not collected plutôt que par 0. match rate et fanout sont calculés à partir de matched ; par conséquent, pour un côté dépourvu de cette valeur, ces trois valeurs sont indiquées comme not collected.
Fanout
fanout mesure la multiplication des lignes :
matched output rows = <output_rows> - <NULL-padded rows of both sides>
fanout = <matched output rows> / <matched_rows of that side>Une jointure externe produit une ligne de sortie complétée par des NULL pour chaque ligne d’un côté préservé qui n’a trouvé aucune correspondance. Ces lignes sont soustraites afin de ne pas diluer le ratio. Seul un côté préservé en comporte : le côté droit pour RIGHT et FULL, le côté gauche pour LEFT et FULL :
fanout = 0— les lignes correspondantes n’ont produit aucune ligne de sortie, comme dans une jointureANTI: elle ne produit que les lignes qui n’ont trouvé aucune correspondance.fanout = 1— une jointure 1:1 sans duplication ; chaque ligne correspondante a produit exactement une ligne de sortie.fanout > 1— une jointure 1:N ; des clés dupliquées de l’autre côté ont multiplié les lignes. Une valeur élevée des deux côtés simultanément indique une explosion cartésienne involontaire.
Cas où les nombres nécessitent matches = 1
La plupart de ces nombres résultent des données que la jointure produit déjà et sont rapportés par un simple EXPLAIN ANALYZE. Les autres nécessitent un suivi que la jointure n’effectuerait pas autrement et ne sont donc rapportés qu’avec EXPLAIN ANALYZE matches = 1. Les cas concernés dépendent de l’algorithme ; dans la famille hash, il y en a deux :
- le côté droit de
ALL INNERetALL LEFT, qui nécessite de marquer chaque ligne droite correspondante ; - le côté gauche de
ALL LEFTetALL FULL, mais uniquement lorsque la requête ne sélectionne aucune donnée de la table de droite et que la sectionONest une simple égalité de clé. Sinon, la sonde enregistre déjà les lignes de gauche qui correspondent — soit pour matérialiser les colonnes de droite, soit pour évaluer la condition résiduelle — et le décompte est exact sans cette option.
partial_merge en a besoin pour le côté droit des quatre types ALL, pour la même raison.
full_sorting_merge et parallel_full_sorting_merge en ont besoin pour les deux côtés des types ANY. Les
types ALL n’en ont pas besoin.
matches = 1 ne permet pas de collecter toutes les combinaisons. Le côté qu’une jointure peut rapporter découle
de ce que cette jointure doit déjà faire ; cela dépend donc de l’algorithme, ainsi que du type et de la
strictness.
Famille hash. hash, parallel_hash et grace_hash donnent toujours les mêmes résultats :
| Jointure | matched à gauche |
matched à droite |
|---|---|---|
ALL INNER, ALL LEFT, ALL RIGHT, ALL FULL |
oui | oui |
SEMI LEFT, ANTI LEFT |
oui | non |
ANY RIGHT, ANTI RIGHT |
non | oui |
ASOF (inner) |
oui | non |
SEMI RIGHT |
non | non |
ANY INNER, ANY LEFT, ASOF LEFT |
non | non |
Le côté droit n’est pas disponible lorsque la jointure ne conserve qu’une ligne par clé dans sa table de hachage, comme le font les jointures ANY, SEMI et ANTI : les lignes de droite dupliquées ne sont jamais stockées et ne peuvent donc pas être comptées. Le côté gauche n’est pas disponible lorsque la jointure supprime la sortie d’une ligne de gauche dont le partenaire a déjà été revendiqué par une autre ligne de gauche, de sorte que les lignes émises sous-estiment les lignes correspondantes.
L’activation de any_join_distinct_right_table_keys fait passer ANY à l’ancienne sémantique RightAny, qui émet une ligne par ligne de gauche et conserve donc les deux décomptes. ANY RIGHT et ANY FULL rapportent alors les deux côtés, et ANY INNER est réécrit en SEMI LEFT.
Le moteur de table Join suit le même tableau, en utilisant le type et la strictness déclarés dans le moteur : Join(ALL, INNER, …) rapporte les deux côtés, Join(ANY, LEFT, …) aucun des deux.
Algorithmes de fusion. full_sorting_merge et parallel_full_sorting_merge acceptent les quatre types ALL, ANY INNER, ANY LEFT, ANY RIGHT, ASOF et ASOF LEFT. Ils rapportent les deux côtés pour tous les types, sauf ASOF et ASOF LEFT, pour lesquels le côté droit est not collected, et ce sans matches = 1 — ils parcourent les deux entrées triées et voient chaque ligne d’une plage d’égalité au fur et à mesure qu’ils la consomment ; rien n’a donc besoin d’être reconstruit par la suite.
partial_merge accepte ALL INNER, ALL LEFT, ALL RIGHT, ALL FULL, ANY INNER, ANY LEFT et SEMI LEFT. Il rapporte les deux côtés pour les quatre types ALL, le côté droit avec matches = 1 ; pour ANY INNER, ANY LEFT et SEMI LEFT, le côté droit est not collected.
direct. Le côté gauche uniquement. Le côté droit est un magasin clé-valeur qui n’est jamais matérialisé sous forme de lignes ; il n’a donc pas de ligne Right:.
CROSS, COMMA et un ON constant. Aucun côté, comme décrit ci-dessus.
Lorsque les deux algorithmes renvoient un nombre, les résultats concordent. Les algorithmes de fusion disposent simplement de davantage d’informations ; ils ne diffèrent pas quant à la définition d’une correspondance.
Lignes spécifiques à l’algorithme
Examinons les lignes supplémentaires ajoutées par chaque implémentation de jointure.
Pour les jointures hash et parallel_hash, ainsi que pour le moteur de table Join, une ligne Hash table: décrit la table de hachage construite à partir de la table de droite :
Hash table: unique keys <unique_keys> · memory <peak_memory>unique keys <unique_keys>— le nombre de clés uniques stockées dans la table de hachage pendant la phase de construction.memory <peak_memory>— le pic de mémoire utilisé par la table de hachage pendant la phase de construction.
Pour la jointure grace_hash, la ligne Hash table: indique également comment la jointure s'est adaptée à la limite de mémoire, et une ligne Spill: indique si des données ont été déversées sur le disque :
Hash table: unique keys <unique_keys> · memory <peak_memory> · buckets <buckets> · rehashes <rehashes>
Spill: yes · left spilled <left_spilled_bytes> · right spilled <right_spilled_bytes>buckets <buckets>— le nombre de buckets auquel la jointure grace hash join est parvenue à la fin de l’exécution. Il s’agit toujours d’une puissance de2.rehashes <rehashes>— le nombre de fois où le nombre de buckets a dû être doublé pour respecter la limite de mémoire.Spill:— un indicateuryes/noprécisant si des données ont été écrites sur le disque. Lorsque c’est le cas,left spilled <left_spilled_bytes>etright spilled <right_spilled_bytes>indiquent le nombre d’octets compressés écrits sur le disque depuis les côtés gauche (probe) et droit (build) ; si aucune donnée n’a été écrite sur le disque, la ligne est simplementSpill: no.
Pour la jointure partial_merge, la ligne Right: contient des informations supplémentaires sur la mise en mémoire tampon et le tri de la table de droite, et le temps de tri s’affiche sur les lignes Stage (build) et Stage (probe) :
Right: rows <right_rows> · matched <matched_right_rows> · size <right_size> · blocks <right_blocks> · storage <in-memory|external> · match rate <match_rate>% · fanout <fanout>
Stage (build): time <t> (<share>%) · parallelism <avg>/<max> · sort time <build_sort_time> · sort share <build_sort_share>%
Stage (probe): time <t> (<share>%) · parallelism <avg>/<max> · sort time <probe_sort_time> · sort share <probe_sort_share>%size <right_size>— la taille en mémoire des blocs de la table de droite.blocks <right_blocks>— le nombre de blocs dans lesquels la table de droite a été mise en mémoire tampon.storage <in-memory|external>— indique si la table de droite tenait en mémoire (in-memory) ou devait être déversée sur le disque (external). Lorsqu’il estexternal, un champ supplémentairespilled <spilled_bytes>indique le nombre d’octets compressés écrits sur le disque.sort time <sort_time>— le temps consacré au tri de la table de droite (lors de l’étape de build) et de chaque bloc gauche entrant (lors de l’étape de probe).sort share <sort_share>%—sort timeen proportion du temps d’activité de cette étape (la somme du temps écoulé de ses processeurs), contrairement au pourcentage detimede l’étape, qui représente une part du temps d’exécution de l’ensemble de la requête.
Pour la jointure full_sorting_merge, seules les lignes communes Left: et Right: sont affichées.
Pour la jointure direct, seule la ligne Left: est affichée, car le côté droit est un stockage clé-valeur consulté directement plutôt que matérialisé en lignes.
Pour une jointure CROSS ou COMMA, et pour toute section ON sans égalité de clés, une ligne Buffer: décrit comment la table de droite a été conservée en mémoire et une ligne Spill: indique si elle a été écrite sur le disque :
Buffer: memory <peak_memory> · compressed <yes|no>
Spill: yes · right spilled <right_spilled_bytes>memory <peak_memory>— le pic de mémoire occupé par la table de droite mise en mémoire tampon.compressed <yes|no>— indique si au moins un bloc mis en mémoire tampon a été compressé ; les lecteurs décompressent alors chaque bloc stocké.Spill:— le même indicateuryes/noque pourgrace_hash,right spilled <right_spilled_bytes>indiquant le nombre d’octets compressés écrits sur le disque.
Les deux côtés indiquent ici matched not collected : un prédicat constant associe soit chaque ligne de gauche à chaque ligne de droite, soit aucune ligne ; il n’est donc pas possible d’indiquer quelles lignes individuelles correspondent.
Pour une jointure utilisant le moteur de table Join, les deux côtés sont indiqués, ainsi que la ligne Hash table: qui décrit la table préconstruite. Le côté droit comptabilise les lignes stockées dans le moteur, et non celles créées pour chaque requête.
Temps par processeur
Avec processors = 1, une ligne supplémentaire est affichée sous chaque étape, montrant la répartition du temps écoulé entre les processeurs de l’étape :
Time per processor (<n>): min <t> · median <t> · max <t> · sum <t><n> correspond au nombre de processeurs dans l’étape. Un écart important entre median et max indique un déséquilibre de charge entre les processeurs parallèles.
EXPLAIN ESTIMATE
Affiche le nombre estimé de lignes, de marks et de parts à lire dans les tables lors du traitement de la requête. Fonctionne avec les tables de la famille MergeTree.
Exemple
Création d’une table :
CREATE TABLE ttt (i Int64) ENGINE = MergeTree() ORDER BY i SETTINGS index_granularity = 16, write_final_mark = 0;
INSERT INTO ttt SELECT number FROM numbers(128);
OPTIMIZE TABLE ttt;EXPLAIN ESTIMATE SELECT * FROM ttt;┌─database─┬─table─┬─parts─┬─rows─┬─marks─┐
│ default │ ttt │ 1 │ 128 │ 8 │
└──────────┴───────┴───────┴──────┴───────┘EXPLAIN WHATIF
Estime le bénéfice qu’un index de saut hypothétique apporterait à une requête SELECT, sans matérialiser l’index sur le disque. Définissez un ou plusieurs candidats avec CREATE HYPOTHETICAL INDEX, puis exécutez EXPLAIN WHATIF SELECT ... pour voir, pour chaque candidat : son applicabilité, le nombre estimé de marks lus, le volume estimé en octets et le taux d’exclusion.
Syntaxe
EXPLAIN WHATIF [empirical = 0] SELECT ...Paramètres
empirical—1(par défaut) exécute l’index en mémoire sur les granules élaguées par rapport à la référence afin de mesurer le taux d’exclusion (une borne supérieure).0ignore cette voie. Dans tous les cas, siempiricalne produit pas de résultat (désactivé, ou si l’index ne peut pas être évalué en mémoire), l’estimateur se rabat sur les statistiques de colonne, puis, en dernier recours, sur un résumé limité à l’applicabilité si aucune des deux options n’est disponible.
Sortie
Baseline (after PK + partition + existing indexes):
table: db.t
parts: 1
marks: 100
est_bytes: 1.50 MiB (only when the query reads rows)
With idx_b (minmax, hypothetical):
status: applicable
marks: 1
est_bytes: 15.00 KiB (only when baseline bytes are known)
skip_ratio: 99.0%
Estimation:
source: empirical | statistical | applicability_only
empirical_status: ok | unsupported | disabled
empirical_reason: <reason> (only when empirical_status = unsupported)
sampled_parts: 50 / 100 (only when source = empirical)
sampled_marks: 50 / 100 (only when source = empirical)
elapsed_us: 631 (only when source = empirical)source— indique comment l’estimation a été obtenue.empirical: l’index a été construit en mémoire sur les granules conservés après l’élagage de référence, puis les granules que l’index permettrait d’ignorer ont été comptés. Il s’agit d’une borne supérieure — voir les limites dansCREATE HYPOTHETICAL INDEX.statistical: dérivée des statistiques de colonnes. Utilisée lorsque l’estimation empirique est désactivée (empirical = 0) ou n’a pas pu produire de résultat, et que des statistiques de colonnes sont définies sur les colonnes concernées.applicability_only: l’index s’applique au prédicat, mais ni l’estimation empirique ni l’estimation statistique n’ont produit de résultat (par ex.empirical = 0et aucune statistique de colonnes définie). Indiqueskip_ratio: 0.0%comme borne prudente.
empirical_reason— indique pourquoi l’estimation empirique n’a pas pu être exécutée. Affiché uniquement avecempirical_status: unsupported. Par exemple, une valeur non nulle demerge_tree_min_rows_for_seekou demerge_tree_min_bytes_for_seekentraîne le regroupement des plages de marks lors d’une lecture réelle, ce que le comptage par granule ne modélise pas ; l’estimation se rabat donc surstatisticalouapplicability_only.sampled_parts/sampled_marks—<baseline-pruned> / <total in the table>. Indique quelle fraction de la table reste après l’élagage par la PK, la partition et les index existants, c.-à-d. l’entrée de l’index hypothétique.est_bytes— estimation du nombre d’octets lus, dérivée de la taille moyenne des lignes de la table ; elle est donc approximative et varie selon le stockage et la compression. La ligne de référence n’apparaît que lorsque la requête lit des lignes ; la ligne par candidat n’apparaît que lorsque l’estimation de référence des octets est connue.
Le paramètre est écrit en intégré entre WHATIF et SELECT — il n’y a pas de mot-clé SETTINGS (comme pour les autres variantes de EXPLAIN, qui acceptent leurs options de cette manière).
Si aucun index hypothétique n’est défini pour la table, EXPLAIN WHATIF indique status: not_applicable avec une suggestion d’en créer un.
Ligne combinée (plusieurs candidats)
Lorsque deux candidats ou plus sont évalués empiriquement, EXPLAIN WHATIF ajoute un bloc supplémentaire nommé (combined: idx_a, idx_b, ...) après les lignes par candidat. Il indique le bénéfice combiné de la présence simultanée de tous ces index : lors d’une lecture réelle, un granule n’est conservé que s’il survit à chaque index de saut, donc l’estimation combinée correspond à l’intersection des granules survivants des candidats. Son skip_ratio est donc au moins aussi élevé que celui du meilleur candidat pris isolément — des index complémentaires élaguent davantage ensemble, tandis que des index redondants le laissent inchangé.
Seuls les candidats avec source: empirical contribuent, car la ligne combinée est construite en intersectant leurs ensembles de survie par granule. Les candidats estimés comme statistical ou applicability_only n’ont pas de données par granule et sont exclus ; par conséquent, le bloc combiné n’apparaît que lorsqu’au moins deux candidats ont produit une estimation empirique, et il est omis dans le cas contraire (par exemple avec empirical = 0). Les champs de son estimation sont les mêmes que pour un bloc empirique par candidat, à ceci près que elapsed_us vaut 0 — l’estimation combinée est dérivée des analyses par candidat, et non d’une nouvelle analyse. Le nom synthétique (combined: ...) n’est qu’une étiquette de rapport et ne peut pas être utilisé avec force_data_skipping_indices.
Exemple empirique
CREATE TABLE t (a UInt64, b UInt64) ENGINE = MergeTree ORDER BY a
SETTINGS index_granularity = 100;
INSERT INTO t SELECT number, number FROM numbers(10000);
CREATE HYPOTHETICAL INDEX idx_b ON t (b) TYPE minmax GRANULARITY 1;
EXPLAIN WHATIF SELECT * FROM t WHERE b = 42;Baseline (after PK + partition + existing indexes):
table: default.t
parts: 1
marks: 100
est_bytes: 85.52 KiB
With idx_b (minmax, hypothetical):
status: applicable
marks: 1
est_bytes: 875.00 B
skip_ratio: 99.0%
Estimation:
source: empirical
empirical_status: ok
sampled_parts: 1 / 1
sampled_marks: 100 / 100Le minmax hypothétique réduirait 100 marks à 1 — skip_ratio: 99.0%. (est_bytes est une estimation basée sur la taille moyenne des lignes, donc la valeur exacte peut varier.)
Exemple statistique
Les statistiques de colonne sont désactivées par défaut. Pour utiliser le chemin statistical, définissez-les d’abord sur les colonnes concernées, puis attendez que la mutation de matérialisation se termine :
ALTER TABLE t ADD STATISTICS b TYPE tdigest;
ALTER TABLE t MATERIALIZE STATISTICS b SETTINGS mutations_sync = 1;Désactivez ensuite le chemin empirique afin que l’estimateur revienne aux statistiques de colonne :
EXPLAIN WHATIF empirical = 0 SELECT * FROM t WHERE b < 10;With idx_b (minmax, hypothetical):
status: applicable
marks: 1
est_bytes: 1.66 KiB
skip_ratio: 99.9%
Estimation:
source: statistical
empirical_status: disabledLe nombre provient de la sélectivité des statistiques de colonne pour b < 10 (environ 10 lignes sur 10 000) et est indiqué comme borne supérieure de skip_ratio. Il n’y a ni sampled_parts ni sampled_marks — aucune donnée n’a été lue.
Si aucun des deux mécanismes n’est disponible (par ex. empirical = 0 et aucune statistique de colonne définie), l’estimateur indique source: applicability_only ainsi qu’un skip_ratio: 0.0% conservateur.
EXPLAIN TABLE OVERRIDE
Affiche le résultat d'une surcharge de table appliquée au schéma d'une table accessible via une fonction de table. Effectue également certaines vérifications et lève une exception si la surcharge aurait entraîné un échec.
Exemple
Supposons que vous ayez une table MySQL distante comme celle-ci :
CREATE TABLE db.tbl (
id INT PRIMARY KEY,
created DATETIME DEFAULT now()
)EXPLAIN TABLE OVERRIDE mysql('127.0.0.1:3306', 'db', 'tbl', 'root', 'clickhouse')
PARTITION BY toYYYYMM(assumeNotNull(created))┌─explain─────────────────────────────────────────────────┐
│ PARTITION BY uses columns: `created` Nullable(DateTime) │
└─────────────────────────────────────────────────────────┘