Получение плана исполнения запроса и AST
YDB предоставляет два типа планов запроса: логический план и план исполнения. Логический план лучше подходит для анализа сложных запросов с большим количеством операторов join. План исполнения более детализирован: он дополнительно показывает стадии распределённого плана и коннекторы между ними, что делает его более удобным для анализа простых OLTP-запросов.
Планы исполнения запроса
Получить план исполнения через YDB CLI можно с помощью команды explain
:
ydb -p <profile_name> table query explain \
-q "SELECT season_id, episode_id, title
FROM episodes
WHERE series_id = 1
AND season_id > 1
ORDER BY season_id, episode_id
LIMIT 3"
Полученный результат:
Query Plan:
ResultSet
└──Limit (Limit: 3)
└──<Merge>
└──TopSort (Limit: 3, TopSortBy: )
└──Filter (Predicate: Exist(item.season_id) And Exist(item.series_id))
└──TablePointLookup (ReadRange: [series_id (1), season_id (1, +∞), episode_id (-∞, +∞)], ReadColumns: [episode_id, season_id, title, series_id], Table: episodes)
Tables: [episodes]
Дополнительно к плану исполнения запроса вы можете получить AST (абстрактное синтаксическое дерево).
Раздел AST содержит представление на внутреннем языке miniKQL.
Для получения AST необходимо вызвать explain
с флагом --ast
:
ydb -p <profile_name> table query explain \
-q "SELECT season_id, episode_id, title
FROM episodes
WHERE series_id = 1
AND season_id > 1
ORDER BY season_id, episode_id
LIMIT 3" --ast
Полученный AST:
Query AST:
(
(let $1 (KqpTable '"episodes" '"72075186224045943:83859" '"" '1))
(let $2 '('"episode_id" '"season_id" '"title" '"series_id"))
(let $3 (Uint64 '1))
(let $4 (KqpRowsSourceSettings $1 $2 '() '((KqlKeyExc $3 $3) (KqlKeyInc $3))))
(let $5 (Uint64 '"3"))
(let $6 (DqPhyStage '((DqSource (DataSource '"KqpReadRangesSource") $4)) (lambda '($12) (block '(
(let $13 (Bool 'true))
(return (FromFlow (TopSort (OrderedMap (OrderedFilter (ToFlow $12) (lambda '($14) (And (Exists (Member $14 '"season_id")) (Exists (Member $14 '"series_id"))))) (lambda '($15) (AsStruct '('"episode_id" (Member $15 '"episode_id")) '('"season_id" (Member $15 '"season_id")) '('"title" (Member $15 '"title"))))) $5 '($13 $13) (lambda '($16) '((Member $16 '"season_id") (Member $16 '"episode_id"))))))
))) '('('"_logical_id" '842) '('"_id" '"14388682-e03b28dd-60f91f84-d7cd002f"))))
(let $7 (DqCnMerge (TDqOutput $6 '0) '('('"season_id" '"Asc") '('"episode_id" '"Asc"))))
(let $8 (DqPhyStage '($7) (lambda '($17) (FromFlow (Take (ToFlow $17) $5))) '('('"_logical_id" '855) '('"_id" '"295c0459-34d4b026-b467e71f-35402430"))))
(let $9 '('"season_id" '"episode_id" '"title"))
(let $10 (DqCnResult (TDqOutput $8 '0) $9))
(let $11 (OptionalType (DataType 'Uint64)))
(return (KqpPhysicalQuery '((KqpPhysicalTx '($6 $8) '($10) '() '('('"type" '"data")))) '((KqpTxResultBinding (ListType (StructType '('"episode_id" $11) '('"season_id" $11) '('"title" (OptionalType (DataType 'String))))) '0 '0)) '('('"type" '"data_query"))))
)
Логический план запроса
Рассмотрим аналитический запрос Q18 из бенчмарка TPCH:
$p = (
select
p.p_brand as p_brand,
p.p_type as p_type,
p.p_size as p_size,
ps.ps_suppkey as ps_suppkey
from
part as p
join
partsupp as ps
on
p.p_partkey = ps.ps_partkey
where
p.p_brand <> 'Brand#45'
and p.p_type not like 'MEDIUM POLISHED%'
and (p.p_size = 49 or p.p_size = 14 or p.p_size = 23 or p.p_size = 45 or p.p_size = 19 or p.p_size = 3 or p.p_size = 36 or p.p_size = 9)
);
$s = (
select
s_suppkey
from
supplier
where
s_comment like "%Customer%Complaints%"
);
$j = (
select
p.p_brand as p_brand,
p.p_type as p_type,
p.p_size as p_size,
p.ps_suppkey as ps_suppkey
from
$p as p
left only join
$s as s
on
p.ps_suppkey = s.s_suppkey
);
select
j.p_brand as p_brand,
j.p_type as p_type,
j.p_size as p_size,
count(distinct j.ps_suppkey) as supplier_cnt
from
$j as j
group by
j.p_brand,
j.p_type,
j.p_size
order by
supplier_cnt desc,
p_brand,
p_type,
p_size
;
Получить логический план выполнения можно командой:
ydb -p <profile_name> sql --explain -f q18.sql
В результате получится логический план запроса, состоящий из операторов плана и различных предсказаний оптимизатора:
Query Plan:
┌───────────┬──────────┬───────────┬───────────────────────────────────────────────────────┐
│ E-Cost │ E-Rows │ E-Size │ Operation │
├───────────┼──────────┼───────────┼───────────────────────────────────────────────────────┤
│ │ │ │ ┌> ResultSet │
│ │ │ │ └─┬> Sort ([row.supplier_cnt,row.p_brand,row.p_type,r │
│ │ │ │ ow.p_size]) │
│ │ │ │ └─┬> Aggregate (Phase: Final) │
│ │ │ │ └─┬> HashShuffle (KeyColumns: ["part.p_brand","pa │
│ │ │ │ rt.p_size","part.p_type"], HashFunc: "HashV2") │
│ │ │ │ └─┬> Aggregate (GroupBy: [part.p_brand,part.p_s │
│ │ │ │ ize,part.p_type], Aggregation: {Inc(_yql_agg_0)}, Pha │
│ │ │ │ se: Intermediate) │
│ │ │ │ └─┬> Aggregate (Phase: Final) │
│ │ │ │ └─┬> HashShuffle (KeyColumns: ["part.p_bran │
│ │ │ │ d","part.p_size","part.p_type","partsupp.ps_suppkey"] │
│ │ │ │ , HashFunc: "HashV2") │
│ │ │ │ └─┬> Aggregate (GroupBy: [part.p_brand,pa │
│ │ │ │ rt.p_size,part.p_type,partsupp.ps_suppkey], Aggregati │
│ │ │ │ on: state, Phase: Intermediate) │
│ 4.350e+08 │ 40000000 │ 3.421e+09 │ └─┬> LeftOnlyJoin (Grace) (partsupp.ps_ │
│ │ │ │ suppkey = supplier.s_suppkey) │
│ │ │ │ ├─┬> HashShuffle (KeyColumns: ["parts │
│ │ │ │ upp.ps_suppkey"], HashFunc: "ColumnShardHashV1") │
│ 3.135e+08 │ 40000000 │ 2.331e+09 │ │ └─┬> InnerJoin (Grace) (part.p_part │
│ │ │ │ key = partsupp.ps_partkey) │
│ 0 │ 9000000 │ 2.869e+08 │ │ ├─┬> Filter (Contains) │
│ 0 │ 9000000 │ 2.869e+08 │ │ │ └─┬> Filter ((p_brand != "Brand │
│ │ │ │ #33") AND (NOT p_type LIKE "PROMO POLISHED%"), Pushdo │
│ │ │ │ wn: True) │
│ 0 │ 20000000 │ 6.375e+08 │ │ │ └──> TableFullScan (Table: pa │
│ │ │ │ rt, ReadColumns: ["p_partkey (-∞, +∞)","p_brand","p_s │
│ │ │ │ ize","p_type"]) │
│ │ │ │ │ └─┬> HashShuffle (KeyColumns: ["p │
│ │ │ │ s_partkey"], HashFunc: "ColumnShardHashV1") │
│ 0 │ 80000000 │ 2.113e+09 │ │ └──> TableFullScan (Table: part │
│ │ │ │ supp, ReadColumns: ["ps_partkey (-∞, +∞)","ps_suppkey │
│ │ │ │ (-∞, +∞)"]) │
│ 0 │ 500000 │ 13621431 │ └─┬> Filter (Apply, Pushdown: True) │
│ 0 │ 1000000 │ 27242862 │ └──> TableFullScan (Table: supplier │
│ │ │ │ , ReadColumns: ["s_suppkey (-∞, +∞)","s_comment"]) │
└───────────┴──────────┴───────────┴───────────────────────────────────────────────────────┘
В этом плане показано дерево операторов YDB и выведены три предсказания оптимизатора для каждого оператора:
E-Cost:
оценка стоимости текущего оператора и всех его входов;E-Rows
: оценка количества записей на выходе текущего оператора;E-Size
: оценка объёма результата оператора в байтах.
Кроме предсказаний оптимизатора, можно также получить реальные значения при выполнении запроса:
A-Cpu
: общее время работы CPU для текущего оператора;A-Rows
: фактическое количество записей на выходе текущего оператора.
Статистика выполнения не всегда доступна для каждого оператора, так как в настоящий момент она собирается по стадиям выполнения. В одной стадии может вычисляться сразу несколько операторов.
Для получения логического плана запроса со статистикой выполнения выполните следующую команду:
ydb -p <profile_name> sql --explain-analyze --format pretty-table --analyze -f q18.sql
Получится следующий логический план запроса:
┌───────┬──────────┬───────────┬──────────┬───────────┬──────────────────────────────────────────────────────────────────┐
│ A-Cpu │ A-Rows │ E-Cost │ E-Rows │ E-Size │ Operation │
├───────┼──────────┼───────────┼──────────┼───────────┼──────────────────────────────────────────────────────────────────┤
│ │ │ │ │ │ ┌> ResultSet │
│ 934 │ 27840 │ │ │ │ └─┬> Sort (A-SelfCpu: 15.706, A-Size: 936768, [row.supplier_cnt, │
│ │ │ │ │ │ row.p_brand,row.p_type,row.p_size]) │
│ │ 27840 │ │ │ │ └─┬> Aggregate (Phase: Final) │
│ │ │ │ │ │ └─┬> HashShuffle (KeyColumns: ["part.p_brand","part.p_size", │
│ │ │ │ │ │ "part.p_type"], HashFunc: "HashV2") │
│ 918 │ 3431671 │ │ │ │ └─┬> Aggregate (GroupBy: [part.p_brand,part.p_size,part.p_ │
│ │ │ │ │ │ type], Aggregation: {Inc(_yql_agg_0)}, A-SelfCpu: 139.139, Phase │
│ │ │ │ │ │ : Intermediate, A-Size: 112058009) │
│ │ │ │ │ │ └─┬> Aggregate (Phase: Final) │
│ │ │ │ │ │ └─┬> HashShuffle (KeyColumns: ["part.p_brand","part.p_ │
│ │ │ │ │ │ size","part.p_type","partsupp.ps_suppkey"], HashFunc: "HashV2") │
│ 779 │ 11865156 │ │ │ │ └─┬> Aggregate (GroupBy: [part.p_brand,part.p_size,p │
│ │ │ │ │ │ art.p_type,partsupp.ps_suppkey], Aggregation: state, A-SelfCpu: │
│ │ │ │ │ │ 194.29, Phase: Intermediate, A-Size: 410786277) │
│ │ 11867631 │ 4.350e+08 │ 40000000 │ 3.421e+09 │ └─┬> LeftOnlyJoin (Grace) (partsupp.ps_suppkey = s │
│ │ │ │ │ │ upplier.s_suppkey) │
│ │ │ │ │ │ ├─┬> HashShuffle (KeyColumns: ["partsupp.ps_supp │
│ │ │ │ │ │ key"], HashFunc: "ColumnShardHashV1") │
│ 584 │ 11873200 │ 3.135e+08 │ 40000000 │ 2.331e+09 │ │ └─┬> InnerJoin (Grace) (A-SelfCpu: 308.14, par │
│ │ │ │ │ │ t.p_partkey = partsupp.ps_partkey, A-Size: 505055811) │
│ 65 │ 2968300 │ 0 │ 9000000 │ 2.869e+08 │ │ ├─┬> Filter (Contains, A-SelfCpu: 65.311, A- │
│ │ │ │ │ │ Size: 105580755) │
│ │ │ 0 │ 9000000 │ 2.869e+08 │ │ │ └─┬> Filter ((p_brand != "Brand#33") AND ( │
│ │ │ │ │ │ NOT p_type LIKE "PROMO POLISHED%"), Pushdown: True) │
│ │ 18560313 │ 0 │ 20000000 │ 6.375e+08 │ │ │ └──> TableFullScan (Table: part, ReadCol │
│ │ │ │ │ │ umns: ["p_partkey (-∞, +∞)","p_brand","p_size","p_type"], A-Size │
│ │ │ │ │ │ : 1060176372) │
│ │ │ │ │ │ │ └─┬> HashShuffle (KeyColumns: ["ps_partkey"] │
│ │ │ │ │ │ , HashFunc: "ColumnShardHashV1") │
│ 211 │ 80000000 │ 0 │ 80000000 │ 2.113e+09 │ │ └──> TableFullScan (Table: partsupp, A-Sel │
│ │ │ │ │ │ fCpu: 210.822, ReadColumns: ["ps_partkey (-∞, +∞)","ps_suppkey ( │
│ │ │ │ │ │ -∞, +∞)"], A-Size: 1283276800) │
│ 0 │ 479 │ 0 │ 500000 │ 13621431 │ └─┬> Filter (Apply, A-SelfCpu: 0.27, Pushdown: T │
│ │ │ │ │ │ rue, A-Size: 4937) │
│ │ 479 │ 0 │ 1000000 │ 27242862 │ └──> TableFullScan (Table: supplier, ReadColum │
│ │ │ │ │ │ ns: ["s_suppkey (-∞, +∞)","s_comment"], A-Size: 7885) │
└───────┴──────────┴───────────┴──────────┴───────────┴──────────────────────────────────────────────────────────────────┘
В этом плане, кроме операторов и предсказаний оптимизатора уже присутствует статистика выполнения - A-Cpu
и A-Rows
.