Skip to content

Commit 8a90b98

Browse files
committed
Add operator-owned sub-plans to EXPLAIN
1 parent 8616efa commit 8a90b98

14 files changed

Lines changed: 302 additions & 46 deletions

‎src/common/render_tree.cpp‎

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,11 +121,19 @@ static void GetTreeWidthHeight(const T &op, idx_t &width, idx_t &height) {
121121
}
122122

123123
static unique_ptr<RenderTreeNode> CreateNode(const LogicalOperator &op) {
124-
return make_uniq<RenderTreeNode>(op.GetName(), op.ParamsToString());
124+
auto result = make_uniq<RenderTreeNode>(op.GetName(), op.ParamsToString());
125+
if (!HidesChildren(op)) {
126+
result->sub_plans = op.GetExplainSubPlans();
127+
}
128+
return result;
125129
}
126130

127131
static unique_ptr<RenderTreeNode> CreateNode(const PhysicalOperator &op) {
128-
return make_uniq<RenderTreeNode>(op.GetName(), op.ParamsToString());
132+
auto result = make_uniq<RenderTreeNode>(op.GetName(), op.ParamsToString());
133+
if (!HidesChildren(op)) {
134+
result->sub_plans = op.GetExplainSubPlans();
135+
}
136+
return result;
129137
}
130138

131139
static unique_ptr<RenderTreeNode> CreateNode(const PipelineRenderNode &op) {
@@ -234,6 +242,11 @@ void RenderTree::SanitizeKeyNames() {
234242
new_map.insert(make_pair(key, value));
235243
}
236244
nodes[i]->extra_text = std::move(new_map);
245+
for (auto &sub_plan : nodes[i]->sub_plans) {
246+
if (sub_plan.tree) {
247+
sub_plan.tree->SanitizeKeyNames();
248+
}
249+
}
237250
}
238251
}
239252

‎src/common/tree_renderer/graphviz_tree_renderer.cpp‎

Lines changed: 47 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -59,45 +59,68 @@ void GRAPHVIZTreeRenderer::Render(const Pipeline &op, BaseTreeRenderer &ss) {
5959
ToStream(*tree, ss);
6060
}
6161

62-
void GRAPHVIZTreeRenderer::ToStreamInternal(RenderTree &root, BaseTreeRenderer &ss) {
63-
const string digraph_format = R"(
64-
digraph G {
65-
node [shape=box, style=rounded, fontname="Courier New", fontsize=10];
66-
%s
67-
%s
62+
static string GraphvizNodeName(const string &prefix, idx_t x, idx_t y) {
63+
if (prefix.empty()) {
64+
return StringUtil::Format("node_%d_%d", x, y);
65+
}
66+
return StringUtil::Format("node_%s_%d_%d", prefix, x, y);
6867
}
69-
)";
70-
71-
vector<string> nodes;
72-
vector<string> edges;
7368

74-
const string node_format = R"( node_%d_%d [label="%s"];)";
75-
76-
for (idx_t y = 0; y < root.height; y++) {
77-
for (idx_t x = 0; x < root.width; x++) {
78-
auto node = root.GetNode(x, y);
69+
static void RenderGraph(RenderTree &tree, const string &prefix, vector<string> &nodes, vector<string> &edges,
70+
idx_t &next_sub_plan) {
71+
for (idx_t y = 0; y < tree.height; y++) {
72+
for (idx_t x = 0; x < tree.width; x++) {
73+
auto node = tree.GetNode(x, y);
7974
if (!node) {
8075
continue;
8176
}
8277

83-
// Create Node
8478
vector<string> body;
8579
body.push_back(node->name);
8680
for (auto &item : node->extra_text) {
87-
auto &key = item.first;
88-
auto &value_raw = item.second;
89-
90-
auto value = QueryProfiler::JSONSanitize(value_raw);
91-
body.push_back(StringUtil::Format("%s:\\n%s", key, value));
81+
auto value = QueryProfiler::JSONSanitize(item.second);
82+
body.push_back(StringUtil::Format("%s:\\n%s", item.first, value));
9283
}
93-
nodes.push_back(StringUtil::Format(node_format, x, y, StringUtil::Join(body, "\\n───\\n")));
84+
auto node_name = GraphvizNodeName(prefix, x, y);
85+
nodes.push_back(
86+
StringUtil::Format(" %s [label=\"%s\"];", node_name, StringUtil::Join(body, "\\n───\\n")));
9487

95-
// Create Edge(s)
9688
for (auto &coord : node->child_positions) {
97-
edges.push_back(StringUtil::Format(" node_%d_%d -> node_%d_%d;", x, y, coord.x, coord.y));
89+
edges.push_back(
90+
StringUtil::Format(" %s -> %s;", node_name, GraphvizNodeName(prefix, coord.x, coord.y)));
91+
}
92+
for (auto &sub_plan : node->sub_plans) {
93+
if (!sub_plan.tree) {
94+
continue;
95+
}
96+
auto sub_plan_name = StringUtil::Format("subplan_%d", next_sub_plan++);
97+
vector<string> sub_plan_nodes;
98+
vector<string> sub_plan_edges;
99+
RenderGraph(*sub_plan.tree, sub_plan_name, sub_plan_nodes, sub_plan_edges, next_sub_plan);
100+
nodes.push_back(StringUtil::Format(" subgraph cluster_%s {\n label=\"%s\";\n%s\n%s\n }",
101+
sub_plan_name, QueryProfiler::JSONSanitize(sub_plan.label),
102+
StringUtil::Join(sub_plan_nodes, "\n"),
103+
StringUtil::Join(sub_plan_edges, "\n")));
104+
edges.push_back(StringUtil::Format(" %s -> %s [style=dashed];", node_name,
105+
GraphvizNodeName(sub_plan_name, 0, 0)));
98106
}
99107
}
100108
}
109+
}
110+
111+
void GRAPHVIZTreeRenderer::ToStreamInternal(RenderTree &root, BaseTreeRenderer &ss) {
112+
const string digraph_format = R"(
113+
digraph G {
114+
node [shape=box, style=rounded, fontname="Courier New", fontsize=10];
115+
%s
116+
%s
117+
}
118+
)";
119+
120+
vector<string> nodes;
121+
vector<string> edges;
122+
idx_t next_sub_plan = 0;
123+
RenderGraph(root, string(), nodes, edges, next_sub_plan);
101124
auto node_lines = StringUtil::Join(nodes, "\n");
102125
auto edge_lines = StringUtil::Join(edges, "\n");
103126

‎src/common/tree_renderer/html_tree_renderer.cpp‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,19 @@ static JSONMutableValue BuildNodeJSON(JSONWriter &writer, RenderTree &tree, idx_
160160
for (auto &child_pos : node.child_positions) {
161161
children.Append(BuildNodeJSON(writer, tree, child_pos.x, child_pos.y, total_time, has_timing));
162162
}
163+
for (auto &sub_plan : node.sub_plans) {
164+
if (!sub_plan.tree) {
165+
continue;
166+
}
167+
auto sub_plan_node = writer.CreateObject();
168+
sub_plan_node.AddString("name", "SUB-PLAN: " + sub_plan.label);
169+
sub_plan_node.AddString("kind", "generic");
170+
sub_plan_node.Add("details", writer.CreateArray());
171+
auto sub_plan_children = writer.CreateArray();
172+
sub_plan_children.Append(BuildNodeJSON(writer, *sub_plan.tree, 0, 0, total_time, has_timing));
173+
sub_plan_node.Add("children", sub_plan_children);
174+
children.Append(sub_plan_node);
175+
}
163176
obj.Add("children", children);
164177
return obj;
165178
}

‎src/common/tree_renderer/json_tree_renderer.cpp‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ static JSONMutableValue RenderRecursive(JSONWriter &writer, RenderTree &tree, id
8989
}
9090
}
9191
object.Add("extra_info", extra_info);
92+
auto sub_plans = writer.CreateArray();
93+
bool has_sub_plans = false;
94+
for (auto &sub_plan : node.sub_plans) {
95+
if (!sub_plan.tree) {
96+
continue;
97+
}
98+
auto sub_plan_object = writer.CreateObject();
99+
sub_plan_object.AddString("label", sub_plan.label);
100+
sub_plan_object.Add("tree", RenderRecursive(writer, *sub_plan.tree, 0, 0));
101+
sub_plans.Append(sub_plan_object);
102+
has_sub_plans = true;
103+
}
104+
if (has_sub_plans) {
105+
object.Add("sub_plans", sub_plans);
106+
}
92107
return object;
93108
}
94109

‎src/common/tree_renderer/mermaid_tree_renderer.cpp‎

Lines changed: 38 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -80,41 +80,59 @@ static string SanitizeMermaidLabel(const string &text) {
8080
return result;
8181
}
8282

83-
void MermaidTreeRenderer::ToStreamInternal(RenderTree &root, BaseTreeRenderer &ss) {
84-
vector<string> nodes;
85-
vector<string> edges;
86-
87-
const string node_format = " node_%d_%d[\"`**%s**%s`\"]";
83+
static string MermaidNodeName(const string &prefix, idx_t x, idx_t y) {
84+
if (prefix.empty()) {
85+
return StringUtil::Format("node_%d_%d", x, y);
86+
}
87+
return StringUtil::Format("node_%s_%d_%d", prefix, x, y);
88+
}
8889

89-
for (idx_t y = 0; y < root.height; y++) {
90-
for (idx_t x = 0; x < root.width; x++) {
91-
auto node = root.GetNode(x, y);
90+
static void RenderMermaidGraph(RenderTree &tree, const string &prefix, vector<string> &nodes, vector<string> &edges,
91+
idx_t &next_sub_plan) {
92+
for (idx_t y = 0; y < tree.height; y++) {
93+
for (idx_t x = 0; x < tree.width; x++) {
94+
auto node = tree.GetNode(x, y);
9295
if (!node) {
9396
continue;
9497
}
9598

96-
// Build node label with markdown formatting
9799
string extra_info;
98100
for (auto &item : node->extra_text) {
99-
auto &key = item.first;
100-
auto &value_raw = item.second;
101-
102-
auto value = QueryProfiler::JSONSanitize(value_raw);
103-
// Add newline and key-value pair
104-
extra_info += StringUtil::Format("\n\t%s: %s", key, SanitizeMermaidLabel(value));
101+
auto value = QueryProfiler::JSONSanitize(item.second);
102+
extra_info += StringUtil::Format("\n\t%s: %s", item.first, SanitizeMermaidLabel(value));
105103
}
106-
107-
// Create node with bold operator name and extra info (trim name to remove trailing spaces)
108104
auto trimmed_name = node->name;
109105
StringUtil::Trim(trimmed_name);
110-
nodes.push_back(StringUtil::Format(node_format, x, y, SanitizeMermaidLabel(trimmed_name), extra_info));
106+
auto node_name = MermaidNodeName(prefix, x, y);
107+
nodes.push_back(StringUtil::Format(" %s[\"`**%s**%s`\"]", node_name, SanitizeMermaidLabel(trimmed_name),
108+
extra_info));
111109

112-
// Create Edge(s)
113110
for (auto &coord : node->child_positions) {
114-
edges.push_back(StringUtil::Format(" node_%d_%d --> node_%d_%d", x, y, coord.x, coord.y));
111+
edges.push_back(
112+
StringUtil::Format(" %s --> %s", node_name, MermaidNodeName(prefix, coord.x, coord.y)));
113+
}
114+
for (auto &sub_plan : node->sub_plans) {
115+
if (!sub_plan.tree) {
116+
continue;
117+
}
118+
auto sub_plan_name = StringUtil::Format("subplan_%d", next_sub_plan++);
119+
vector<string> sub_plan_nodes;
120+
vector<string> sub_plan_edges;
121+
RenderMermaidGraph(*sub_plan.tree, sub_plan_name, sub_plan_nodes, sub_plan_edges, next_sub_plan);
122+
nodes.push_back(StringUtil::Format(
123+
" subgraph %s [\"%s\"]\n%s\n%s\n end", sub_plan_name, SanitizeMermaidLabel(sub_plan.label),
124+
StringUtil::Join(sub_plan_nodes, "\n\n"), StringUtil::Join(sub_plan_edges, "\n")));
125+
edges.push_back(StringUtil::Format(" %s -.-> %s", node_name, MermaidNodeName(sub_plan_name, 0, 0)));
115126
}
116127
}
117128
}
129+
}
130+
131+
void MermaidTreeRenderer::ToStreamInternal(RenderTree &root, BaseTreeRenderer &ss) {
132+
vector<string> nodes;
133+
vector<string> edges;
134+
idx_t next_sub_plan = 0;
135+
RenderMermaidGraph(root, string(), nodes, edges, next_sub_plan);
118136

119137
// Output Mermaid flowchart
120138
ss << "flowchart TD\n";

‎src/common/tree_renderer/text_tree_renderer.cpp‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1412,6 +1412,21 @@ void TextTreeRenderer::ToStreamInternal(RenderTree &root, BaseTreeRenderer &ss)
14121412
ss << "\n";
14131413
}
14141414
ss.max_render_width = MaxValue<idx_t>(ss.max_render_width, max_width);
1415+
for (idx_t y = 0; y < root.height; y++) {
1416+
for (idx_t x = 0; x < root.width; x++) {
1417+
auto node = root.GetNode(x, y);
1418+
if (!node) {
1419+
continue;
1420+
}
1421+
for (auto &sub_plan : node->sub_plans) {
1422+
if (!sub_plan.tree) {
1423+
continue;
1424+
}
1425+
ss.Render("\nSub-plan \"" + sub_plan.label + "\" of " + node->name + ":\n", TreeRenderType::HEADER);
1426+
TextTreeRenderer(config).ToStream(*sub_plan.tree, ss);
1427+
}
1428+
}
1429+
}
14151430
}
14161431

14171432
void TextTreeRenderer::Configure(const unordered_map<string, Value> &settings) {

‎src/common/tree_renderer/yaml_tree_renderer.cpp‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,20 @@ void YAMLTreeRenderer::RenderRecursive(RenderTree &node, BaseTreeRenderer &ss, i
129129
RenderRecursive(node, ss, indent + 2, child_pos.x, child_pos.y);
130130
}
131131
}
132+
133+
bool rendered_sub_plans = false;
134+
for (auto &sub_plan : current_node.sub_plans) {
135+
if (!sub_plan.tree) {
136+
continue;
137+
}
138+
if (!rendered_sub_plans) {
139+
ss << string(indent, ' ') << "sub_plans:\n";
140+
rendered_sub_plans = true;
141+
}
142+
ss << string(indent + 2, ' ') << "- label: " << EscapedString(sub_plan.label) << '\n';
143+
ss << string(indent + 4, ' ') << "tree:\n";
144+
RenderRecursive(*sub_plan.tree, ss, indent + 6, 0, 0);
145+
}
132146
}
133147

134148
} // namespace duckdb

‎src/execution/physical_operator.cpp‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ vector<const_reference<PhysicalOperator>> PhysicalOperator::GetChildren() const
5555
return result;
5656
}
5757

58+
vector<ExplainSubPlan> PhysicalOperator::GetExplainSubPlans() const {
59+
return {};
60+
}
61+
5862
void PhysicalOperator::SetEstimatedCardinality(InsertionOrderPreservingMap<string> &result,
5963
idx_t estimated_cardinality) {
6064
result[RenderTreeNode::ESTIMATED_CARDINALITY] = StringUtil::Format("%llu", estimated_cardinality);

‎src/include/duckdb/common/render_tree.hpp‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,16 @@ class LogicalOperator;
1919
class PhysicalOperator;
2020
class Pipeline;
2121
class ProfilingNode;
22+
struct RenderTree;
2223
struct PipelineRenderNode;
2324

25+
struct ExplainSubPlan {
26+
//! Caption shown above the sub-plan.
27+
string label;
28+
//! A render tree owned independently of the enclosing operator tree.
29+
unique_ptr<RenderTree> tree;
30+
};
31+
2432
struct RenderTreeNode {
2533
public:
2634
static constexpr const char *CARDINALITY = "__cardinality__";
@@ -50,6 +58,7 @@ struct RenderTreeNode {
5058
string name;
5159
InsertionOrderPreservingMap<string> extra_text;
5260
vector<Coordinate> child_positions;
61+
vector<ExplainSubPlan> sub_plans;
5362
};
5463

5564
struct RenderTree {

‎src/include/duckdb/execution/physical_operator.hpp‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class Pipeline;
3333
class PipelineBuildState;
3434
class MetaPipeline;
3535
class PhysicalPlan;
36+
struct ExplainSubPlan;
3637

3738
enum class TableFunctionParallelism : uint8_t;
3839
enum class OperatorCachingMode : uint8_t { NONE, PARTITIONED, ORDERED, UNORDERED };
@@ -81,6 +82,8 @@ class PhysicalOperator {
8182
const ProfilerPrintFormat &format = ProfilerPrintFormat::Default()) const;
8283
void Print() const;
8384
virtual vector<const_reference<PhysicalOperator>> GetChildren() const;
85+
//! Sub-plans owned by this operator that are not part of the enclosing plan tree.
86+
virtual vector<ExplainSubPlan> GetExplainSubPlans() const;
8487

8588
//! Return a vector of the types that will be returned by this operator
8689
const vector<LogicalType> &GetTypes() const {

0 commit comments

Comments
 (0)