From 8306fdd72aa412dfc6961f1464fdae2f55de31f4 Mon Sep 17 00:00:00 2001 From: Tam Nguyen Duc <1218621+tamnd@users.noreply.github.com> Date: Tue, 20 Oct 2026 11:38:14 +0700 Subject: [PATCH 1/3] Add the planner of EXPLAIN --- _typos.toml | 2 + crates/rupg-opt/Cargo.toml | 4 + crates/rupg-opt/src/cost.rs | 151 +++++ crates/rupg-opt/src/exprs.rs | 215 +++++++ crates/rupg-opt/src/from.rs | 994 ++++++++++++++++++++++++++++++++ crates/rupg-opt/src/lib.rs | 300 +++++++++- crates/rupg-opt/src/output.rs | 272 +++++++++ crates/rupg-opt/src/upper.rs | 809 ++++++++++++++++++++++++++ crates/rupg-plan/Cargo.toml | 2 + crates/rupg-plan/src/explain.rs | 600 +++++++++++++++++++ crates/rupg-plan/src/format.rs | 312 ++++++++++ crates/rupg-plan/src/lib.rs | 314 +++++++++- 12 files changed, 3972 insertions(+), 3 deletions(-) create mode 100644 crates/rupg-opt/src/cost.rs create mode 100644 crates/rupg-opt/src/exprs.rs create mode 100644 crates/rupg-opt/src/from.rs create mode 100644 crates/rupg-opt/src/output.rs create mode 100644 crates/rupg-opt/src/upper.rs create mode 100644 crates/rupg-plan/src/explain.rs create mode 100644 crates/rupg-plan/src/format.rs diff --git a/_typos.toml b/_typos.toml index ccf6bcf3..f4f13b52 100644 --- a/_typos.toml +++ b/_typos.toml @@ -26,6 +26,8 @@ dispell = "dispell" fals = "fals" valu = "valu" abd = "abd" +# The function `make_ands_implicit` of the planner. +ands = "ands" # Test inputs that are wrong on purpose. [default] diff --git a/crates/rupg-opt/Cargo.toml b/crates/rupg-opt/Cargo.toml index d4665c82..065bcf2f 100644 --- a/crates/rupg-opt/Cargo.toml +++ b/crates/rupg-opt/Cargo.toml @@ -14,6 +14,10 @@ readme.workspace = true publish = false [dependencies] +rupg-analyze.workspace = true +rupg-pgcatalog.workspace = true +rupg-plan.workspace = true +rupg-types.workspace = true [lints] workspace = true diff --git a/crates/rupg-opt/src/cost.rs b/crates/rupg-opt/src/cost.rs new file mode 100644 index 00000000..adf27d0d --- /dev/null +++ b/crates/rupg-opt/src/cost.rs @@ -0,0 +1,151 @@ +//! The estimates of `costsize.c` and `selfuncs.c` with no statistics: the number of rows of a table, the widths of the values, the selectivity of the conditions and the costs of the nodes. + +use rupg_analyze::{BoolOp, Expr, ExprKind, Relation}; +use rupg_pgcatalog::builtin; +use rupg_types::oid; + +use crate::exprs; + +/// `seq_page_cost`. +pub(crate) const SEQ_PAGE: f64 = 1.0; +/// `cpu_tuple_cost`. +pub(crate) const CPU_TUPLE: f64 = 0.01; +/// `cpu_operator_cost`. +pub(crate) const CPU_OPERATOR: f64 = 0.0025; +/// The number of pages that the planner takes for a table that `VACUUM` and `ANALYZE` have not seen, as `estimate_rel_size`. +const DEFAULT_PAGES: f64 = 10.0; +/// `BLCKSZ - SizeOfPageHeaderData`. +const PAGE_SPACE: i32 = 8168; +/// `MAXALIGN(SizeofHeapTupleHeader) + sizeof(ItemIdData)`. +const TUPLE_OVERHEAD: i32 = 28; +/// `DEFAULT_EQ_SEL`. +pub(crate) const EQ_SEL: f64 = 0.005; +/// `DEFAULT_INEQ_SEL`. +const INEQ_SEL: f64 = 1.0 / 3.0; +/// `DEFAULT_NUM_DISTINCT`. +pub(crate) const NUM_DISTINCT: f64 = 200.0; + +/// `get_typavgwidth`: the average width of a value of a type with a typmod. +pub(crate) fn type_width(ty: u32, typmod: i32) -> i32 { + if let Some(row) = builtin::type_by_oid(ty) + && row.len > 0 + { + return i32::from(row.len); + } + let max = match ty { + // `type_maximum_size`: four bytes for each character of UTF-8, and the header. + oid::BPCHAR | oid::VARCHAR if typmod > 4 => (typmod - 4) * 4 + 4, + oid::NUMERIC if typmod > 4 => { + let precision = ((typmod - 4) >> 16) & 0xffff; + 8 + (precision + 6) / 4 * 2 + } + _ => 0, + }; + if max > 0 { + if ty == oid::BPCHAR || max <= 32 { + return max; + } + return 32 + (max.min(1000) - 32) / 2; + } + 32 +} + +/// The width of a column of a relation, as `get_attavgwidth` with no statistics. A system column has the width of its type. +pub(crate) fn column_width(relation: &Relation, attnum: i16) -> i32 { + match attnum { + 0 => row_width(relation), + -1 => 6, + a if a < 0 => 4, + a => relation + .columns + .get(usize::try_from(a - 1).unwrap_or(usize::MAX)) + .map_or(32, |c| type_width(c.ty, c.typmod)), + } +} + +/// `get_rel_data_width`: the width of the columns of a relation that are not dropped. +pub(crate) fn row_width(relation: &Relation) -> i32 { + relation.columns.iter().filter(|c| c.ty != 0).map(|c| type_width(c.ty, c.typmod)).sum() +} + +/// The width of a value of an expression: the width of the column for a column, or else the width of its type. +pub(crate) fn expr_width(expr: &Expr, relations: &[Relation]) -> i32 { + match expr.kind { + ExprKind::Var(var) if var.levels_up == 0 => { + relations.get(var.relation).map_or(32, |r| column_width(r, var.attnum)) + } + _ => type_width(expr.ty, expr.typmod), + } +} + +/// `estimate_rel_size` for a table that has no pages yet: the pages and the rows. +pub(crate) fn table_size(relation: &Relation) -> (f64, f64) { + let density = (PAGE_SPACE / (row_width(relation) + TUPLE_OVERHEAD)).max(1); + (DEFAULT_PAGES, DEFAULT_PAGES * f64::from(density)) +} + +/// `clamp_row_est`: a number of rows that is a whole number and at least 1. +pub(crate) fn clamp_rows(rows: f64) -> f64 { + if rows <= 1.0 { 1.0 } else { rows.round() } +} + +/// `clause_selectivity` with no statistics: the fraction of the rows that pass a condition. +pub(crate) fn selectivity(expr: &Expr) -> f64 { + match &expr.kind { + ExprKind::Bool(BoolOp::And, args) => args.iter().map(selectivity).product(), + ExprKind::Bool(BoolOp::Or, args) => { + args.iter().map(selectivity).fold(0.0, |s1, s2| s1 + s2 - s1 * s2) + } + ExprKind::Bool(BoolOp::Not, args) => 1.0 - args.first().map_or(0.5, selectivity), + ExprKind::NullTest(_, true) => EQ_SEL, + ExprKind::NullTest(_, false) => 1.0 - EQ_SEL, + ExprKind::Const(_) if exprs::is_true(expr) => 1.0, + ExprKind::Const(_) => 0.0, + _ => match exprs::binary(expr) { + Some((op, _, _)) => operator_selectivity(op), + None => INEQ_SEL, + }, + } +} + +/// The selectivity of the estimator of an operator, `oprrest`, with no statistics. +fn operator_selectivity(op: u32) -> f64 { + let Some(row) = builtin::operator_by_oid(op) else { return INEQ_SEL }; + match row.name { + "=" => EQ_SEL, + "<>" => 1.0 - EQ_SEL, + "~~" | "~~*" | "~" | "~*" => EQ_SEL, + "!~~" | "!~~*" | "!~" | "!~*" => 1.0 - EQ_SEL, + _ => INEQ_SEL, + } +} + +/// The cost of the conditions for each row, as `cost_qual_eval`. +pub(crate) fn qual_cost<'a>(quals: impl IntoIterator) -> f64 { + quals.into_iter().map(|q| CPU_OPERATOR * exprs::calls(q) as f64).sum() +} + +/// `cost_sort`: the costs of a sort of `rows` rows that has the input costs `input`, with the number of rows of `LIMIT` when it has one. +pub(crate) fn sort(input: (f64, f64), rows: f64, bound: Option) -> (f64, f64) { + let tuples = rows.max(2.0); + let comparison = 2.0 * CPU_OPERATOR; + let startup = match bound { + Some(bound) if bound > 0.0 && tuples > 2.0 * bound => { + input.1 + comparison * tuples * (2.0 * bound).log2() + } + _ => input.1 + comparison * tuples * tuples.log2(), + }; + (startup, startup + CPU_OPERATOR * tuples) +} + +/// `estimate_num_groups` with no statistics: the number of groups of the rows for a number of keys. +pub(crate) fn groups(rows: f64, keys: usize) -> f64 { + let mut groups: f64 = 1.0; + for _ in 0..keys { + groups *= NUM_DISTINCT; + if groups >= rows { + break; + } + } + clamp_rows(groups.min(rows)) +} diff --git a/crates/rupg-opt/src/exprs.rs b/crates/rupg-opt/src/exprs.rs new file mode 100644 index 00000000..c4a919b9 --- /dev/null +++ b/crates/rupg-opt/src/exprs.rs @@ -0,0 +1,215 @@ +//! The facts about the expressions of a query that the planner reads: the relations that an expression reads, its volatile calls, and the parts of `AND`. + +use std::collections::BTreeSet; + +use rupg_analyze::{Aggref, BoolOp, Expr, ExprKind, FuncForm, Query, Var, WindowFunc}; +use rupg_pgcatalog::builtin; +use rupg_types::Value; + +/// The relations of the query that an expression reads, also in its subqueries, as `pull_varnos`. +pub(crate) fn relations(expr: &Expr) -> BTreeSet { + relations_at(expr, 0) +} + +/// The relations of the query that an expression reads, for an expression `levels` subqueries below the query. +pub(crate) fn relations_at(expr: &Expr, levels: usize) -> BTreeSet { + let mut out = BTreeSet::new(); + expr.find(0, &mut |e, depth| { + if let ExprKind::Var(var) = e.kind + && var.levels_up == depth + levels + { + out.insert(var.relation); + } + None::<()> + }); + out +} + +/// The columns of the query that an expression `levels` subqueries below the query reads, also in its subqueries, in the order that they first come, as `pull_var_clause`. Each column is a `Var` of the query. +pub(crate) fn columns_at(expr: &Expr, levels: usize, out: &mut Vec) { + expr.find(0, &mut |e, depth| { + if let ExprKind::Var(var) = e.kind + && var.levels_up == depth + levels + { + let var = Var { levels_up: 0, ..var }; + if !out.iter().any(|o| matches!(o.kind, ExprKind::Var(v) if v == var)) { + let mut column = Expr::new(ExprKind::Var(var), e.ty); + column.typmod = e.typmod; + out.push(column); + } + } + None::<()> + }); +} + +/// `contain_volatile_functions`: true when the expression calls a volatile function, also in a subquery. +pub(crate) fn volatile(expr: &Expr) -> bool { + let volatile = |func: u32| builtin::proc_by_oid(func).is_none_or(|p| p.volatile == b'v'); + expr.find(0, &mut |e, _| match &e.kind { + ExprKind::Func(f) if volatile(f.oid) => Some(()), + ExprKind::NullIf { equal: func, .. } + | ExprKind::Distinct { equal: func, .. } + | ExprKind::ScalarArrayOp { func, .. } + if volatile(*func) => + { + Some(()) + } + _ => None, + }) + .is_some() +} + +/// True when a query calls a volatile function in an expression of it or of its subqueries. +pub(crate) fn query_volatile(query: &Query) -> bool { + query.all_exprs(0).into_iter().any(|(e, _)| volatile(e)) +} + +/// The parts of a condition that `AND` joins, as `make_ands_implicit`. +pub(crate) fn and_parts<'q>(expr: &'q Expr, out: &mut Vec<&'q Expr>) { + match &expr.kind { + ExprKind::Bool(BoolOp::And, args) => args.iter().for_each(|a| and_parts(a, out)), + _ => out.push(expr), + } +} + +/// True for the constant `true`. +pub(crate) fn is_true(expr: &Expr) -> bool { + matches!(expr.as_const(), Some(Value::Bool(true))) +} + +/// True for the constant `false` or a null constant, which no row passes. +pub(crate) fn is_false(expr: &Expr) -> bool { + matches!(expr.as_const(), Some(Value::Bool(false) | Value::Null)) +} + +/// The OID of the operator of a call of an operator with two arguments, with the arguments. +pub(crate) fn binary(expr: &Expr) -> Option<(u32, &Expr, &Expr)> { + match &expr.kind { + ExprKind::Func(f) => match (f.form, f.args.as_slice()) { + (FuncForm::Operator(op), [left, right]) => Some((op, left, right)), + _ => None, + }, + _ => None, + } +} + +/// The call of the commutator of an operator, with the arguments in the other order, as `commute_op_clause`. +pub(crate) fn commute(expr: &Expr) -> Option { + let (op, left, right) = binary(expr)?; + let row = builtin::operator_by_oid(builtin::operator_by_oid(op)?.commutator)?; + let ExprKind::Func(f) = &expr.kind else { return None }; + let mut func = f.clone(); + func.oid = row.code; + func.form = FuncForm::Operator(row.oid); + func.args = vec![right.clone(), left.clone()]; + let mut out = Expr::new(ExprKind::Func(func), expr.ty); + out.location = expr.location; + Some(out) +} + +/// True when the operator can test a hash join, `oprcanhash`. +pub(crate) fn can_hash(op: u32) -> bool { + builtin::operator_by_oid(op).is_some_and(|row| row.can_hash) +} + +/// True when the operator can test a merge join, `oprcanmerge`, which makes the condition an equivalence of PostgreSQL. +pub(crate) fn can_merge(op: u32) -> bool { + builtin::operator_by_oid(op).is_some_and(|row| row.can_merge) +} + +/// The number of calls of functions and operators in an expression, outside its subqueries, which is the cost of the expression for each row in units of `cpu_operator_cost`, as `cost_qual_eval`. +pub(crate) fn calls(expr: &Expr) -> usize { + let own = usize::from(matches!( + expr.kind, + ExprKind::Func(_) + | ExprKind::NullIf { .. } + | ExprKind::Distinct { .. } + | ExprKind::ScalarArrayOp { .. } + | ExprKind::MinMax { .. } + | ExprKind::RowCompare(_) + )); + own + expr.children().into_iter().map(calls).sum::() +} + +/// The aggregate calls of the query in the expressions, outside the arguments of other aggregate calls, as `get_agg_clause_costs` finds them. A call in a subquery that reads only the query is a call of the query. +pub(crate) fn aggregates<'q>(exprs: &[&'q Expr], out: &mut Vec<&'q Aggref>) { + fn walk<'q>(expr: &'q Expr, depth: usize, out: &mut Vec<&'q Aggref>) { + if let ExprKind::Agg(agg) = &expr.kind + && agg.levels_up == depth + { + out.push(agg); + return; + } + for child in expr.children() { + walk(child, depth, out); + } + if let ExprKind::SubLink(sub) = &expr.kind { + for (e, d) in sub.query.all_exprs(depth + 1) { + walk(e, d, out); + } + } + } + for expr in exprs { + walk(expr, 0, out); + } +} + +/// The window function calls in an expression, outside its subqueries. +pub(crate) fn window_calls<'q>(expr: &'q Expr, out: &mut Vec<&'q WindowFunc>) { + if let ExprKind::Window(w) = &expr.kind { + out.push(w); + } + if matches!(expr.kind, ExprKind::SubLink(_)) { + return; + } + for child in expr.children() { + window_calls(child, out); + } +} + +/// True when the expression reads a column of the query, also in a subquery. +pub(crate) fn reads_columns(expr: &Expr) -> bool { + !relations(expr).is_empty() +} + +/// True when a query reads a column of the query that is `levels` levels outside it, as the parameters of a correlated subquery. +pub(crate) fn correlated(query: &Query, levels: usize) -> bool { + query.all_exprs(levels).into_iter().any(|(e, depth)| { + e.find(depth, &mut |e, depth| match e.kind { + ExprKind::Var(var) if var.levels_up == depth => Some(()), + ExprKind::Agg(ref agg) if agg.levels_up == depth => Some(()), + _ => None, + }) + .is_some() + }) +} + +/// The number of the references in a query and in its subqueries to the query of `WITH` with this name, other than the references of a recursive query to itself. +pub(crate) fn cte_references(query: &Query, name: &str) -> usize { + let mut count = 0; + for relation in &query.relations { + if let Some(cte) = &relation.cte + && cte.name == name + && cte.work.is_none() + { + count += 1; + } + if let Some(sub) = &relation.subquery { + count += cte_references(sub, name); + } + } + for expr in query.exprs() { + expr.find(0, &mut |e, _| { + if let ExprKind::SubLink(sub) = &e.kind { + count += cte_references(&sub.query, name); + } + None::<()> + }); + } + for cte in &query.ctes { + if cte.name != name { + count += cte_references(&cte.query, name); + } + } + count +} diff --git a/crates/rupg-opt/src/from.rs b/crates/rupg-opt/src/from.rs new file mode 100644 index 00000000..ae6e5571 --- /dev/null +++ b/crates/rupg-opt/src/from.rs @@ -0,0 +1,994 @@ +//! The plan of `FROM` and `WHERE`: the scan of each relation, the joins and the place of each condition, as `query_planner`, `make_one_rel` and `create_scan_plan` make them. + +use std::collections::{BTreeMap, BTreeSet}; + +use rupg_analyze::{BoolOp, BoolTest, Expr, ExprKind, FromItem, JoinKind, Relation}; +use rupg_pgcatalog::builtin; +use rupg_plan::{Cost, Item, Kind, Node, Replaced, SortKey}; +use rupg_types::{Value, oid}; + +use crate::{Builder, Level, cost, exprs, upper}; + +/// The rows of each relation of the query before and after its conditions, as `tuples` and `rows` of its `RelOptInfo`. +type Stats = BTreeMap; + +/// A condition of `WHERE` or of `ON` with the relations that it reads, as a `RestrictInfo`. +#[derive(Clone, Debug)] +struct Qual<'q> { + expr: &'q Expr, + rels: BTreeSet, + /// True for a condition of `ON` of an outer join that goes to the nullable side, which is not an equivalence of PostgreSQL. + outer: bool, +} + +/// The tree of the joins of `FROM`, as `deconstruct_jointree` sees it. +#[derive(Debug)] +enum Tree<'q> { + Base { relation: usize, quals: Vec> }, + Join(Box>), +} + +#[derive(Debug)] +struct JoinTree<'q> { + kind: JoinKind, + left: Tree<'q>, + right: Tree<'q>, + /// The parts of the condition of `ON`, before the planner gives each a place. + on: Vec<&'q Expr>, + /// The conditions of the join. + quals: Vec>, + /// The conditions above an outer join, which the join tests on its rows with the nulls. + filter: Vec>, + rels: BTreeSet, +} + +impl Tree<'_> { + fn rels(&self) -> BTreeSet { + match self { + Tree::Base { relation, .. } => BTreeSet::from([*relation]), + Tree::Join(join) => join.rels.clone(), + } + } +} + +/// A key of the order of the rows of a node: the expressions that have the same value at the key, as a `PathKey` of an equivalence class. +#[derive(Clone, Debug)] +pub(crate) struct OrderKey { + pub(crate) exprs: Vec, + pub(crate) desc: bool, + pub(crate) nulls_first: bool, +} + +impl OrderKey { + /// True when the key orders the rows by the expression in this direction. + pub(crate) fn sorts(&self, expr: &Expr, desc: bool, nulls_first: bool) -> bool { + self.desc == desc + && self.nulls_first == nulls_first + && self.exprs.iter().any(|e| e.same(expr)) + } +} + +/// The plan of a part of `FROM`, with the relations that it reads and the order of its rows. +#[derive(Debug)] +struct Rel<'q> { + node: Node<'q>, + rels: BTreeSet, + order: Vec, +} + +impl<'q> Builder<'q> { + /// `query_planner`: the plan of the rows of `FROM` that pass `WHERE`, with the order of the rows. + pub(crate) fn from(&mut self, level: &Level<'q>) -> (Node<'q>, Vec) { + let query = level.query; + let mut parts = Vec::new(); + if let Some(filter) = &query.filter { + exprs::and_parts(filter, &mut parts); + } + parts.extend(upper::having_split(query).0); + let trees = query.from.iter().map(tree); + let Some(mut tree) = + trees.reduce(|left, right| join_tree(JoinKind::Inner, left, right, Vec::new())) + else { + return (result(level.scope, parts), Vec::new()); + }; + let all = tree.rels(); + let mut gating = Vec::new(); + let mut quals = Vec::new(); + for part in parts { + let rels = exprs::relations(part); + let volatile = exprs::volatile(part); + if rels.is_empty() && !volatile { + if exprs::is_false(part) { + return (dummy(level.scope, &all), Vec::new()); + } + if !exprs::is_true(part) { + gating.push(Item::of(part)); + } + continue; + } + // A volatile condition with no column runs once for each row of the join of all the relations, as `distribute_qual_to_rels`. + let rels = if rels.is_empty() { all.clone() } else { rels }; + quals.push(Qual { expr: part, rels, outer: false }); + } + let mut nonnullable = BTreeSet::new(); + for qual in &quals { + nonnullable.extend(nonnullable_rels(qual.expr, true)); + } + reduce(&mut tree, &nonnullable); + place_on(&mut tree); + for qual in quals { + place(&mut tree, qual); + } + let mut stats = Stats::new(); + let rel = self.build(level, tree, &mut stats); + let mut node = rel.node; + if !gating.is_empty() { + // `create_gating_plan`: the `Result` has the costs of the node below. + let cost = node.cost; + node = Node::new( + Kind::Result { replaces: None, one_time: gating }, + level.scope, + vec![node], + ); + node.cost = cost; + } + (node, rel.order) + } + + /// The plan of a part of the tree of the joins. + fn build(&mut self, level: &Level<'q>, tree: Tree<'q>, stats: &mut Stats) -> Rel<'q> { + match tree { + Tree::Base { relation, quals } => self.scan(level, relation, quals, stats), + Tree::Join(join) => { + let JoinTree { kind, left, right, quals, filter, .. } = *join; + let left = self.build(level, left, stats); + let right = self.build(level, right, stats); + join_rels(level, kind, left, right, quals, filter, stats) + } + } + } + + /// The scan of a relation with its conditions, as `create_scan_plan`, with the costs of `cost_seqscan` and the other functions of `costsize.c` for each kind of scan. + fn scan( + &mut self, + level: &Level<'q>, + r: usize, + quals: Vec>, + stats: &mut Stats, + ) -> Rel<'q> { + let relation = &level.query.relations[r]; + let quals = order_quals(quals); + let selectivity: f64 = quals.iter().map(|q| cost::selectivity(q.expr)).product(); + let mut per_row = cost::CPU_TUPLE + cost::qual_cost(quals.iter().map(|q| q.expr)); + let mut children = Vec::new(); + let (kind, startup, run, tuples) = match &relation.cte { + Some(cte) if cte.work.is_some() => { + let rows = self.work.iter().rev().find(|(w, _)| Some(*w) == cte.work); + (Kind::WorkTableScan(r), 0.0, 0.0, rows.map_or(10.0, |(_, rows)| rows * 10.0)) + } + Some(cte) + if relation.subquery.is_none() + || cte.write.is_some() + || self.cte(&cte.name).is_some_and(|c| !c.inline) => + { + let rows = self.cte(&cte.name).map_or(1000.0, |c| c.rows); + (Kind::CteScan(r), 0.0, 0.0, rows) + } + _ => match (&relation.subquery, &relation.function, &relation.values) { + (Some(sub), _, _) => { + let child = self.query(sub, Some(level.scope)); + let c = child.cost; + children.push(child); + (Kind::SubqueryScan(r), c.startup, c.total - c.startup, c.rows) + } + (None, Some(function), _) => { + let startup = cost::qual_cost(function.calls.iter().map(|(call, _)| call)); + if relation.json_table().is_some() { + (Kind::TableFunctionScan(r), startup, 0.0, 100.0) + } else { + let rows = function.calls.iter().map(|(call, _)| function_rows(call)); + (Kind::FunctionScan(r), startup, 0.0, rows.fold(1.0, f64::max)) + } + } + (None, None, Some(values)) => { + per_row += cost::CPU_OPERATOR; + (Kind::ValuesScan(r), 0.0, 0.0, values.len() as f64) + } + (None, None, None) => { + let (pages, tuples) = cost::table_size(relation); + match &relation.tablesample { + Some(sample) => { + let fraction = sample + .args + .first() + .and_then(number) + .map_or(0.1, |p| (p / 100.0).clamp(0.0, 1.0)); + (Kind::SampleScan(r), 0.0, pages * cost::SEQ_PAGE, tuples * fraction) + } + None => (Kind::SeqScan(r), 0.0, pages * cost::SEQ_PAGE, tuples), + } + } + }, + }; + let rows = cost::clamp_rows(tuples * selectivity); + stats.insert(r, (tuples, rows)); + let mut node = Node::new(kind, level.scope, children); + node.filter = quals.iter().map(|q| Item::of(q.expr)).collect(); + node.cost = Cost { startup, total: startup + run + tuples * per_row, rows, width: 0 }; + Rel { node, rels: BTreeSet::from([r]), order: Vec::new() } + } +} + +/// The tree of a part of `FROM`. A right join is a left join with the sides in the other order. +fn tree(item: &FromItem) -> Tree<'_> { + match item { + FromItem::Relation(r) => Tree::Base { relation: *r, quals: Vec::new() }, + FromItem::Join(join) => { + let (mut left, mut right) = (tree(&join.left), tree(&join.right)); + let mut kind = join.kind; + if kind == JoinKind::Right { + std::mem::swap(&mut left, &mut right); + kind = JoinKind::Left; + } + let mut on = Vec::new(); + if let Some(cond) = &join.on { + exprs::and_parts(cond, &mut on); + } + join_tree(kind, left, right, on) + } + } +} + +fn join_tree<'q>(kind: JoinKind, left: Tree<'q>, right: Tree<'q>, on: Vec<&'q Expr>) -> Tree<'q> { + let mut rels = left.rels(); + rels.extend(right.rels()); + Tree::Join(Box::new(JoinTree { + kind, + left, + right, + on, + quals: Vec::new(), + filter: Vec::new(), + rels, + })) +} + +/// The `Result` of a query with no `FROM`, with the conditions of `WHERE` as its `One-Time Filter`. +fn result<'q>(scope: usize, parts: Vec<&'q Expr>) -> Node<'q> { + let parts: Vec<&Expr> = parts.into_iter().filter(|p| !exprs::is_true(p)).collect(); + if parts.iter().any(|p| exprs::is_false(p)) { + let mut node = Node::new( + Kind::Result { replaces: None, one_time: vec![false_item()] }, + scope, + Vec::new(), + ); + node.cost = Cost::default(); + return node; + } + let cost = cost::qual_cost(parts.iter().copied()); + let one_time = parts.into_iter().map(Item::of).collect(); + let mut node = Node::new(Kind::Result { replaces: None, one_time }, scope, Vec::new()); + node.cost = Cost { startup: 0.0, total: cost::CPU_TUPLE + cost, rows: 1.0, width: 0 }; + node +} + +/// The `Result` with no rows that takes the place of the relations when a condition of `WHERE` is never true, as `mark_dummy_rel`. +fn dummy<'q>(scope: usize, rels: &BTreeSet) -> Node<'q> { + let replaces = match rels.iter().collect::>().as_slice() { + [r] => Replaced::Scan(**r), + _ => Replaced::Join(rels.iter().copied().collect()), + }; + Node::new( + Kind::Result { replaces: Some(replaces), one_time: vec![false_item()] }, + scope, + Vec::new(), + ) +} + +/// The constant `false`. +fn false_item<'q>() -> Item<'q> { + Item::owned(Expr::new(ExprKind::Const(Value::Bool(false)), oid::BOOL)) +} + +/// A number of a constant. +fn number(expr: &Expr) -> Option { + match expr.strip_implicit().as_const()? { + Value::Int2(v) => Some(f64::from(*v)), + Value::Int4(v) => Some(f64::from(*v)), + Value::Int8(v) => Some(*v as f64), + Value::Float4(v) => Some(f64::from(*v)), + Value::Float8(v) => Some(*v), + _ => None, + } +} + +/// The number of rows of a function in `FROM`: the rows of `generate_series` with constant integers, as its support function gives them, or else `prorows`. +pub(crate) fn function_rows(call: &Expr) -> f64 { + let ExprKind::Func(f) = &call.kind else { return 1.0 }; + let name = builtin::proc_by_oid(f.oid).map(|p| p.name); + if name == Some("generate_series") { + let ints: Option> = f + .args + .iter() + .map(|a| matches!(a.ty, oid::INT4 | oid::INT8).then(|| number(a)).flatten()) + .collect(); + if let Some(ints) = ints { + let step = ints.get(2).copied().unwrap_or(1.0); + if let [start, stop, ..] = ints[..] + && step != 0.0 + { + return ((stop - start) / step).floor().max(-1.0) + 1.0; + } + } + } + match name { + _ if !f.retset => 1.0, + Some( + "unnest" | "json_array_elements" | "jsonb_array_elements" | "json_each" | "jsonb_each", + ) => 100.0, + Some("regexp_matches") => 10.0, + _ => 1000.0, + } +} + +/// `find_nonnullable_rels`: the relations whose columns cannot be null in a row that passes the condition. At the top of the condition, `IS NOT NULL` and the other tests that are false for a null count too. +fn nonnullable_rels(expr: &Expr, top: bool) -> BTreeSet { + let strict = |func: u32| builtin::proc_by_oid(func).is_some_and(|p| p.strict); + let union = |args: &[Expr], top: bool| { + let mut out = BTreeSet::new(); + for arg in args { + out.extend(nonnullable_rels(arg, top)); + } + out + }; + match &expr.kind { + ExprKind::Var(var) if var.levels_up == 0 => BTreeSet::from([var.relation]), + ExprKind::Bool(BoolOp::And, args) => union(args, top), + ExprKind::Bool(BoolOp::Or, args) => { + let mut sets = args.iter().map(|a| nonnullable_rels(a, top)); + let first = sets.next().unwrap_or_default(); + sets.fold(first, |acc, s| acc.intersection(&s).copied().collect()) + } + ExprKind::Bool(BoolOp::Not, args) => union(args, false), + ExprKind::Func(f) if strict(f.oid) => union(&f.args, false), + ExprKind::ScalarArrayOp { func, args, .. } if strict(*func) => union(args, false), + ExprKind::Relabel(arg, _) | ExprKind::CoerceViaIo(arg, _) => nonnullable_rels(arg, top), + ExprKind::NullTest(arg, false) if top => nonnullable_rels(arg, false), + ExprKind::BooleanTest( + arg, + BoolTest::IsTrue | BoolTest::IsFalse | BoolTest::IsNotUnknown, + ) if top => nonnullable_rels(arg, false), + _ => BTreeSet::new(), + } +} + +/// `reduce_outer_joins`: an outer join is an inner join when a condition above it is false or null for each row with nulls for the relations of its nullable side. +fn reduce(tree: &mut Tree<'_>, above: &BTreeSet) { + let Tree::Join(join) = tree else { return }; + let overlaps = |rels: BTreeSet| !rels.is_disjoint(above); + match join.kind { + JoinKind::Left if overlaps(join.right.rels()) => join.kind = JoinKind::Inner, + JoinKind::Full => match (overlaps(join.left.rels()), overlaps(join.right.rels())) { + (true, true) => join.kind = JoinKind::Inner, + (true, false) => join.kind = JoinKind::Left, + (false, true) => { + join.kind = JoinKind::Left; + std::mem::swap(&mut join.left, &mut join.right); + } + (false, false) => {} + }, + _ => {} + } + let mut local = BTreeSet::new(); + if join.kind != JoinKind::Full { + for part in &join.on { + local.extend(nonnullable_rels(part, true)); + } + } + let (left, right) = match join.kind { + JoinKind::Inner => { + local.extend(above.iter().copied()); + (local.clone(), local) + } + JoinKind::Left => (above.clone(), local), + _ => (BTreeSet::new(), BTreeSet::new()), + }; + reduce(&mut join.left, &left); + reduce(&mut join.right, &right); +} + +/// Gives each part of the condition of `ON` of each join its place. A part of a left join that reads only the nullable side goes to that side. +fn place_on<'q>(tree: &mut Tree<'q>) { + let Tree::Join(join) = tree else { return }; + place_on(&mut join.left); + place_on(&mut join.right); + let right = join.right.rels(); + for part in std::mem::take(&mut join.on) { + let rels = exprs::relations(part); + let volatile = exprs::volatile(part); + if rels.is_empty() && !volatile { + if !exprs::is_true(part) { + join.quals.push(Qual { expr: part, rels, outer: false }); + } + continue; + } + let rels = if rels.is_empty() { join.rels.clone() } else { rels }; + match join.kind { + JoinKind::Inner => place_join(join, Qual { expr: part, rels, outer: false }), + JoinKind::Left if rels.is_subset(&right) => { + place(&mut join.right, Qual { expr: part, rels, outer: true }); + } + _ => join.quals.push(Qual { expr: part, rels, outer: false }), + } + } +} + +/// Gives a condition from above its place in the tree: the scan of its relation, or the lowest join that has its relations. A condition on the nullable side of an outer join stays above the join. +fn place<'q>(tree: &mut Tree<'q>, qual: Qual<'q>) { + match tree { + Tree::Base { quals, .. } => quals.push(qual), + Tree::Join(join) => place_join(join, qual), + } +} + +fn place_join<'q>(join: &mut JoinTree<'q>, qual: Qual<'q>) { + match join.kind { + JoinKind::Inner if qual.rels.is_subset(&join.left.rels()) => place(&mut join.left, qual), + JoinKind::Inner if qual.rels.is_subset(&join.right.rels()) => place(&mut join.right, qual), + JoinKind::Inner => join.quals.push(qual), + JoinKind::Left if qual.rels.is_subset(&join.left.rels()) => place(&mut join.left, qual), + _ => join.filter.push(qual), + } +} + +/// True for a condition that PostgreSQL keeps in an equivalence class: an `=` that a merge join can use, with no volatile function, that is not a condition of an outer join. +fn equivalence(qual: &Qual<'_>) -> bool { + !qual.outer + && !exprs::volatile(qual.expr) + && exprs::binary(qual.expr).is_some_and(|(op, _, _)| exprs::can_merge(op)) +} + +/// The conditions of a scan in the order of `order_qual_clauses`: the conditions that are not equivalences, then the equivalences, which the planner makes again, then by their cost for each row. +fn order_quals(quals: Vec>) -> Vec> { + let (mut out, classes): (Vec<_>, Vec<_>) = quals.into_iter().partition(|q| !equivalence(q)); + out.extend(classes); + out.sort_by_key(|q| exprs::calls(q.expr)); + out +} + +/// A condition of a join that tests a value of the outer side and a value of the inner side with an operator, with the outer value on the left. +struct Clause<'q> { + item: Item<'q>, + outer: Expr, + inner: Expr, +} + +/// The condition with the value of `outer` on the left, when the condition compares a value of each side with an operator that `usable` accepts. +fn clause<'q>( + qual: &Qual<'q>, + outer: &BTreeSet, + inner: &BTreeSet, + usable: fn(u32) -> bool, +) -> Option> { + let (op, left, right) = exprs::binary(qual.expr)?; + if !usable(op) || exprs::volatile(qual.expr) { + return None; + } + let (l, r) = (exprs::relations(left), exprs::relations(right)); + if l.is_empty() || r.is_empty() { + return None; + } + if l.is_subset(outer) && r.is_subset(inner) { + return Some(Clause { + item: Item::of(qual.expr), + outer: left.clone(), + inner: right.clone(), + }); + } + if l.is_subset(inner) && r.is_subset(outer) { + let commuted = exprs::commute(qual.expr)?; + return Some(Clause { + item: Item::owned(commuted), + outer: right.clone(), + inner: left.clone(), + }); + } + None +} + +/// The number of distinct values of an expression with no statistics, as `get_variable_numdistinct`: the rows of a small table, or else 200, and at most the rows of the relation after its conditions. The flag is true for the default. +fn distinct(expr: &Expr, stats: &Stats) -> (f64, bool) { + if let ExprKind::Var(var) = expr.strip_implicit().kind + && var.levels_up == 0 + && let Some(&(tuples, rows)) = stats.get(&var.relation) + { + if var.attnum < 0 && var.attnum != -6 { + // A system column other than `tableoid` is unique. + return (rows, false); + } + if tuples < cost::NUM_DISTINCT { + return (cost::clamp_rows(tuples).min(rows.max(1.0)), false); + } + return (cost::NUM_DISTINCT.min(rows.max(1.0)), true); + } + (cost::NUM_DISTINCT, true) +} + +/// The selectivity of a condition of a join: `1 / max(nd1, nd2)` for an `=` of a value of each side, as `eqjoinsel` with no statistics, or else the selectivity of the condition. +fn join_selectivity(qual: &Qual<'_>, stats: &Stats) -> f64 { + if let Some((op, left, right)) = exprs::binary(qual.expr) + && builtin::operator_by_oid(op).is_some_and(|o| o.name == "=") + && exprs::reads_columns(left) + && exprs::reads_columns(right) + { + let (l, _) = distinct(left, stats); + let (r, _) = distinct(right, stats); + return 1.0 / l.max(r); + } + cost::selectivity(qual.expr) +} + +/// The kinds of node that read the rows of the node below once and keep them, so that a scan again costs little, as `cost_rescan`. +fn keeps_rows(kind: &Kind<'_>) -> bool { + matches!( + kind, + Kind::FunctionScan(_) + | Kind::TableFunctionScan(_) + | Kind::CteScan(_) + | Kind::WorkTableScan(_) + | Kind::Materialize + | Kind::Sort(_) + ) +} + +/// The costs of a scan of the inner side of a nested loop after the first, as `cost_rescan`: the start and the total. +fn rescan(node: &Node<'_>) -> (f64, f64) { + let c = node.cost; + match node.kind { + Kind::Materialize | Kind::Sort(_) => (0.0, cost::CPU_OPERATOR * c.rows), + Kind::FunctionScan(_) + | Kind::TableFunctionScan(_) + | Kind::CteScan(_) + | Kind::WorkTableScan(_) => (0.0, c.total - c.startup), + _ => (c.startup, c.total), + } +} + +/// A `Material` node over a node, as `cost_material`. +pub(crate) fn materialize(child: Node<'_>) -> Node<'_> { + let c = child.cost; + let mut node = Node::new(Kind::Materialize, child.scope, vec![child]); + node.cost = + Cost { startup: c.startup, total: c.total + 2.0 * cost::CPU_OPERATOR * c.rows, ..c }; + node +} + +/// A `Sort` of a node on keys, as `cost_sort`. +pub(crate) fn sort<'q>(child: Node<'q>, keys: Vec>, bound: Option) -> Node<'q> { + let c = child.cost; + let (startup, total) = cost::sort((c.startup, c.total), c.rows, bound); + let mut node = Node::new(Kind::Sort(keys), child.scope, vec![child]); + node.cost = Cost { startup, total, ..c }; + node +} + +/// True when the relations of a node read the relations of another side, as a lateral reference. +fn lateral(level: &Level<'_>, rels: &BTreeSet, other: &BTreeSet) -> bool { + rels.iter().any(|r| !lateral_rels(level, *r).is_disjoint(other)) +} + +/// The relations of the query that the expressions of a relation of `FROM` read: its function, its `VALUES` lists or the columns of the query in its subquery. +pub(crate) fn lateral_rels(level: &Level<'_>, r: usize) -> BTreeSet { + let mut out = BTreeSet::new(); + for expr in lateral_exprs(&level.query.relations[r]) { + out.extend(exprs::relations_at(expr.0, expr.1)); + } + out.remove(&r); + out +} + +/// The relations of the query below a node of the query, and the relations that a `Result` takes the place of. +pub(crate) fn node_rels(node: &Node<'_>) -> BTreeSet { + let mut out = BTreeSet::new(); + match &node.kind { + Kind::SeqScan(r) + | Kind::SampleScan(r) + | Kind::FunctionScan(r) + | Kind::TableFunctionScan(r) + | Kind::ValuesScan(r) + | Kind::SubqueryScan(r) + | Kind::CteScan(r) + | Kind::WorkTableScan(r) => { + out.insert(*r); + return out; + } + Kind::Result { replaces: Some(Replaced::Scan(r)), .. } => { + out.insert(*r); + } + Kind::Result { replaces: Some(Replaced::Join(rels)), .. } => { + out.extend(rels.iter().copied()) + } + _ => {} + } + for child in &node.children { + if child.scope == node.scope { + out.extend(node_rels(child)); + } + } + out +} + +/// The expressions of a relation of `FROM` that can read the other relations of the query, each with its depth below the query. +pub(crate) fn lateral_exprs(relation: &Relation) -> Vec<(&Expr, usize)> { + let mut out = Vec::new(); + if let Some(function) = &relation.function { + out.extend(function.calls.iter().map(|(call, _)| (call, 0))); + } + if let Some(values) = &relation.values { + out.extend(values.iter().flatten().map(|e| (e, 0))); + } + if relation.lateral + && let Some(sub) = &relation.subquery + { + out.extend(sub.all_exprs(1)); + } + out +} + +/// A join of two plans, with the costs of each kind of join for each order of the sides, as `add_paths_to_joinrel`. The plan with the lowest total cost wins. +fn join_rels<'q>( + level: &Level<'q>, + kind: JoinKind, + left: Rel<'q>, + right: Rel<'q>, + quals: Vec>, + filter: Vec>, + stats: &Stats, +) -> Rel<'q> { + let mut rels = left.rels.clone(); + rels.extend(right.rels.iter().copied()); + let selectivity: f64 = quals.iter().map(|q| join_selectivity(q, stats)).product(); + let mut rows = left.node.cost.rows * right.node.cost.rows * selectivity; + match kind { + JoinKind::Left => rows = rows.max(left.node.cost.rows), + JoinKind::Full => rows = rows.max(left.node.cost.rows).max(right.node.cost.rows), + _ => {} + } + let filter_selectivity: f64 = filter.iter().map(|q| cost::selectivity(q.expr)).product(); + let rows = cost::clamp_rows(rows * filter_selectivity); + let mut quals = quals; + quals.sort_by_key(|q| exprs::calls(q.expr)); + let filter_cost = cost::qual_cost(filter.iter().map(|q| q.expr)); + let lateral_right = lateral(level, &right.rels, &left.rels); + // The orders of the sides: the left side first, and the right side first when the join can have the sides in the other order. + let mut orders = vec![false]; + if !lateral_right && kind != JoinKind::Left || kind == JoinKind::Left && !lateral_right { + orders.push(true); + } + let mut best: Option> = None; + for swapped in orders { + let (outer, inner) = if swapped { (&right, &left) } else { (&left, &right) }; + let join = match (kind, swapped) { + (JoinKind::Left, true) => JoinKind::Right, + (k, _) => k, + }; + let input = Input { outer, inner, quals: &quals, filter_cost, stats }; + let nested = kind == JoinKind::Inner || !swapped && kind == JoinKind::Left; + let candidates = [ + merge_join(&input, join), + if nested { Some(nested_loop(&input, join, lateral_right)) } else { None }, + hash_join(&input, join), + ]; + for candidate in candidates.into_iter().flatten() { + if best.as_ref().is_none_or(|b| candidate.total < b.total) { + best = Some(Candidate { swapped, ..candidate }); + } + } + } + let Some(best) = best else { + return nested_join(level, kind, left, right, quals, filter, rows, rels); + }; + let (outer, inner) = if best.swapped { (right, left) } else { (left, right) }; + let mut out = best.make(level.scope, outer, inner, &quals, rows); + out.node.filter = filter.iter().map(|q| Item::of(q.expr)).collect(); + out.rels = rels; + out +} + +/// A full join with no condition that a merge join or a hash join can use, which PostgreSQL cannot plan. The plan is a nested loop. +#[allow(clippy::too_many_arguments)] +fn nested_join<'q>( + level: &Level<'q>, + kind: JoinKind, + left: Rel<'q>, + right: Rel<'q>, + quals: Vec>, + filter: Vec>, + rows: f64, + rels: BTreeSet, +) -> Rel<'q> { + let c = (left.node.cost, right.node.cost); + let inner = materialize(right.node); + let join_filter = quals.iter().map(|q| Item::of(q.expr)).collect(); + let mut node = Node::new( + Kind::NestedLoop { join: kind, join_filter }, + level.scope, + vec![left.node, inner], + ); + node.filter = filter.iter().map(|q| Item::of(q.expr)).collect(); + let total = c.0.total + c.1.total + c.0.rows * c.1.rows * cost::CPU_TUPLE; + node.cost = Cost { startup: c.0.startup + c.1.startup, total, rows, width: 0 }; + Rel { node, rels, order: Vec::new() } +} + +/// The parts of a join that each kind of join reads. +struct Input<'a, 'q> { + outer: &'a Rel<'q>, + inner: &'a Rel<'q>, + quals: &'a [Qual<'q>], + filter_cost: f64, + stats: &'a Stats, +} + +/// A kind of join for an order of the sides, with its cost. +struct Candidate<'q> { + method: Method, + join: JoinKind, + swapped: bool, + startup: f64, + total: f64, + /// For a merge join, true when the inner side needs a `Material` node; for a nested loop, true when it gets one. + materialize: bool, + /// For a merge join, true when each side needs a `Sort`. + sort: (bool, bool), + _marker: std::marker::PhantomData<&'q ()>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum Method { + Nested, + Merge, + Hash, +} + +/// The conditions of a join that a kind of join tests itself, with the outer value on the left, and the other conditions. +fn split<'q>( + quals: &[Qual<'q>], + outer: &BTreeSet, + inner: &BTreeSet, + usable: fn(u32) -> bool, +) -> (Vec>, Vec>) { + let mut clauses = Vec::new(); + let mut other = Vec::new(); + for qual in quals { + match clause(qual, outer, inner, usable) { + Some(c) => clauses.push(c), + None => other.push(qual.clone()), + } + } + (clauses, other) +} + +/// The number of rows of the join that pass the conditions, as `approx_tuple_count`. +fn tuple_count(input: &Input<'_, '_>, clauses: &[Clause<'_>]) -> f64 { + let mut selectivity = 1.0; + for c in clauses { + let (l, _) = distinct(&c.outer, input.stats); + let (r, _) = distinct(&c.inner, input.stats); + selectivity /= l.max(r); + } + cost::clamp_rows(input.outer.node.cost.rows * input.inner.node.cost.rows * selectivity) +} + +/// The costs of a nested loop, as `initial_cost_nestloop` and `final_cost_nestloop`. The inner side gets a `Material` node unless it keeps its rows already or reads the outer side. +fn nested_loop<'q>(input: &Input<'_, 'q>, join: JoinKind, lateral: bool) -> Candidate<'q> { + let o = input.outer.node.cost; + let materialize = !lateral && !keeps_rows(&input.inner.node.kind); + let (inner, rescan) = if materialize { + let i = input.inner.node.cost; + let total = i.total + 2.0 * cost::CPU_OPERATOR * i.rows; + (Cost { total, ..i }, (0.0, cost::CPU_OPERATOR * i.rows)) + } else { + (input.inner.node.cost, rescan(&input.inner.node)) + }; + let startup = o.startup + inner.startup; + let mut run = o.total - o.startup + inner.total - inner.startup; + if o.rows > 1.0 { + run += (o.rows - 1.0) * rescan.1; + } + let per_tuple = + cost::CPU_TUPLE + cost::qual_cost(input.quals.iter().map(|q| q.expr)) + input.filter_cost; + run += per_tuple * o.rows * inner.rows; + Candidate { + method: Method::Nested, + join, + swapped: false, + startup, + total: startup + run, + materialize, + sort: (false, false), + _marker: std::marker::PhantomData, + } +} + +/// The costs of a hash join with a hash table of the inner side, as `initial_cost_hashjoin` and `final_cost_hashjoin`. +fn hash_join<'q>(input: &Input<'_, 'q>, join: JoinKind) -> Option> { + let (clauses, other) = + split(input.quals, &input.outer.rels, &input.inner.rels, exprs::can_hash); + if clauses.is_empty() { + return None; + } + let (o, i) = (input.outer.node.cost, input.inner.node.cost); + let n = clauses.len() as f64; + let startup = o.startup + i.total + (cost::CPU_OPERATOR * n + cost::CPU_TUPLE) * i.rows; + let mut run = o.total - o.startup + cost::CPU_OPERATOR * n * o.rows; + // `estimate_hash_bucket_stats`: the fraction of the inner rows in a bucket, 0.1 with no statistics. + let bucket = clauses + .iter() + .map(|c| match distinct(&c.inner, input.stats) { + (_, true) => 0.1, + (nd, false) => 1.0 / nd.clamp(1.0, 1024.0).max(1.0), + }) + .fold(1.0, f64::min); + run += cost::CPU_OPERATOR * n * o.rows * cost::clamp_rows(i.rows * bucket) * 0.5; + let per_tuple = + cost::CPU_TUPLE + cost::qual_cost(other.iter().map(|q| q.expr)) + input.filter_cost; + run += per_tuple * tuple_count(input, &clauses); + Some(Candidate { + method: Method::Hash, + join, + swapped: false, + startup, + total: startup + run, + materialize: false, + sort: (false, false), + _marker: std::marker::PhantomData, + }) +} + +/// True when the order of the rows of a plan is the order of the values of the clauses, from the first key. +fn ordered(order: &[OrderKey], values: &[&Expr]) -> bool { + values.len() <= order.len() && values.iter().zip(order).all(|(v, k)| k.sorts(v, false, false)) +} + +/// The costs of a merge join of the two sides in the order of the values of the `=` conditions, as `initial_cost_mergejoin` and `final_cost_mergejoin`. A side that is not in that order gets a `Sort`. +fn merge_join<'q>(input: &Input<'_, 'q>, join: JoinKind) -> Option> { + let (clauses, other) = + split(input.quals, &input.outer.rels, &input.inner.rels, exprs::can_merge); + if clauses.is_empty() { + return None; + } + let outer_values: Vec<&Expr> = clauses.iter().map(|c| &c.outer).collect(); + let inner_values: Vec<&Expr> = clauses.iter().map(|c| &c.inner).collect(); + let sort_outer = !ordered(&input.outer.order, &outer_values); + let sort_inner = !ordered(&input.inner.order, &inner_values); + let side = |rel: &Rel<'_>, sorted: bool| { + let c = rel.node.cost; + if sorted { cost::sort((c.startup, c.total), c.rows, None) } else { (c.startup, c.total) } + }; + let (o, i) = (side(input.outer, sort_outer), side(input.inner, sort_inner)); + let (outer_rows, inner_rows) = (input.outer.node.cost.rows, input.inner.node.cost.rows); + let tuples = tuple_count(input, &clauses); + let rescanned = (tuples - inner_rows).max(0.0); + let ratio = 1.0 + rescanned / inner_rows.max(1.0); + let inner_run = i.1 - i.0; + let startup = o.0 + i.0; + let mut run = o.1 - o.0; + let material = inner_run + cost::CPU_OPERATOR * inner_rows * ratio; + let bare = inner_run * ratio; + // The inner side needs a `Material` node when it is cheaper, or when its node cannot go back to a row that it gave, as `ExecSupportsMarkRestore`. + let materialize = material < bare + || !sort_inner + && !matches!( + input.inner.node.kind, + Kind::Sort(_) | Kind::Materialize | Kind::Result { .. } + ); + run += if materialize { material } else { bare }; + let n = clauses.len() as f64; + run += cost::CPU_OPERATOR * n * (outer_rows + inner_rows * ratio); + let per_tuple = + cost::CPU_TUPLE + cost::qual_cost(other.iter().map(|q| q.expr)) + input.filter_cost; + run += per_tuple * tuples; + Some(Candidate { + method: Method::Merge, + join, + swapped: false, + startup, + total: startup + run, + materialize, + sort: (sort_outer, sort_inner), + _marker: std::marker::PhantomData, + }) +} + +/// The sort keys of the values of the clauses of a merge join, in ascending order. +fn merge_keys<'q>(values: Vec) -> Vec> { + values + .into_iter() + .map(|v| SortKey { + item: Item { expr: std::borrow::Cow::Owned(v), below: true }, + desc: false, + using: None, + nulls_first: false, + }) + .collect() +} + +impl<'q> Candidate<'q> { + /// The plan of the join. + fn make( + &self, + scope: usize, + outer: Rel<'q>, + inner: Rel<'q>, + quals: &[Qual<'q>], + rows: f64, + ) -> Rel<'q> { + let mut order = Vec::new(); + let node = match self.method { + Method::Nested => { + let inner_node = + if self.materialize { materialize(inner.node) } else { inner.node }; + let join_filter = quals.iter().map(|q| Item::of(q.expr)).collect(); + order = outer.order; + Node::new( + Kind::NestedLoop { join: self.join, join_filter }, + scope, + vec![outer.node, inner_node], + ) + } + Method::Hash => { + let (clauses, other) = split(quals, &outer.rels, &inner.rels, exprs::can_hash); + let c = inner.node.cost; + let mut hash = Node::new(Kind::Hash, scope, vec![inner.node]); + hash.cost = Cost { startup: c.total, ..c }; + let cond = clauses.into_iter().map(|c| c.item).collect(); + let join_filter = other.iter().map(|q| Item::of(q.expr)).collect(); + Node::new( + Kind::HashJoin { join: self.join, cond, join_filter }, + scope, + vec![outer.node, hash], + ) + } + Method::Merge => { + let (clauses, other) = split(quals, &outer.rels, &inner.rels, exprs::can_merge); + let outer_values: Vec = clauses.iter().map(|c| c.outer.clone()).collect(); + let inner_values: Vec = clauses.iter().map(|c| c.inner.clone()).collect(); + if self.join != JoinKind::Full { + order = outer_values + .iter() + .zip(&inner_values) + .map(|(o, i)| OrderKey { + exprs: vec![o.clone(), i.clone()], + desc: false, + nulls_first: false, + }) + .collect(); + } + let outer_node = if self.sort.0 { + sort(outer.node, merge_keys(outer_values), None) + } else { + outer.node + }; + let mut inner_node = if self.sort.1 { + sort(inner.node, merge_keys(inner_values), None) + } else { + inner.node + }; + if self.materialize { + inner_node = materialize(inner_node); + } + let cond = clauses.into_iter().map(|c| c.item).collect(); + let join_filter = other.iter().map(|q| Item::of(q.expr)).collect(); + Node::new( + Kind::MergeJoin { join: self.join, cond, join_filter }, + scope, + vec![outer_node, inner_node], + ) + } + }; + let mut node = node; + node.cost = Cost { startup: self.startup, total: self.total, rows, width: 0 }; + Rel { node, rels: BTreeSet::new(), order } + } +} diff --git a/crates/rupg-opt/src/lib.rs b/crates/rupg-opt/src/lib.rs index 2dcab58f..36610cc3 100644 --- a/crates/rupg-opt/src/lib.rs +++ b/crates/rupg-opt/src/lib.rs @@ -1,5 +1,301 @@ -//! The optimizer, statistics, `ANALYZE`, the cost model, join order, the use of summaries. +//! The planner: the plan of a query, as `planner.c` and `createplan.c` make it (document 15 section 15.9). //! -//! This crate first ships in milestone M3. See `spec/22-crate-layout.md` section 22.4 and `spec/23-milestones.md`. +//! [`plan`] turns the query tree of the analyzer into a [`Plan`] of `rupg-plan`. The planner has no statistics and no indexes yet, so it chooses its nodes as PostgreSQL does for tables that `ANALYZE` has not seen: a sequential scan of each table, the join of the lowest cost of a merge join, a hash join and a nested loop, and the estimates of `costsize.c` with the defaults of `selfuncs.c`. #![forbid(unsafe_code)] + +mod cost; +mod exprs; +mod from; +mod output; +mod upper; + +use rupg_analyze::{Cte, Expr, ExprKind, FromItem, Query, SubLink, SubLinkKind, WriteKind}; +use rupg_plan::{Item, Kind, Label, Node, Plan, Scope, SubPlan}; + +/// The plan of a query. +pub fn plan(query: &Query) -> Plan<'_> { + let mut b = Builder::default(); + let mut root = b.query(query, None); + let mut pending = std::mem::take(&mut b.pending); + attach(&mut root, &mut pending); + // A subplan that no node holds, such as one in an expression that the planner made again, goes to the top node. + root.sub_plans.extend(pending.into_iter().map(|(_, sub)| sub)); + let mut order = std::mem::take(&mut b.current); + for group in b.groups { + order.extend(group); + } + Plan { root, scopes: b.scopes, sublinks: b.labels, order } +} + +/// The state of the planner for a statement, as `PlannerGlobal`. +#[derive(Debug, Default)] +struct Builder<'q> { + scopes: Vec>, + labels: Vec>, + /// The number of subplans of each kind of subquery so far, for their names, as `choose_plan_name`. + counts: [usize; 6], + /// The scopes of the subplan that the planner makes now, in the order of the range table. + current: Vec, + /// The scopes of each subplan that is done, in the order that the subplans end. + groups: Vec>, + /// The subplans that run for each row, which go to the node that holds their subquery. + pending: Vec<(&'q SubLink, SubPlan<'q>)>, + /// The queries of `WITH` that the query and the queries outside it give. + ctes: Vec>, + /// The rows of the part of each recursive query that is not recursive, by the number of its work table, for the estimate of a `WorkTableScan`. + work: Vec<(usize, f64)>, +} + +/// A query of `WITH` in the scope of the planner. +#[derive(Debug)] +struct CteUse<'q> { + cte: &'q Cte, + /// True when each reference reads its own copy of the query as a subquery, as `inline_cte`. Else the plan runs the query once as an `InitPlan`, and each reference is a `CteScan`. + inline: bool, + /// The rows of the plan of a query that is not inline. + rows: f64, +} + +/// A query that the planner plans, as `PlannerInfo`. +#[derive(Debug)] +pub(crate) struct Level<'q> { + pub(crate) scope: usize, + pub(crate) query: &'q Query, + /// The scope and the query of the expressions of the nodes above a set operation: the leftmost `SELECT` of the operation, as `EXPLAIN` shows them. For another query it is the query. + pub(crate) display: usize, + pub(crate) display_query: &'q Query, + /// The subplans that run once before the top node: the queries of `WITH` and the subqueries with no reference to the query. + pub(crate) init: Vec>, +} + +impl<'q> Level<'q> { + /// The expression of a target of the query of the display scope. + pub(crate) fn target(&self, index: usize) -> &'q Expr { + &self.display_query.targets[index].expr + } +} + +/// The query of the rows of a statement with the conditions of check constraints, which `with_checks` of the analyzer puts around the query of the rows. +fn unwrap_checks(query: &Query) -> &Query { + let Some(write) = &query.write else { return query }; + match query.relations.as_slice() { + [relation] + if !write.checks.is_empty() && relation.oid == 0 && relation.name == write.name => + { + relation.subquery.as_deref().unwrap_or(query) + } + _ => query, + } +} + +impl<'q> Builder<'q> { + /// `subquery_planner`: the plan of a query, whose scope is in the scope `parent`. + fn query(&mut self, outer: &'q Query, parent: Option) -> Node<'q> { + let write = outer.write.as_ref(); + let query = unwrap_checks(outer); + let target = write.is_some_and(|w| w.kind == WriteKind::Insert); + let scope = self.push_scope(query, parent, target); + let mut level = + Level { scope, query, display: scope, display_query: query, init: Vec::new() }; + let depth = self.ctes.len(); + if !std::ptr::eq(outer, query) { + self.ctes_of(outer, &mut level); + } + self.ctes_of(query, &mut level); + for expr in preprocess_order(query) { + self.sublinks_in(expr, &mut level); + } + let (mut node, order) = match &query.set_op { + Some(op) => (self.set_op(&mut level, op), Vec::new()), + None => self.from(&level), + }; + node = self.upper(&level, node, order); + if let Some(write) = write { + node = self.modify(&level, write, node); + } + output::level(&level, &mut node); + let mut init = std::mem::take(&mut level.init); + init.append(&mut node.init_plans); + node.init_plans = init; + self.ctes.truncate(depth); + node + } + + /// A new scope in the current subplan. + fn push_scope(&mut self, query: &'q Query, parent: Option, target: bool) -> usize { + self.scopes.push(Scope { query, parent, target }); + let index = self.scopes.len() - 1; + self.current.push(index); + index + } + + /// The plan of a subplan, whose scopes take part in the names of the relations after the scopes of the statement. + fn subplan(&mut self, f: impl FnOnce(&mut Self) -> Node<'q>) -> Node<'q> { + let saved = std::mem::take(&mut self.current); + let node = f(self); + let group = std::mem::replace(&mut self.current, saved); + self.groups.push(group); + node + } + + /// `SS_process_ctes`: the queries of `WITH` of a query. A query that only one reference reads and that has no volatile function goes into the reference, as `inline_cte`. A `SELECT` that no reference reads has no plan. + fn ctes_of(&mut self, query: &'q Query, level: &mut Level<'q>) { + for cte in &query.ctes { + let select = cte.write.is_none(); + let refs = exprs::cte_references(query, &cte.name); + if select && refs == 0 { + continue; + } + let inline = select + && !cte.recursive + && cte.materialized != Some(true) + && (refs == 1 || cte.materialized == Some(false)) + && !exprs::query_volatile(&cte.query); + let mut rows = 1000.0; + if !inline { + let scope = level.scope; + let plan = self.subplan(|b| b.query(&cte.query, Some(scope))); + rows = plan.cost.rows; + level.init.push(SubPlan { name: format!("CTE {}", cte.name), plan }); + } + self.ctes.push(CteUse { cte, inline, rows }); + } + } + + /// The query of `WITH` that a reference names, as the query that holds the reference sees it. + pub(crate) fn cte(&self, name: &str) -> Option<&CteUse<'q>> { + self.ctes.iter().rev().find(|c| c.cte.name == name) + } + + /// `SS_process_sublinks`: the subplans of the subqueries in an expression of the query, the subqueries in the test of a subquery first. + fn sublinks_in(&mut self, expr: &'q Expr, level: &mut Level<'q>) { + for child in expr.children() { + self.sublinks_in(child, level); + } + if let ExprKind::SubLink(link) = &expr.kind { + self.sublink(link, level); + } + } + + /// `make_subplan`: the plan of a subquery in an expression. A subquery with no reference to the query runs once as an `InitPlan`, except for `ANY` and `ALL`, which keep its rows in a hash table or in a `Material` node. + fn sublink(&mut self, link: &'q SubLink, level: &mut Level<'q>) { + let (index, prefix) = match link.kind { + SubLinkKind::Exists => (0, "exists"), + SubLinkKind::Expr => (1, "expr"), + SubLinkKind::Any => (2, "any"), + SubLinkKind::All => (3, "all"), + SubLinkKind::Array => (4, "array"), + SubLinkKind::RowCompare => (5, "rowcompare"), + }; + self.counts[index] += 1; + let name = format!("{prefix}_{}", self.counts[index]); + let correlated = exprs::correlated(&link.query, 1); + let scope = level.scope; + let mut plan = self.subplan(|b| b.query(&link.query, Some(scope))); + let init = !correlated && !matches!(link.kind, SubLinkKind::Any | SubLinkKind::All); + let hashed = !correlated && link.kind == SubLinkKind::Any && hashable_test(link); + if !correlated && !init && !hashed { + plan = upper::materialize(plan); + } + self.labels.push(Label { link, name: name.clone(), init, hashed }); + if init { + level.init.push(SubPlan { name: format!("InitPlan {name}"), plan }); + } else { + self.pending.push((link, SubPlan { name: format!("SubPlan {name}"), plan })); + } + } +} + +/// True when each comparison of the test of `ANY` has an operator that can use a hash table, as `testexpr_is_hashable`. +fn hashable_test(link: &SubLink) -> bool { + fn hashable(expr: &Expr) -> bool { + match &expr.kind { + ExprKind::Bool(rupg_analyze::BoolOp::And, args) => args.iter().all(hashable), + _ => exprs::binary(expr).is_some_and(|(op, left, right)| { + exprs::can_hash(op) + && !matches!(left.kind, ExprKind::SubColumn(_)) + && matches!(right.kind, ExprKind::SubColumn(_)) + }), + } + } + link.test.as_ref().is_some_and(hashable) +} + +/// The expressions of a query in the order that `subquery_planner` gives them to `preprocess_expression`: the targets, the conditions of `FROM` and `WHERE`, `HAVING`, the offsets of the frames, `OFFSET`, `LIMIT`, then the functions, the `VALUES` lists and the arguments of `TABLESAMPLE` of the relations. +fn preprocess_order(query: &Query) -> Vec<&Expr> { + fn joins<'a>(item: &'a FromItem, out: &mut Vec<&'a Expr>) { + if let FromItem::Join(j) = item { + joins(&j.left, out); + joins(&j.right, out); + out.extend(&j.on); + } + } + let mut out: Vec<&Expr> = query.targets.iter().map(|t| &t.expr).collect(); + for item in &query.from { + joins(item, &mut out); + } + out.extend(query.filter.iter().chain(&query.having)); + for window in &query.windows { + out.extend(window.frame.start.iter().chain(&window.frame.end)); + } + out.extend(query.offset.iter().chain(&query.limit)); + for relation in &query.relations { + if let Some(function) = &relation.function { + out.extend(function.calls.iter().map(|(call, _)| call)); + } + out.extend(relation.values.iter().flatten().flatten()); + if let Some(sample) = &relation.tablesample { + out.extend(sample.args.iter().chain(&sample.repeatable)); + } + } + out +} + +/// Moves each subplan that runs for each row to the node with the expression that holds its subquery, as `SS_finalize_plan` sees it. +fn attach<'q>(node: &mut Node<'q>, pending: &mut Vec<(&'q SubLink, SubPlan<'q>)>) { + if !pending.is_empty() { + let mut links = Vec::new(); + for item in own_items(node) { + sublinks(&item.expr, &mut links); + } + let found: Vec<*const SubLink> = links.into_iter().map(std::ptr::from_ref).collect(); + for link in found { + if let Some(at) = pending.iter().position(|(l, _)| std::ptr::eq(*l, link)) { + let (_, sub) = pending.remove(at); + node.sub_plans.push(sub); + } + } + } + for child in &mut node.children { + attach(child, pending); + } + for sub in node.init_plans.iter_mut().chain(&mut node.sub_plans) { + attach(&mut sub.plan, pending); + } +} + +/// The subqueries of an expression outside the queries of other subqueries, in the order of the text. +fn sublinks<'e>(expr: &'e Expr, out: &mut Vec<&'e SubLink>) { + for child in expr.children() { + sublinks(child, out); + } + if let ExprKind::SubLink(link) = &expr.kind { + out.push(link); + } +} + +/// The expressions that a node computes: its target list apart from the values that it reads from below, its conditions and the other expressions of its kind. +fn own_items<'n, 'q>(node: &'n Node<'q>) -> Vec<&'n Item<'q>> { + let mut out: Vec<&Item<'q>> = node.output.iter().filter(|i| !i.below).collect(); + match &node.kind { + Kind::Result { one_time, .. } => out.extend(one_time), + Kind::NestedLoop { join_filter, .. } => out.extend(join_filter), + Kind::MergeJoin { cond, join_filter, .. } | Kind::HashJoin { cond, join_filter, .. } => { + out.extend(cond.iter().chain(join_filter)); + } + _ => {} + } + out.extend(&node.filter); + out +} diff --git a/crates/rupg-opt/src/output.rs b/crates/rupg-opt/src/output.rs new file mode 100644 index 00000000..89b2869e --- /dev/null +++ b/crates/rupg-opt/src/output.rs @@ -0,0 +1,272 @@ +//! The target list of each node of a query and the width of its rows, as `create_plan` makes them with `build_path_tlist` and `use_physical_tlist`. +//! +//! The width of a node is the width of the values that the nodes above it read, as the width of the `PathTarget` of its path. A scan below a join or an `Agg` gives all the columns of its relation, as a physical target list, but its width counts only the columns that the nodes above read. + +use rupg_analyze::{Expr, ExprKind, Var}; +use rupg_plan::{Item, Kind, Node}; + +use crate::{Level, cost, exprs, from}; + +/// Sets the target lists and the widths of the nodes of the plan of a query level. +pub(crate) fn level<'q>(level: &Level<'q>, node: &mut Node<'q>) { + let want = level.display_query.targets.iter().map(|t| Item::of(&t.expr)).collect(); + set(level, node, want, false); +} + +/// The item of an expression that the planner made. +fn item<'q>(expr: Expr) -> Item<'q> { + Item::owned(expr) +} + +/// Adds the columns of the expression that are not in the list. +fn add_columns<'q>(expr: &Expr, levels: usize, out: &mut Vec>) { + let mut columns = Vec::new(); + exprs::columns_at(expr, levels, &mut columns); + for column in columns { + if !out.iter().any(|o| o.expr.same(&column)) { + out.push(item(column)); + } + } +} + +/// The width of the values of the items. +fn width(level: &Level<'_>, scope: usize, items: &[Item<'_>]) -> i32 { + let query = if scope == level.scope { level.query } else { level.display_query }; + items.iter().map(|i| cost::expr_width(&i.expr, &query.relations)).sum() +} + +/// True when the expression is a column of the query. +fn is_var(expr: &Expr) -> bool { + matches!(expr.kind, ExprKind::Var(_)) +} + +/// Sets the target list of a node, whose parent reads the items of `want`, and of the nodes below it in the query level. With `physical`, a scan gives all the columns of its relation. +fn set<'q>(level: &Level<'q>, node: &mut Node<'q>, want: Vec>, physical: bool) { + if node.scope != level.scope && node.scope != level.display { + return; + } + let set_op = level.query.set_op.is_some(); + match &node.kind { + Kind::Append | Kind::SetOp { .. } | Kind::RecursiveUnion => {} + Kind::Aggregate { .. } if set_op => { + node.output = want.iter().map(Item::passed).collect(); + node.cost.width = node.children.first().map_or(0, |c| c.cost.width); + } + Kind::Modify { .. } => { + let want = level.query.targets.iter().map(|t| Item::of(&t.expr)).collect(); + if let Some(child) = node.children.first_mut() { + set(level, child, want, false); + } + node.output.clear(); + node.cost.width = 0; + } + Kind::Sort(_) + | Kind::Limit + | Kind::Unique + | Kind::LockRows + | Kind::Materialize + | Kind::Hash + | Kind::Result { replaces: None, .. } + if !node.children.is_empty() => + { + let mut below = want.clone(); + if let Kind::Sort(keys) = &node.kind { + for key in keys { + if !below.iter().any(|w| w.expr.same(&key.item.expr)) { + below.push(Item { expr: key.item.expr.clone(), below: false }); + } + } + } + node.output = want.iter().map(Item::passed).collect(); + if let Some(child) = node.children.first_mut() { + set(level, child, below, false); + node.cost.width = child.cost.width; + } + } + Kind::Sort(_) + | Kind::Limit + | Kind::Unique + | Kind::LockRows + | Kind::Materialize + | Kind::Hash => {} + Kind::Aggregate { keys, .. } => { + let keys: Vec = keys.iter().map(|k| k.expr.as_ref().clone()).collect(); + let vars = keys.iter().all(is_var) + && keys.iter().enumerate().all(|(i, k)| !keys[..i].iter().any(|o| o.same(k))); + grouped(level, node, want, &keys, vars); + } + Kind::WindowAgg { window, .. } => { + let clause = &level.query.windows[*window]; + let keys: Vec = clause + .partition + .iter() + .chain(&clause.order) + .map(|s| level.target(s.target).clone()) + .collect(); + grouped(level, node, want, &keys, false); + } + Kind::ProjectSet => { + let mut below = Vec::new(); + for w in &want { + add_columns(&w.expr, 0, &mut below); + } + if let Some(child) = node.children.first_mut() { + set(level, child, below, false); + } + node.cost.width = width(level, node.scope, &want); + node.output = want; + } + Kind::NestedLoop { join_filter, .. } => { + let parts: Vec = join_filter.iter().map(|i| i.expr.as_ref().clone()).collect(); + join(level, node, want, &parts); + } + Kind::MergeJoin { cond, join_filter, .. } | Kind::HashJoin { cond, join_filter, .. } => { + let parts: Vec = + cond.iter().chain(join_filter).map(|i| i.expr.as_ref().clone()).collect(); + join(level, node, want, &parts); + } + Kind::Result { .. } => { + node.cost.width = width(level, node.scope, &want); + node.output = want; + } + Kind::SeqScan(r) + | Kind::SampleScan(r) + | Kind::FunctionScan(r) + | Kind::TableFunctionScan(r) + | Kind::ValuesScan(r) + | Kind::SubqueryScan(r) + | Kind::CteScan(r) + | Kind::WorkTableScan(r) => { + let r = *r; + node.cost.width = width(level, node.scope, &want); + let plain = + want.iter().all(|w| matches!(w.expr.kind, ExprKind::Var(v) if v.attnum > 0)); + node.output = if physical && plain { all_columns(level, r) } else { want }; + if matches!(node.kind, Kind::SubqueryScan(_)) && trivial(level, node, r) { + let child = node.children.pop(); + if let Some(child) = child { + *node = child; + } + } + } + } +} + +/// The columns of a relation of the query, as `build_physical_tlist`. +fn all_columns<'q>(level: &Level<'q>, r: usize) -> Vec> { + let relation = &level.query.relations[r]; + relation + .columns + .iter() + .enumerate() + .filter(|(_, c)| c.ty != 0) + .filter_map(|(i, c)| { + let attnum = i16::try_from(i + 1).ok()?; + let mut expr = + Expr::new(ExprKind::Var(Var { relation: r, attnum, levels_up: 0 }), c.ty); + expr.typmod = c.typmod; + Some(item(expr)) + }) + .collect() +} + +/// True when a `SubqueryScan` gives the rows of its subquery as they are, so that `trivial_subqueryscan` takes it out of the plan. +fn trivial(level: &Level<'_>, node: &Node<'_>, r: usize) -> bool { + let columns = level.query.relations[r].columns.len(); + node.filter.is_empty() + && node.output.len() == columns + && node.output.iter().enumerate().all(|(i, o)| { + matches!(o.expr.kind, ExprKind::Var(v) if v.relation == r && usize::try_from(v.attnum).ok() == Some(i + 1)) + }) +} + +/// The target list of an `Agg` or a `WindowAgg`: a value that is a key comes from below, and the node computes the others from the columns below. +fn grouped<'q>( + level: &Level<'q>, + node: &mut Node<'q>, + want: Vec>, + keys: &[Expr], + physical: bool, +) { + let mut below: Vec> = Vec::new(); + for key in keys { + if !below.iter().any(|b| b.expr.same(key)) { + below.push(item(key.clone())); + } + } + let mut output = Vec::new(); + for w in &want { + if keys.iter().any(|k| k.same(&w.expr)) { + output.push(Item { expr: w.expr.clone(), below: !is_var(&w.expr) }); + continue; + } + if let Kind::WindowAgg { .. } = node.kind { + // The window calls of the windows below this one come from below. + let mut calls = Vec::new(); + exprs::window_calls(&w.expr, &mut calls); + if let Kind::WindowAgg { window, .. } = node.kind { + for call in calls { + if call.winref != window + 1 { + let expr = Expr::new(ExprKind::Window(Box::new(call.clone())), w.expr.ty); + if !below.iter().any(|b| b.expr.same(&expr)) { + below.push(item(expr)); + } + } + } + } + } + add_columns(&w.expr, 0, &mut below); + output.push(w.clone()); + } + for f in &node.filter { + add_columns(&f.expr, 0, &mut below); + } + if let Some(child) = node.children.first_mut() { + set(level, child, below, physical); + } + node.cost.width = width(level, node.scope, &output); + node.output = output; +} + +/// The target list of a join: the items that the parent reads. Each side gives the columns of its relations that the join and the nodes above it read, and the columns that a lateral reference of the other side reads. +fn join<'q>(level: &Level<'q>, node: &mut Node<'q>, want: Vec>, parts: &[Expr]) { + let mut needed: Vec> = Vec::new(); + for expr in want + .iter() + .map(|w| w.expr.as_ref()) + .chain(parts) + .chain(node.filter.iter().map(|f| f.expr.as_ref())) + { + add_columns(expr, 0, &mut needed); + } + let sides: Vec<_> = node.children.iter().map(from::node_rels).collect(); + for (i, child) in node.children.iter_mut().enumerate() { + let rels = &sides[i]; + let mut below: Vec> = needed + .iter() + .filter(|n| matches!(n.expr.kind, ExprKind::Var(v) if rels.contains(&v.relation))) + .cloned() + .collect(); + for (j, other) in sides.iter().enumerate() { + if j == i { + continue; + } + for r in other { + for (expr, depth) in from::lateral_exprs(&level.query.relations[*r]) { + let mut refs = Vec::new(); + add_columns(expr, depth, &mut refs); + for col in refs { + if matches!(col.expr.kind, ExprKind::Var(v) if rels.contains(&v.relation)) + && !below.iter().any(|b| b.expr.same(&col.expr)) + { + below.push(col); + } + } + } + } + } + set(level, child, below, true); + } + node.cost.width = width(level, node.scope, &want); + node.output = want; +} diff --git a/crates/rupg-opt/src/upper.rs b/crates/rupg-opt/src/upper.rs new file mode 100644 index 00000000..547120b1 --- /dev/null +++ b/crates/rupg-opt/src/upper.rs @@ -0,0 +1,809 @@ +//! The nodes above the join of `FROM`, as `grouping_planner` makes them: the aggregates, the windows, the functions that give sets, `DISTINCT`, `ORDER BY`, the locks of rows and `LIMIT`. Also the plan of a set operation and the node of a statement that writes a table. + +use rupg_analyze::{ + Aggref, Expr, ExprKind, Query, SetKind, SetOp, SetTree, SortGroup, WindowFunc, Write, + WriteKind, expand_grouping_sets, +}; +use rupg_pgcatalog::builtin; +use rupg_plan::{Conflict, Cost, Item, Kind, Node, Operation, SortKey, Strategy, TARGET}; + +use crate::from::{self, OrderKey, sort}; +use crate::{Builder, Level, cost, exprs}; + +/// `FRAMEOPTION_DEFAULTS`: `RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW`. +const FRAME_DEFAULTS: i32 = 0x422; +/// `FRAMEOPTION_ROWS`. +const FRAME_ROWS: i32 = 0x4; +/// `FRAMEOPTION_GROUPS`. +const FRAME_GROUPS: i32 = 0x8; +/// `FRAMEOPTION_END_UNBOUNDED_FOLLOWING`. +const FRAME_END_UNBOUNDED_FOLLOWING: i32 = 0x100; +/// `FRAMEOPTION_END_CURRENT_ROW`. +const FRAME_END_CURRENT_ROW: i32 = 0x400; +/// `FRAMEOPTION_END_OFFSET_PRECEDING`. +const FRAME_END_OFFSET_PRECEDING: i32 = 0x1000; +/// `FRAMEOPTION_END_OFFSET_FOLLOWING`. +const FRAME_END_OFFSET_FOLLOWING: i32 = 0x4000; +/// The most grouping sets that the planner expands, as the limit of `expand_grouping_sets` in `subquery_planner`. +const MAX_SETS: usize = 4096; + +/// The conditions of `HAVING` that go to `WHERE`, and the conditions that stay in `HAVING`, as `subquery_planner` divides them. A condition with no aggregate goes to `WHERE`, but with no `GROUP BY` it also stays, because the group is there also when no row passes. +pub(crate) fn having_split(query: &Query) -> (Vec<&Expr>, Vec<&Expr>) { + let mut parts = Vec::new(); + if let Some(having) = &query.having { + exprs::and_parts(having, &mut parts); + } + let (mut to_where, mut keep) = (Vec::new(), Vec::new()); + for part in parts { + let mut aggs = Vec::new(); + exprs::aggregates(&[part], &mut aggs); + let stays = !aggs.is_empty() + || has_grouping(part) + || exprs::volatile(part) + || has_sublink(part) + || !query.grouping_sets.is_empty(); + if stays { + keep.push(part); + } else if !query.group.is_empty() { + to_where.push(part); + } else { + to_where.push(part); + keep.push(part); + } + } + (to_where, keep) +} + +/// True when the expression calls `GROUPING` of the query. +fn has_grouping(expr: &Expr) -> bool { + expr.find(0, &mut |e, depth| match &e.kind { + ExprKind::Grouping(g) if g.levels_up == depth => Some(()), + _ => None, + }) + .is_some() +} + +/// True when the expression has a subquery. +fn has_sublink(expr: &Expr) -> bool { + expr.find(0, &mut |e, _| matches!(e.kind, ExprKind::SubLink(_)).then_some(())).is_some() +} + +/// A `Material` node over a node. +pub(crate) fn materialize(node: Node<'_>) -> Node<'_> { + from::materialize(node) +} + +/// A key of a sort for an item of `ORDER BY`, `GROUP BY` or `DISTINCT` with its operator: `ASC` for the `<` of the type, `DESC` for its `>`, or else `USING` with the operator. +fn sort_key<'q>(expr: &'q Expr, item: &SortGroup) -> SortKey<'q> { + let name = builtin::operator_by_oid(item.operator).map_or("<", |o| o.name); + SortKey { + item: Item::below(expr), + desc: name == ">", + using: (name != "<" && name != ">").then(|| name.to_string()), + nulls_first: item.nulls_first, + } +} + +/// The order of the rows after a sort on the keys. +fn sorted_order(keys: &[SortKey<'_>]) -> Vec { + keys.iter() + .map(|k| OrderKey { + exprs: vec![k.item.expr.as_ref().clone()], + desc: k.desc, + nulls_first: k.nulls_first, + }) + .collect() +} + +/// True when the rows are in the order of the keys, `pathkeys_contained_in`. +fn satisfies(order: &[OrderKey], keys: &[SortKey<'_>]) -> bool { + keys.len() <= order.len() + && keys + .iter() + .zip(order) + .all(|(k, o)| k.using.is_none() && o.sorts(&k.item.expr, k.desc, k.nulls_first)) +} + +/// A sort of the node on the keys unless its rows are in that order already. +fn sort_if<'q>( + node: Node<'q>, + keys: Vec>, + order: &mut Vec, + bound: Option, +) -> Node<'q> { + if keys.is_empty() || satisfies(order, &keys) { + return node; + } + *order = sorted_order(&keys); + sort(node, keys, bound) +} + +/// The cost of the transition of the aggregate calls for each row, in units of `cpu_operator_cost`, as `get_agg_clause_costs`: a call of the transition function and the cost of the arguments and of `FILTER`. +fn agg_units(aggs: &[&Aggref]) -> f64 { + aggs.iter() + .map(|a| { + let args: usize = a.args.iter().map(exprs::calls).sum(); + let filter = a.filter.as_deref().map_or(0, exprs::calls); + (1 + args + filter) as f64 + }) + .sum() +} + +/// The value of a constant `bigint` of `LIMIT` or `OFFSET`, `Some(None)` for a null constant, or `None` for an expression that is not a constant. +fn const_count(expr: &Expr) -> Option> { + use rupg_types::Value; + match expr.strip_implicit().as_const()? { + Value::Null => Some(None), + Value::Int8(v) => Some(Some(*v as f64)), + Value::Int4(v) => Some(Some(f64::from(*v))), + Value::Int2(v) => Some(Some(f64::from(*v))), + _ => None, + } +} + +/// The estimates of `LIMIT` and `OFFSET`, as `preprocess_limit`: 0 for no clause, -1 for an expression that is not a constant, or else the value. +fn limit_estimates(query: &Query) -> (f64, f64) { + let count = query.limit.as_ref().map_or(0.0, |l| match const_count(l) { + Some(None) => 0.0, + Some(Some(v)) => v.max(1.0), + None => -1.0, + }); + let offset = query.offset.as_ref().map_or(0.0, |o| match const_count(o) { + Some(None) => 0.0, + Some(Some(v)) => v.max(0.0), + None => -1.0, + }); + (count, offset) +} + +/// `limit_needed`: false when `LIMIT` is a null constant or absent and `OFFSET` is absent, null or 0. +fn limit_needed(query: &Query) -> bool { + let limit = query.limit.as_ref().is_some_and(|l| const_count(l) != Some(None)); + let offset = query + .offset + .as_ref() + .is_some_and(|o| !matches!(const_count(o), Some(None) | Some(Some(0.0)))); + limit || offset +} + +/// `adjust_limit_rows_costs`: the costs and the rows of a `Limit` over a node. +fn limit_cost(input: Cost, count: f64, offset: f64) -> Cost { + let mut out = input; + let input_rows = input.rows; + if offset != 0.0 { + let skip = if offset > 0.0 { offset } else { cost::clamp_rows(input_rows * 0.1) }; + let skip = skip.min(out.rows); + if out.rows > 0.0 { + out.startup += (out.total - out.startup) * skip / input_rows; + } + out.rows = (out.rows - skip).max(1.0); + } + if count != 0.0 { + let take = if count > 0.0 { count } else { cost::clamp_rows(input_rows * 0.1) }; + let take = take.min(out.rows); + if input_rows > 0.0 { + out.total = out.startup + (input.total - input.startup) * take / input_rows; + } + out.rows = take.max(1.0); + } + out +} + +/// The number of rows of a function that gives a set in the targets, as `expression_returns_set_rows`. +fn set_rows(expr: &Expr) -> f64 { + let own = match &expr.kind { + ExprKind::Func(f) if f.retset => from::function_rows(expr), + _ => 1.0, + }; + expr.children().into_iter().map(set_rows).fold(own, f64::max) +} + +impl<'q> Builder<'q> { + /// `grouping_planner`: the nodes above the join of `FROM`. + pub(crate) fn upper( + &mut self, + level: &Level<'q>, + mut node: Node<'q>, + mut order: Vec, + ) -> Node<'q> { + let query = level.query; + let scope = level.display; + if query.set_op.is_none() { + if query.grouped { + node = aggregate(level, node, &mut order); + } + node = windows(level, node, &mut order); + if query.has_target_srfs() { + let c = node.cost; + let rows = query.targets.iter().map(|t| set_rows(&t.expr)).fold(1.0, f64::max); + let per_row = cost::qual_cost(query.targets.iter().map(|t| &t.expr)); + let out_rows = c.rows * rows; + node = Node::new(Kind::ProjectSet, scope, vec![node]); + node.cost = Cost { + startup: c.startup, + total: c.total + + (cost::CPU_TUPLE + per_row) * c.rows + + (out_rows - c.rows) * cost::CPU_TUPLE / 2.0, + rows: out_rows, + width: 0, + }; + } + if !query.distinct.is_empty() { + node = distinct(level, node, &mut order); + } + } + let (count, offset) = limit_estimates(query); + if !query.sort.is_empty() { + let keys: Vec> = + query.sort.iter().map(|s| sort_key(level.target(s.target), s)).collect(); + let bound = + (count > 0.0 && offset >= 0.0 && !query.with_ties).then_some(count + offset); + node = sort_if(node, keys, &mut order, bound); + } + if !query.row_marks.is_empty() { + let c = node.cost; + node = Node::new(Kind::LockRows, scope, vec![node]); + node.cost = Cost { total: c.total + cost::CPU_TUPLE * c.rows, ..c }; + } + if limit_needed(query) { + let c = node.cost; + node = Node::new(Kind::Limit, scope, vec![node]); + node.cost = limit_cost(c, count, offset); + } + node + } + + /// The `ModifyTable` node of a statement that writes a table, as `create_modifytable_path`. + pub(crate) fn modify( + &mut self, + level: &Level<'q>, + write: &'q Write, + node: Node<'q>, + ) -> Node<'q> { + let operation = match write.kind { + WriteKind::Insert => Operation::Insert, + WriteKind::Update => Operation::Update, + WriteKind::Delete => Operation::Delete, + WriteKind::Merge => Operation::Merge, + }; + let conflict = write.on_conflict.as_ref().map(|c| { + let mut filter = Vec::new(); + let mut scope = None; + if let Some(update) = &c.update { + let update = crate::unwrap_checks(update); + scope = Some(self.scopes.len()); + self.scopes.push(rupg_plan::Scope { + query: update, + parent: Some(level.scope), + target: false, + }); + let mut parts = Vec::new(); + if let Some(cond) = &update.filter { + exprs::and_parts(cond, &mut parts); + } + filter = parts.into_iter().map(Item::of).collect(); + } + Conflict { + update: c.update.is_some(), + arbiters: c + .arbiters + .iter() + .filter_map(|i| write.unique.get(*i)) + .map(|u| u.oid) + .collect(), + filter, + scope, + } + }); + let relation = if operation == Operation::Insert { TARGET } else { 0 }; + let c = node.cost; + let mut out = Node::new( + Kind::Modify { operation, table: write.table, relation, conflict }, + level.scope, + vec![node], + ); + out.cost = Cost { rows: 0.0, width: 0, ..c }; + out + } + + /// The plan of a set operation, as `plan_set_operations`. The nodes above it show the expressions of the leftmost `SELECT`. + pub(crate) fn set_op(&mut self, level: &mut Level<'q>, op: &'q SetOp) -> Node<'q> { + let leftmost = op.left.leftmost(); + let node = self.set_node(level, op); + if let Some(sub) = level.query.relations.get(leftmost).and_then(|r| r.subquery.as_deref()) + && let Some(scope) = self.scopes.iter().position(|s| std::ptr::eq(s.query, sub)) + { + level.display = scope; + level.display_query = sub; + } + node + } + + /// The plan of a part of the tree of a set operation. + fn set_tree(&mut self, level: &Level<'q>, tree: &'q SetTree) -> Node<'q> { + match tree { + SetTree::Leaf(i) => { + match level.query.relations.get(*i).and_then(|r| r.subquery.as_deref()) { + Some(sub) => self.query(sub, Some(level.scope)), + None => Node::new( + Kind::Result { replaces: None, one_time: Vec::new() }, + level.scope, + Vec::new(), + ), + } + } + SetTree::Op(op) => self.set_node(level, op), + } + } + + /// The members of a `UNION`, with the members of each `UNION` below it that PostgreSQL makes one `Append` with, as `recurse_union_children`. + fn union_members( + &mut self, + level: &Level<'q>, + top: &'q SetOp, + tree: &'q SetTree, + out: &mut Vec>, + ) { + if let SetTree::Op(op) = tree + && op.kind == SetKind::Union + && op.work.is_none() + && (op.all == top.all || op.all) + && op.columns == top.columns + { + self.union_members(level, top, &op.left, out); + self.union_members(level, top, &op.right, out); + return; + } + out.push(self.set_tree(level, tree)); + } + + fn set_node(&mut self, level: &Level<'q>, op: &'q SetOp) -> Node<'q> { + let scope = level.scope; + if let Some(work) = op.work { + let left = self.set_tree(level, &op.left); + self.work.push((work, left.cost.rows)); + let right = self.set_tree(level, &op.right); + self.work.pop(); + let (l, r) = (left.cost, right.cost); + let rows = l.rows + 10.0 * r.rows; + let mut node = Node::new(Kind::RecursiveUnion, scope, vec![left, right]); + node.cost = Cost { + startup: l.startup, + total: l.total + 10.0 * r.total + cost::CPU_TUPLE * rows, + rows, + width: l.width.max(r.width), + }; + return node; + } + let columns = op.columns.len(); + match op.kind { + SetKind::Union => { + let mut members = Vec::new(); + self.union_members(level, op, &op.left, &mut members); + self.union_members(level, op, &op.right, &mut members); + let startup = members.first().map_or(0.0, |m| m.cost.startup); + let width = members.first().map_or(0, |m| m.cost.width); + let rows: f64 = members.iter().map(|m| m.cost.rows).sum(); + let total: f64 = members.iter().map(|m| m.cost.total).sum::() + + 0.5 * cost::CPU_TUPLE * rows; + let mut append = Node::new(Kind::Append, scope, members); + append.cost = Cost { startup, total, rows, width }; + if op.all { + return append; + } + let c = append.cost; + let hashable = op.groups.iter().all(|g| g.hashable); + let hash_startup = c.total + cost::CPU_OPERATOR * c.rows * columns as f64; + let hash_total = hash_startup + cost::CPU_TUPLE * rows; + let (sort_startup, sort_total) = cost::sort((c.startup, c.total), c.rows, None); + let unique_total = sort_total + cost::CPU_OPERATOR * c.rows * columns as f64; + let keys = self.leaf_items(level, op); + if hashable && hash_total <= unique_total { + let mut node = Node::new( + Kind::Aggregate { strategy: Strategy::Hashed, keys, sets: Vec::new() }, + scope, + vec![append], + ); + node.cost = Cost { startup: hash_startup, total: hash_total, rows, width }; + node + } else { + let sort_keys = keys + .into_iter() + .zip(&op.groups) + .map(|(item, g)| SortKey { + item, + desc: false, + using: None, + nulls_first: g.nulls_first, + }) + .collect(); + let mut sorted = Node::new(Kind::Sort(sort_keys), scope, vec![append]); + sorted.cost = Cost { startup: sort_startup, total: sort_total, ..c }; + let mut node = Node::new(Kind::Unique, scope, vec![sorted]); + node.cost = Cost { startup: sort_startup, total: unique_total, rows, width }; + node + } + } + SetKind::Intersect | SetKind::Except => { + let left = self.set_tree(level, &op.left); + let right = self.set_tree(level, &op.right); + let (l, r) = (left.cost, right.cost); + let left_groups = cost::groups(l.rows, columns); + let right_groups = cost::groups(r.rows, columns); + let rows = if op.kind == SetKind::Intersect { + left_groups.min(right_groups) + } else { + left_groups + }; + let startup = + l.total + r.total + cost::CPU_OPERATOR * (l.rows + r.rows) * columns as f64; + let mut node = Node::new( + Kind::SetOp { except: op.kind == SetKind::Except, all: op.all }, + scope, + vec![left, right], + ); + node.cost = Cost { + startup, + total: startup + cost::CPU_TUPLE * left_groups, + rows, + width: l.width, + }; + node + } + } + } + + /// The columns of the leftmost `SELECT` of a set operation, as the keys of the node that finds the equal rows. + fn leaf_items(&self, level: &Level<'q>, op: &'q SetOp) -> Vec> { + let leftmost = op.left.leftmost(); + match level.query.relations.get(leftmost).and_then(|r| r.subquery.as_deref()) { + Some(sub) => { + sub.targets.iter().filter(|t| !t.junk).map(|t| Item::below(&t.expr)).collect() + } + None => Vec::new(), + } + } +} + +/// The ordered aggregate call whose keys the input of the `Agg` gets sorted on, as the presorted aggregates of `make_pathkeys_for_groupagg`: the first call with `ORDER BY` or `DISTINCT`. +fn ordered_keys<'q>(aggs: &[&'q Aggref]) -> Vec> { + let Some(agg) = aggs.iter().find(|a| !a.order.is_empty() || !a.distinct.is_empty()) else { + return Vec::new(); + }; + let items = if agg.distinct.is_empty() { &agg.order } else { &agg.distinct }; + items.iter().filter_map(|s| agg.args.get(s.target).map(|e| sort_key(e, s))).collect() +} + +/// The `Agg` node of a query with groups, with a `Sort` below it when the strategy needs one, as `add_paths_to_grouping_rel`. +fn aggregate<'q>(level: &Level<'q>, input: Node<'q>, order: &mut Vec) -> Node<'q> { + let query = level.query; + let scope = level.scope; + let mut exprs_of: Vec<&Expr> = query.targets.iter().map(|t| &t.expr).collect(); + let (_, having) = having_split(query); + exprs_of.extend(having.iter().copied()); + let mut aggs = Vec::new(); + exprs::aggregates(&exprs_of, &mut aggs); + let units = agg_units(&aggs); + let ordered = ordered_keys(&aggs); + let selectivity: f64 = having.iter().map(|h| cost::selectivity(h)).product(); + let having_cost = cost::qual_cost(having.iter().copied()); + let filter: Vec> = having.iter().map(|h| Item::of(h)).collect(); + let c = input.cost; + let n = c.rows; + if !query.grouping_sets.is_empty() { + return grouping_sets(level, input, order, units, filter, selectivity); + } + if query.group.is_empty() { + let mut input = input; + if !ordered.is_empty() { + input = sort_if(input, ordered, order, None); + } + let c = input.cost; + let startup = c.total + cost::CPU_OPERATOR * n * units; + let total = startup + cost::CPU_TUPLE + having_cost; + let mut node = Node::new( + Kind::Aggregate { strategy: Strategy::Plain, keys: Vec::new(), sets: Vec::new() }, + scope, + vec![input], + ); + node.filter = filter; + node.cost = Cost { startup, total, rows: cost::clamp_rows(selectivity), width: 0 }; + order.clear(); + return node; + } + let group = group_keys(level); + let nkeys = group.len() as f64; + let groups = cost::groups(n, group.len()); + let keys: Vec> = group.iter().map(|k| k.item.clone()).collect(); + let rows = cost::clamp_rows(groups * selectivity); + let hashable = query.group.iter().all(|g| g.hashable) && ordered.is_empty(); + // The sorted strategy: a `Sort` on the keys of the groups, and on the keys of an ordered call after them. + let mut sort_keys = group.clone(); + sort_keys.extend(ordered); + let (sorted_input, sorted_order) = if satisfies(order, &sort_keys) { + (c, order.clone()) + } else { + let (startup, total) = cost::sort((c.startup, c.total), n, None); + (Cost { startup, total, ..c }, sorted_order(&sort_keys)) + }; + let sorted_total = sorted_input.total + + cost::CPU_OPERATOR * n * (nkeys + units) + + cost::CPU_TUPLE * groups + + having_cost * groups; + let hash_startup = c.total + cost::CPU_OPERATOR * n * (nkeys + units); + let hash_total = hash_startup + cost::CPU_TUPLE * groups + having_cost * groups; + if hashable && hash_total * 1.01 < sorted_total { + let mut node = Node::new( + Kind::Aggregate { strategy: Strategy::Hashed, keys, sets: Vec::new() }, + scope, + vec![input], + ); + node.filter = filter; + node.cost = Cost { startup: hash_startup, total: hash_total, rows, width: 0 }; + order.clear(); + return node; + } + let input = if satisfies(order, &sort_keys) { input } else { sort(input, sort_keys, None) }; + *order = sorted_order; + order.truncate(group.len()); + let mut node = Node::new( + Kind::Aggregate { strategy: Strategy::Sorted, keys, sets: Vec::new() }, + scope, + vec![input], + ); + node.filter = filter; + node.cost = Cost { startup: sorted_input.startup, total: sorted_total, rows, width: 0 }; + node +} + +/// The keys of `GROUP BY` as the keys of a sort, in the order of `ORDER BY` when its items are items of `GROUP BY`, as `preprocess_groupclause`. +fn group_keys<'q>(level: &Level<'q>) -> Vec> { + let query = level.query; + let mut items: Vec<&SortGroup> = Vec::new(); + let in_group = |s: &SortGroup| query.group.iter().find(|g| g.target == s.target); + if query.sort.iter().all(|s| in_group(s).is_some_and(|g| g.equal == s.equal)) { + items.extend(query.sort.iter()); + } + for g in &query.group { + if !items.iter().any(|i| i.target == g.target) { + items.push(g); + } + } + items.into_iter().map(|s| sort_key(level.target(s.target), s)).collect() +} + +/// The `Agg` of a query with grouping sets: `HashAggregate` when each set can use a hash table, `MixedAggregate` when one of them is `()`, or else `GroupAggregate` on the sorted rows. +fn grouping_sets<'q>( + level: &Level<'q>, + input: Node<'q>, + order: &mut Vec, + units: f64, + filter: Vec>, + selectivity: f64, +) -> Node<'q> { + let query = level.query; + let scope = level.scope; + let sets = expand_grouping_sets(&query.grouping_sets, query.group_distinct, MAX_SETS) + .unwrap_or_default(); + let group_of = |t: usize| query.group.iter().find(|g| g.target == t); + let hashable = |set: &Vec| set.iter().all(|t| group_of(*t).is_some_and(|g| g.hashable)); + let all_hashable = sets.iter().filter(|s| !s.is_empty()).all(hashable); + let has_empty = sets.iter().any(Vec::is_empty); + let strategy = if !all_hashable { + Strategy::Sorted + } else if has_empty { + Strategy::Mixed + } else { + Strategy::Hashed + }; + let c = input.cost; + let n = c.rows; + let mut ordered: Vec<&Vec> = sets.iter().collect(); + ordered.reverse(); + let mut groups = 0.0; + let mut units_all = 0.0; + let mut out_sets = Vec::new(); + let mut keys: Vec> = Vec::new(); + for set in &ordered { + groups += if set.is_empty() { 1.0 } else { cost::groups(n, set.len()) }; + units_all += set.len() as f64 + units; + let items: Vec> = set.iter().map(|t| Item::below(level.target(*t))).collect(); + for item in &items { + if !keys.iter().any(|k| k.expr.same(&item.expr)) { + keys.push(item.clone()); + } + } + out_sets.push((strategy != Strategy::Sorted && !set.is_empty(), items)); + } + let mut input = input; + let mut startup = c.total; + if strategy == Strategy::Sorted { + let first = ordered.first().map(|s| s.as_slice()).unwrap_or_default(); + let sort_keys: Vec> = first + .iter() + .filter_map(|t| group_of(*t).map(|g| sort_key(level.target(*t), g))) + .collect(); + input = sort_if(input, sort_keys, order, None); + startup = input.cost.startup; + } + let total = input.cost.total.max(c.total) + + cost::CPU_OPERATOR * n * units_all + + cost::CPU_TUPLE * groups; + if strategy != Strategy::Sorted { + startup += cost::CPU_OPERATOR * n * units_all; + } + order.clear(); + let mut node = + Node::new(Kind::Aggregate { strategy, keys, sets: out_sets }, scope, vec![input]); + node.filter = filter; + node.cost = Cost { + startup, + total: total.max(startup), + rows: cost::clamp_rows(groups * selectivity), + width: 0, + }; + node +} + +/// The window calls of the query for each window, by `winref`. +fn window_calls(query: &Query) -> Vec> { + let mut out = vec![Vec::new(); query.windows.len()]; + for target in &query.targets { + let mut calls = Vec::new(); + exprs::window_calls(&target.expr, &mut calls); + for call in calls { + if let Some(list) = out.get_mut(call.winref.wrapping_sub(1)) { + list.push(call); + } + } + } + out +} + +/// The keys of the sort of a window: the items of `PARTITION BY`, then the items of `ORDER BY`. +fn window_keys<'q>(level: &Level<'q>, window: usize) -> Vec> { + let clause = &level.query.windows[window]; + let mut keys: Vec> = Vec::new(); + for item in clause.partition.iter().chain(&clause.order) { + let key = sort_key(level.target(item.target), item); + if !keys.iter().any(|k| k.item.expr.same(&key.item.expr)) { + keys.push(key); + } + } + keys +} + +/// `get_windowclause_startup_tuples`: the rows that a window reads before its first row. +fn startup_tuples(level: &Level<'_>, window: usize, rows: f64) -> f64 { + let clause = &level.query.windows[window]; + let partitions = + if clause.partition.is_empty() { 1.0 } else { cost::groups(rows, clause.partition.len()) }; + let partition_rows = (rows / partitions).max(1.0); + let options = clause.frame.options; + if options & FRAME_END_UNBOUNDED_FOLLOWING != 0 { + return cost::clamp_rows(partition_rows); + } + let end_offset = || { + clause + .frame + .end + .as_ref() + .and_then(const_count) + .flatten() + .map_or(10.0, |v| if options & FRAME_END_OFFSET_PRECEDING != 0 { -v } else { v }) + }; + let peers = || { + if clause.order.is_empty() { + partition_rows + } else { + partition_rows / cost::groups(partition_rows, clause.order.len()) + } + }; + let out = if options & FRAME_ROWS != 0 { + if options & FRAME_END_CURRENT_ROW != 0 { 1.0 } else { 1.0 + end_offset() } + } else if options & FRAME_GROUPS != 0 { + if options & FRAME_END_CURRENT_ROW != 0 { peers() } else { peers() * (1.0 + end_offset()) } + } else if options & FRAME_END_CURRENT_ROW != 0 { + peers() + } else if options & (FRAME_END_OFFSET_FOLLOWING | FRAME_END_OFFSET_PRECEDING) != 0 { + partition_rows / 3.0 + } else { + partition_rows + }; + cost::clamp_rows(out.min(partition_rows)) +} + +/// The `WindowAgg` nodes of the query, each with a `Sort` below it when the rows are not in the order of its window. The windows with the most keys come first, as `select_active_windows`. +fn windows<'q>(level: &Level<'q>, mut node: Node<'q>, order: &mut Vec) -> Node<'q> { + let calls = window_calls(level.query); + let mut active: Vec = (0..calls.len()).filter(|w| !calls[*w].is_empty()).collect(); + active.sort_by_key(|w| std::cmp::Reverse(window_keys(level, *w).len())); + for window in active { + let clause = &level.query.windows[window]; + node = sort_if(node, window_keys(level, window), order, None); + let c = node.cost; + let mut total = c.total; + for call in &calls[window] { + let args: usize = call.args.iter().map(exprs::calls).sum(); + let filter = call.filter.as_deref().map_or(0, exprs::calls); + total += cost::CPU_OPERATOR * (1 + args + filter) as f64 * c.rows; + } + let nkeys = (clause.partition.len() + clause.order.len()) as f64; + total += cost::CPU_OPERATOR * nkeys * c.rows + cost::CPU_TUPLE * c.rows; + let mut startup = c.startup; + let tuples = startup_tuples(level, window, c.rows); + if tuples > 1.0 && c.rows > 0.0 { + startup += (total - startup) * tuples / c.rows; + } + let rows_frame = clause.frame.options == FRAME_DEFAULTS + && calls[window].iter().all(|f| { + builtin::proc_by_oid(f.oid).is_some_and(|p| { + matches!( + p.name, + "row_number" + | "rank" + | "dense_rank" + | "percent_rank" + | "ntile" + | "cume_dist" + ) + }) + }); + let name = clause.name.clone().unwrap_or_else(|| format!("w{}", window + 1)); + node = Node::new(Kind::WindowAgg { name, window, rows_frame }, level.scope, vec![node]); + node.cost = Cost { startup, total, ..c }; + } + node +} + +/// The node of `DISTINCT`: a `HashAggregate` on the targets, or a `Unique` on the rows sorted on them, whichever costs less, as `create_distinct_paths`. `DISTINCT ON` always sorts. +fn distinct<'q>(level: &Level<'q>, input: Node<'q>, order: &mut Vec) -> Node<'q> { + let query = level.query; + let scope = level.scope; + let c = input.cost; + let ncols = query.distinct.len(); + let groups = cost::groups(c.rows, ncols); + // The sort puts the items of `ORDER BY` first, so that the sort of `ORDER BY` after it has no work. + let mut keys: Vec> = Vec::new(); + let distinct_target = |t: usize| query.distinct.iter().any(|d| d.target == t); + if query.sort.iter().all(|s| distinct_target(s.target)) { + keys.extend(query.sort.iter().map(|s| sort_key(level.target(s.target), s))); + } + for d in &query.distinct { + let key = sort_key(level.target(d.target), d); + if !keys.iter().any(|k| k.item.expr.same(&key.item.expr)) { + keys.push(key); + } + } + let presorted = satisfies(order, &keys); + let (sort_startup, sort_total) = if presorted { + (c.startup, c.total) + } else { + cost::sort((c.startup, c.total), c.rows, None) + }; + let unique_total = sort_total + cost::CPU_OPERATOR * c.rows * ncols as f64; + let hashable = !query.distinct_on && query.distinct.iter().all(|d| d.hashable); + let hash_startup = c.total + cost::CPU_OPERATOR * c.rows * ncols as f64; + let hash_total = hash_startup + cost::CPU_TUPLE * groups; + // The hash table loses its order, so the sort of `ORDER BY` counts for it. + let hash_after = if query.sort.is_empty() { + hash_total + } else { + cost::sort((hash_startup, hash_total), groups, None).1 + }; + if hashable && hash_after < unique_total { + let items = query.distinct.iter().map(|d| Item::below(level.target(d.target))).collect(); + let mut node = Node::new( + Kind::Aggregate { strategy: Strategy::Hashed, keys: items, sets: Vec::new() }, + scope, + vec![input], + ); + node.cost = Cost { startup: hash_startup, total: hash_total, rows: groups, width: 0 }; + order.clear(); + return node; + } + let input = sort_if(input, keys, order, None); + let mut node = Node::new(Kind::Unique, scope, vec![input]); + node.cost = Cost { startup: sort_startup, total: unique_total, rows: groups, ..c }; + node +} diff --git a/crates/rupg-plan/Cargo.toml b/crates/rupg-plan/Cargo.toml index 7f7263b2..cb69a792 100644 --- a/crates/rupg-plan/Cargo.toml +++ b/crates/rupg-plan/Cargo.toml @@ -14,6 +14,8 @@ readme.workspace = true publish = false [dependencies] +rupg-analyze.workspace = true +rupg-common.workspace = true [lints] workspace = true diff --git a/crates/rupg-plan/src/explain.rs b/crates/rupg-plan/src/explain.rs new file mode 100644 index 00000000..2d3c19db --- /dev/null +++ b/crates/rupg-plan/src/explain.rs @@ -0,0 +1,600 @@ +//! `ExplainPrintPlan` and `ExplainNode` of `explain.c`: the nodes of a plan with their names, their estimates and their parts, in the form that the options of `EXPLAIN` choose. + +use rupg_analyze::{ExprKind, JoinKind, Relation}; +use rupg_common::Result; + +use crate::format::Writer; +use crate::{Item, Kind, Node, Plan, Replaced, SortKey, Strategy, Texts}; + +/// The form of the output, the option `FORMAT`. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Format { + #[default] + Text, + Xml, + Json, + Yaml, +} + +/// The options of `EXPLAIN` that change the output of a plan with no run. +#[derive(Clone, Copy, Debug)] +pub struct Options { + pub verbose: bool, + pub costs: bool, + pub format: Format, + /// With `SUMMARY`, the time of the planning in milliseconds. + pub planning_time: Option, +} + +impl Default for Options { + fn default() -> Self { + Options { verbose: false, costs: true, format: Format::Text, planning_time: None } + } +} + +/// The output of `EXPLAIN` for a plan. The text form has a line for each row of the result, and the other forms are one value. +/// +/// # Errors +/// +/// The errors of the deparse of the expressions. +pub fn explain(plan: &Plan<'_>, options: &Options, texts: &mut dyn Texts) -> Result { + let mut e = Explainer { + w: Writer::new(options.format), + options, + texts, + plan, + many: plan.rtable_size() > 1, + }; + e.w.begin(); + e.w.open_group("Query", None, true); + e.node(&plan.root, None, None)?; + if let Some(ms) = options.planning_time { + e.w.float("Planning Time", Some("ms"), ms, 3); + } + e.w.close_group("Query", true); + e.w.end(); + let mut out = e.w.buf; + if options.format == Format::Text && out.ends_with('\n') { + out.pop(); + } + Ok(out) +} + +/// The state of `ExplainState` that the nodes read. +struct Explainer<'a, 'q> { + w: Writer, + options: &'a Options, + texts: &'a mut dyn Texts, + plan: &'a Plan<'q>, + /// `list_length(es->rtable) > 1`: the plan reads more than one relation, so the columns show the names of their relations. + many: bool, +} + +/// The names of a node: the name of the text form, the name of `Node Type`, and the `Strategy`, the `Partial Mode` and the `Operation` of the other forms. +struct Names { + pname: String, + sname: &'static str, + strategy: Option<&'static str>, + partial: Option<&'static str>, + operation: Option<&'static str>, +} + +/// The names of a node of this kind. +fn names(kind: &Kind<'_>) -> Names { + let simple = |name: &'static str| Names { + pname: name.to_string(), + sname: name, + strategy: None, + partial: None, + operation: None, + }; + match kind { + Kind::Result { .. } => simple("Result"), + Kind::SeqScan(_) => simple("Seq Scan"), + Kind::SampleScan(_) => simple("Sample Scan"), + Kind::FunctionScan(_) => simple("Function Scan"), + Kind::TableFunctionScan(_) => simple("Table Function Scan"), + Kind::ValuesScan(_) => simple("Values Scan"), + Kind::SubqueryScan(_) => simple("Subquery Scan"), + Kind::CteScan(_) => simple("CTE Scan"), + Kind::WorkTableScan(_) => simple("WorkTable Scan"), + Kind::NestedLoop { .. } => simple("Nested Loop"), + Kind::MergeJoin { .. } => Names { pname: "Merge".to_string(), ..simple("Merge Join") }, + Kind::HashJoin { .. } => Names { pname: "Hash".to_string(), ..simple("Hash Join") }, + Kind::Hash => simple("Hash"), + Kind::Materialize => simple("Materialize"), + Kind::Aggregate { strategy, .. } => { + let (pname, strategy) = match strategy { + Strategy::Plain => ("Aggregate", "Plain"), + Strategy::Sorted => ("GroupAggregate", "Sorted"), + Strategy::Hashed => ("HashAggregate", "Hashed"), + Strategy::Mixed => ("MixedAggregate", "Mixed"), + }; + Names { + pname: pname.to_string(), + sname: "Aggregate", + strategy: Some(strategy), + partial: Some("Simple"), + operation: None, + } + } + Kind::Sort(_) => simple("Sort"), + Kind::Limit => simple("Limit"), + Kind::Unique => simple("Unique"), + Kind::Append => simple("Append"), + Kind::SetOp { .. } => { + Names { pname: "HashSetOp".to_string(), strategy: Some("Hashed"), ..simple("SetOp") } + } + Kind::RecursiveUnion => simple("Recursive Union"), + Kind::WindowAgg { .. } => simple("WindowAgg"), + Kind::ProjectSet => simple("ProjectSet"), + Kind::LockRows => simple("LockRows"), + Kind::Modify { operation, .. } => Names { + pname: operation.name().to_string(), + operation: Some(operation.name()), + ..simple("ModifyTable") + }, + } +} + +/// The name of a kind of join. +fn join_type(kind: JoinKind) -> &'static str { + match kind { + JoinKind::Inner => "Inner", + JoinKind::Left => "Left", + JoinKind::Right => "Right", + JoinKind::Full => "Full", + } +} + +/// The target of a scan or of a `ModifyTable`, as `ExplainTargetRel`: the label and the name of the object, its schema, and the name of the relation in the plan. +struct Target { + tag: Option<&'static str>, + object: Option, + schema: Option, + refname: String, +} + +impl Explainer<'_, '_> { + /// `ExplainNode`: a node, its parts, then its subplans and the nodes below it. `relationship` is the relation to the node above, and `plan_name` is the name of a subplan. + fn node( + &mut self, + node: &Node<'_>, + relationship: Option<&str>, + plan_name: Option<&str>, + ) -> Result<()> { + let text = self.w.format == Format::Text; + let save_indent = self.w.indent; + let n = names(&node.kind); + if text { + if let Some(name) = plan_name { + self.w.indent_text(); + self.w.buf.push_str(name); + self.w.buf.push('\n'); + self.w.indent += 1; + } + if self.w.indent > 0 { + self.w.indent_text(); + self.w.buf.push_str("-> "); + self.w.indent += 2; + } + self.w.buf.push_str(&n.pname); + self.w.indent += 1; + } else { + self.w.open_group( + "Plan", + if relationship.is_some() { None } else { Some("Plan") }, + true, + ); + self.w.text("Node Type", n.sname); + if let Some(strategy) = n.strategy { + self.w.text("Strategy", strategy); + } + if let Some(partial) = n.partial { + self.w.text("Partial Mode", partial); + } + if let Some(operation) = n.operation { + self.w.text("Operation", operation); + } + if let Some(relationship) = relationship { + self.w.text("Parent Relationship", relationship); + } + if let Some(name) = plan_name { + self.w.text("Subplan Name", name); + } + self.w.boolean("Parallel Aware", false); + self.w.boolean("Async Capable", false); + } + if let Some(target) = self.target(node) { + self.write_target(&target); + } + match &node.kind { + Kind::NestedLoop { join, .. } + | Kind::MergeJoin { join, .. } + | Kind::HashJoin { join, .. } => { + if !text { + self.w.text("Join Type", join_type(*join)); + } else if *join != JoinKind::Inner { + self.w.buf.push_str(&format!(" {} Join", join_type(*join))); + } else if matches!(node.kind, Kind::HashJoin { .. } | Kind::MergeJoin { .. }) { + self.w.buf.push_str(" Join"); + } + } + Kind::SetOp { except, all } => { + let command = match (except, all) { + (false, false) => "Intersect", + (false, true) => "Intersect All", + (true, false) => "Except", + (true, true) => "Except All", + }; + if text { + self.w.buf.push(' '); + self.w.buf.push_str(command); + } else { + self.w.text("Command", command); + } + } + _ => {} + } + if self.options.costs { + let c = node.cost; + if text { + self.w.buf.push_str(&format!( + " (cost={:.2}..{:.2} rows={:.0} width={})", + c.startup, c.total, c.rows, c.width + )); + } else { + self.w.float("Startup Cost", None, c.startup, 2); + self.w.float("Total Cost", None, c.total, 2); + self.w.float("Plan Rows", None, c.rows, 0); + self.w.integer("Plan Width", None, i64::from(c.width)); + } + } + if text { + self.w.buf.push('\n'); + } else { + self.w.boolean("Disabled", false); + } + if self.options.verbose && !node.output.is_empty() { + let output = self.items(node.scope, &node.output, self.many)?; + self.w.list("Output", &output); + } + if matches!( + node.kind, + Kind::NestedLoop { .. } | Kind::MergeJoin { .. } | Kind::HashJoin { .. } + ) && (!text || self.options.verbose) + { + self.w.boolean("Inner Unique", false); + } + self.parts(node)?; + let has_children = + !node.init_plans.is_empty() || !node.children.is_empty() || !node.sub_plans.is_empty(); + if has_children { + self.w.open_group("Plans", Some("Plans"), false); + } + for sub in &node.init_plans { + self.node(&sub.plan, Some("InitPlan"), Some(&sub.name))?; + } + for (i, child) in node.children.iter().enumerate() { + let relationship = match node.kind { + Kind::Append => "Member", + Kind::SubqueryScan(_) => "Subquery", + _ if i == 0 => "Outer", + _ => "Inner", + }; + self.node(child, Some(relationship), None)?; + } + for sub in &node.sub_plans { + self.node(&sub.plan, Some("SubPlan"), Some(&sub.name))?; + } + if has_children { + self.w.close_group("Plans", false); + } + if text { + self.w.indent = save_indent; + } + self.w.close_group("Plan", true); + Ok(()) + } + + /// The target of a scan or of a `ModifyTable`. + fn target(&mut self, node: &Node<'_>) -> Option { + let scope = node.scope; + let query = self.plan.scopes[scope].query; + let relation_of = |index: usize| -> &Relation { &query.relations[index] }; + let (index, tag, object): (usize, Option<&'static str>, Option<(u32, bool)>) = match node + .kind + { + Kind::SeqScan(r) | Kind::SampleScan(r) => { + (r, Some("Relation Name"), Some((relation_of(r).oid, false))) + } + Kind::FunctionScan(r) => { + let function = relation_of(r).function.as_ref(); + let call = match function.map(|f| f.calls.as_slice()) { + Some([(call, _)]) => match &call.kind { + ExprKind::Func(f) => Some((f.oid, true)), + _ => None, + }, + _ => None, + }; + (r, Some("Function Name"), call) + } + Kind::TableFunctionScan(r) => { + let refname = self.texts.refname(scope, r); + return Some(Target { + tag: Some("Table Function Name"), + object: Some("json_table".to_string()), + schema: None, + refname, + }); + } + Kind::CteScan(r) | Kind::WorkTableScan(r) => { + let name = relation_of(r).cte.as_ref().map(|c| c.name.clone()); + let refname = self.texts.refname(scope, r); + return Some(Target { tag: Some("CTE Name"), object: name, schema: None, refname }); + } + Kind::ValuesScan(r) | Kind::SubqueryScan(r) => (r, None, None), + Kind::Modify { table, relation, .. } => { + (relation, Some("Relation Name"), Some((table, false))) + } + _ => return None, + }; + let refname = self.texts.refname(scope, index); + let (object, schema) = match object { + Some((oid, function)) => { + let (name, schema) = self.texts.object(oid, function); + (Some(name), self.options.verbose.then_some(schema)) + } + None => (None, None), + }; + Some(Target { tag, object, schema, refname }) + } + + /// `ExplainTargetRel`: ` on` with the name of the target in text, or its properties in the other forms. + fn write_target(&mut self, target: &Target) { + if self.w.format == Format::Text { + self.w.buf.push_str(" on"); + match (&target.schema, &target.object) { + (Some(schema), Some(object)) => { + let text = + format!(" {}.{}", self.texts.quote(schema), self.texts.quote(object)); + self.w.buf.push_str(&text); + } + (None, Some(object)) => { + let text = format!(" {}", self.texts.quote(object)); + self.w.buf.push_str(&text); + } + _ => {} + } + if target.object.as_deref() != Some(target.refname.as_str()) { + let text = format!(" {}", self.texts.quote(&target.refname)); + self.w.buf.push_str(&text); + } + } else { + if let (Some(tag), Some(object)) = (target.tag, &target.object) { + self.w.text(tag, object); + } + if let Some(schema) = &target.schema { + self.w.text("Schema", schema); + } + self.w.text("Alias", &target.refname); + } + } + + /// The parts of a node after its target list: the conditions, the keys and the other properties of its kind. + fn parts(&mut self, node: &Node<'_>) -> Result<()> { + let scope = node.scope; + let upper = self.many || self.options.verbose; + match &node.kind { + Kind::Result { replaces, one_time } => { + if let Some(replaced) = replaces { + let text = match replaced { + Replaced::Scan(r) => format!("Scan on {}", self.texts.refname(scope, *r)), + Replaced::Join(rs) => { + let names: Vec = + rs.iter().map(|r| self.texts.refname(scope, *r)).collect(); + format!("Join on {}", names.join(", ")) + } + }; + self.w.text("Replaces", &text); + } + self.qual("One-Time Filter", scope, one_time, upper)?; + self.qual("Filter", scope, &node.filter, upper)?; + } + Kind::SeqScan(_) + | Kind::ValuesScan(_) + | Kind::CteScan(_) + | Kind::WorkTableScan(_) + | Kind::TableFunctionScan(_) => { + self.qual("Filter", scope, &node.filter, self.options.verbose)?; + } + Kind::SampleScan(r) => { + self.sampling(scope, *r, upper)?; + self.qual("Filter", scope, &node.filter, self.options.verbose)?; + } + Kind::FunctionScan(r) => { + if self.options.verbose { + let relation = &self.plan.scopes[scope].query.relations[*r]; + if let Some(function) = &relation.function { + let calls: Vec = function + .calls + .iter() + .map(|(call, _)| self.texts.expr(scope, call, true)) + .collect::>()?; + let text = if calls.len() == 1 { + calls.join("") + } else { + format!("ROWS FROM({})", calls.join(", ")) + }; + self.w.text("Function Call", &text); + } + } + self.qual("Filter", scope, &node.filter, self.options.verbose)?; + } + Kind::SubqueryScan(_) => self.qual("Filter", scope, &node.filter, true)?, + Kind::NestedLoop { join_filter, .. } => { + self.qual("Join Filter", scope, join_filter, upper)?; + self.qual("Filter", scope, &node.filter, upper)?; + } + Kind::MergeJoin { cond, join_filter, .. } => { + self.qual("Merge Cond", scope, cond, upper)?; + self.qual("Join Filter", scope, join_filter, upper)?; + self.qual("Filter", scope, &node.filter, upper)?; + } + Kind::HashJoin { cond, join_filter, .. } => { + self.qual("Hash Cond", scope, cond, upper)?; + self.qual("Join Filter", scope, join_filter, upper)?; + self.qual("Filter", scope, &node.filter, upper)?; + } + Kind::Aggregate { strategy, keys, sets } => { + if sets.is_empty() { + if !keys.is_empty() { + let keys = self.items(scope, keys, upper)?; + self.w.list("Group Key", &keys); + } + } else { + self.grouping_sets(scope, sets, upper)?; + } + self.qual("Filter", scope, &node.filter, upper)?; + if matches!(strategy, Strategy::Hashed | Strategy::Mixed) + && self.options.costs + && self.w.format != Format::Text + { + self.w.integer("Planned Partitions", None, 0); + } + } + Kind::Sort(keys) => { + let keys = self.sort_keys(scope, keys, upper)?; + self.w.list("Sort Key", &keys); + } + Kind::Append => { + if self.w.format != Format::Text { + self.w.integer("Subplans Removed", None, 0); + } + } + Kind::WindowAgg { name, window, rows_frame } => { + let spec = self.texts.window(scope, *window, *rows_frame, upper)?; + self.w.text("Window", &format!("{} AS {spec}", self.texts.quote(name))); + } + Kind::Modify { conflict: Some(conflict), .. } => { + self.w.text( + "Conflict Resolution", + if conflict.update { "UPDATE" } else { "NOTHING" }, + ); + if !conflict.arbiters.is_empty() { + let names: Vec = conflict + .arbiters + .iter() + .map(|oid| self.texts.object(*oid, false).0) + .collect(); + self.w.list("Conflict Arbiter Indexes", &names); + } + if let Some(update) = conflict.scope { + self.qual("Conflict Filter", update, &conflict.filter, upper)?; + } + } + _ => {} + } + Ok(()) + } + + /// `show_qual`: the conditions of a node, with `AND` between them, or nothing for no condition. + fn qual(&mut self, label: &str, scope: usize, items: &[Item<'_>], prefix: bool) -> Result<()> { + if items.is_empty() { + return Ok(()); + } + let texts = self.items(scope, items, prefix)?; + let text = if texts.len() == 1 { + texts.into_iter().next().unwrap_or_default() + } else { + format!("({})", texts.join(" AND ")) + }; + self.w.text(label, &text); + Ok(()) + } + + /// The texts of the expressions of a node. An expression that the node reads from the node below and that is not a column is in parentheses, as `get_special_variable` writes it. + fn items(&mut self, scope: usize, items: &[Item<'_>], prefix: bool) -> Result> { + items + .iter() + .map(|item| { + let text = self.texts.expr(scope, &item.expr, prefix)?; + Ok(if item.below && !matches!(item.expr.kind, ExprKind::Var(_)) { + format!("({text})") + } else { + text + }) + }) + .collect() + } + + /// `show_sort_group_keys`: the keys with ` DESC`, ` USING` and the place of the nulls when they are not the defaults of the order. + fn sort_keys( + &mut self, + scope: usize, + keys: &[SortKey<'_>], + prefix: bool, + ) -> Result> { + let mut out = Vec::with_capacity(keys.len()); + for key in keys { + let mut text = self.items(scope, std::slice::from_ref(&key.item), prefix)?.remove(0); + let reverse = match &key.using { + Some(op) => { + text.push_str(" USING "); + text.push_str(op); + key.desc + } + None if key.desc => { + text.push_str(" DESC"); + true + } + None => false, + }; + if key.nulls_first && !reverse { + text.push_str(" NULLS FIRST"); + } else if !key.nulls_first && reverse { + text.push_str(" NULLS LAST"); + } + out.push(text); + } + Ok(out) + } + + /// `show_grouping_sets` in text: a `Hash Key` for each set in a hash table and a `Group Key` for each set of a sorted input, with `()` for a set with no keys. + fn grouping_sets( + &mut self, + scope: usize, + sets: &[(bool, Vec>)], + prefix: bool, + ) -> Result<()> { + for (hashed, keys) in sets { + let label = if *hashed { "Hash Key" } else { "Group Key" }; + let texts = self.items(scope, keys, prefix)?; + if texts.is_empty() { + self.w.list(label, &["()".to_string()]); + } else { + self.w.list(label, &texts); + } + } + Ok(()) + } + + /// `show_tablesample`: the method of `TABLESAMPLE` with its arguments, and `REPEATABLE`. + fn sampling(&mut self, scope: usize, relation: usize, prefix: bool) -> Result<()> { + let Some(sample) = &self.plan.scopes[scope].query.relations[relation].tablesample else { + return Ok(()); + }; + let (method, _) = self.texts.object(sample.handler, true); + let args: Vec = sample + .args + .iter() + .map(|arg| self.texts.expr(scope, arg, prefix)) + .collect::>()?; + self.w.text("Sampling", &format!("{method} ({})", args.join(", "))); + if let Some(repeatable) = &sample.repeatable { + let text = self.texts.expr(scope, repeatable, prefix)?; + self.w.text("Repeatable Seed", &text); + } + Ok(()) + } +} diff --git a/crates/rupg-plan/src/format.rs b/crates/rupg-plan/src/format.rs new file mode 100644 index 00000000..ac975d06 --- /dev/null +++ b/crates/rupg-plan/src/format.rs @@ -0,0 +1,312 @@ +//! The forms of the output of `EXPLAIN`, as `explain_format.c`: the groups and the properties in text, XML, JSON and YAML. + +use crate::Format; + +/// The output of `EXPLAIN` while it grows, as the parts of `ExplainState` that the forms use. +#[derive(Debug)] +pub(crate) struct Writer { + pub(crate) format: Format, + pub(crate) buf: String, + /// `indent`: the level of the current line. + pub(crate) indent: usize, + /// `grouping_stack`: for JSON, true when the group has an item; for YAML, true when the group has a label. + stack: Vec, +} + +impl Writer { + pub(crate) fn new(format: Format) -> Writer { + Writer { format, buf: String::new(), indent: 0, stack: Vec::new() } + } + + /// `ExplainBeginOutput`. + pub(crate) fn begin(&mut self) { + match self.format { + Format::Text => {} + Format::Xml => { + self.buf.push_str("\n"); + self.indent += 1; + } + Format::Json => { + self.buf.push('['); + self.stack.push(false); + self.indent += 1; + } + Format::Yaml => self.stack.push(false), + } + } + + /// `ExplainEndOutput`. + pub(crate) fn end(&mut self) { + match self.format { + Format::Text => {} + Format::Xml => { + self.indent -= 1; + self.buf.push_str(""); + } + Format::Json => { + self.indent -= 1; + self.buf.push_str("\n]"); + self.stack.pop(); + } + Format::Yaml => { + self.stack.pop(); + } + } + } + + /// `ExplainOpenGroup`: a group of properties of the type `objtype`, with the label `label` in JSON and YAML. A group is an object when `labeled` is true, or else a list. + pub(crate) fn open_group(&mut self, objtype: &str, label: Option<&str>, labeled: bool) { + match self.format { + Format::Text => {} + Format::Xml => { + self.xml_tag(objtype, false, true); + self.indent += 1; + } + Format::Json => { + self.json_line_ending(); + self.spaces(2 * self.indent); + if let Some(label) = label { + escape_json(&mut self.buf, label); + self.buf.push_str(": "); + } + self.buf.push(if labeled { '{' } else { '[' }); + self.stack.push(false); + self.indent += 1; + } + Format::Yaml => { + self.yaml_line_starting(); + if let Some(label) = label { + self.buf.push_str(label); + self.buf.push_str(": "); + self.stack.push(true); + } else { + self.buf.push_str("- "); + self.stack.push(false); + } + self.indent += 1; + } + } + } + + /// `ExplainCloseGroup`. + pub(crate) fn close_group(&mut self, objtype: &str, labeled: bool) { + match self.format { + Format::Text => {} + Format::Xml => { + self.indent -= 1; + self.xml_tag(objtype, true, true); + } + Format::Json => { + self.indent -= 1; + self.buf.push('\n'); + self.spaces(2 * self.indent); + self.buf.push(if labeled { '}' } else { ']' }); + self.stack.pop(); + } + Format::Yaml => { + self.indent -= 1; + self.stack.pop(); + } + } + } + + /// `ExplainProperty`: a property with a text, or a number or a Boolean value when `numeric` is true, which JSON and YAML do not quote. Text adds the unit after the value. + pub(crate) fn property(&mut self, label: &str, unit: Option<&str>, value: &str, numeric: bool) { + match self.format { + Format::Text => { + self.indent_text(); + self.buf.push_str(label); + self.buf.push_str(": "); + self.buf.push_str(value); + if let Some(unit) = unit { + self.buf.push(' '); + self.buf.push_str(unit); + } + self.buf.push('\n'); + } + Format::Xml => { + self.spaces(2 * self.indent); + self.xml_tag(label, false, false); + escape_xml(&mut self.buf, value); + self.xml_tag(label, true, false); + self.buf.push('\n'); + } + Format::Json => { + self.json_line_ending(); + self.spaces(2 * self.indent); + escape_json(&mut self.buf, label); + self.buf.push_str(": "); + if numeric { + self.buf.push_str(value); + } else { + escape_json(&mut self.buf, value); + } + } + Format::Yaml => { + self.yaml_line_starting(); + self.buf.push_str(label); + self.buf.push_str(": "); + if numeric { + self.buf.push_str(value); + } else { + escape_json(&mut self.buf, value); + } + } + } + } + + /// `ExplainPropertyText`. + pub(crate) fn text(&mut self, label: &str, value: &str) { + self.property(label, None, value, false); + } + + /// `ExplainPropertyBool`. + pub(crate) fn boolean(&mut self, label: &str, value: bool) { + self.property(label, None, if value { "true" } else { "false" }, true); + } + + /// `ExplainPropertyFloat`: a number with `digits` digits after the point. + pub(crate) fn float(&mut self, label: &str, unit: Option<&str>, value: f64, digits: usize) { + self.property(label, unit, &format!("{value:.digits$}"), true); + } + + /// `ExplainPropertyInteger`. + pub(crate) fn integer(&mut self, label: &str, unit: Option<&str>, value: i64) { + self.property(label, unit, &value.to_string(), true); + } + + /// `ExplainPropertyList`: a property with a list of texts. + pub(crate) fn list(&mut self, label: &str, items: &[String]) { + match self.format { + Format::Text => { + self.indent_text(); + self.buf.push_str(label); + self.buf.push_str(": "); + self.buf.push_str(&items.join(", ")); + self.buf.push('\n'); + } + Format::Xml => { + self.xml_tag(label, false, true); + for item in items { + self.spaces(2 * self.indent + 2); + self.buf.push_str(""); + escape_xml(&mut self.buf, item); + self.buf.push_str("\n"); + } + self.xml_tag(label, true, true); + } + Format::Json => { + self.json_line_ending(); + self.spaces(2 * self.indent); + escape_json(&mut self.buf, label); + self.buf.push_str(": ["); + for (i, item) in items.iter().enumerate() { + if i > 0 { + self.buf.push_str(", "); + } + escape_json(&mut self.buf, item); + } + self.buf.push(']'); + } + Format::Yaml => { + self.yaml_line_starting(); + self.buf.push_str(label); + self.buf.push_str(": "); + for item in items { + self.buf.push('\n'); + self.spaces(2 * self.indent + 2); + self.buf.push_str("- "); + escape_json(&mut self.buf, item); + } + } + } + } + + /// `ExplainIndentText`: the spaces of the level at the start of a line of text. + pub(crate) fn indent_text(&mut self) { + if self.buf.is_empty() || self.buf.ends_with('\n') { + self.spaces(2 * self.indent); + } + } + + /// `appendStringInfoSpaces`. + fn spaces(&mut self, count: usize) { + for _ in 0..count { + self.buf.push(' '); + } + } + + /// `ExplainXMLTag`: an opening or a closing tag. Each character of the name that is not a letter, a digit, `-`, `_` or `.` becomes `-`. With `whitespace` the tag has its own line. + fn xml_tag(&mut self, name: &str, closing: bool, whitespace: bool) { + if whitespace { + self.spaces(2 * self.indent); + } + self.buf.push('<'); + if closing { + self.buf.push('/'); + } + for c in name.chars() { + let keep = c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'); + self.buf.push(if keep { c } else { '-' }); + } + self.buf.push('>'); + if whitespace { + self.buf.push('\n'); + } + } + + /// `ExplainJSONLineEnding`: a comma after the item before, and a new line. + fn json_line_ending(&mut self) { + if let Some(top) = self.stack.last_mut() { + if *top { + self.buf.push(','); + } else { + *top = true; + } + } + self.buf.push('\n'); + } + + /// `ExplainYAMLLineStarting`: a new line with the indent, except for the first item of a group with no label, which goes after its `- `. + fn yaml_line_starting(&mut self) { + match self.stack.last_mut() { + Some(top) if !*top => *top = true, + _ => { + self.buf.push('\n'); + self.spaces(2 * self.indent); + } + } + } +} + +/// `escape_json`: the text in double quotes, with the escapes of JSON. +pub(crate) fn escape_json(buf: &mut String, text: &str) { + buf.push('"'); + for c in text.chars() { + match c { + '\u{8}' => buf.push_str("\\b"), + '\u{c}' => buf.push_str("\\f"), + '\n' => buf.push_str("\\n"), + '\r' => buf.push_str("\\r"), + '\t' => buf.push_str("\\t"), + '"' => buf.push_str("\\\""), + '\\' => buf.push_str("\\\\"), + c if u32::from(c) < 0x20 => buf.push_str(&format!("\\u{:04x}", u32::from(c))), + c => buf.push(c), + } + } + buf.push('"'); +} + +/// `escape_xml`: the text with the entities of `&`, `<`, `>` and a carriage return. +fn escape_xml(buf: &mut String, text: &str) { + for c in text.chars() { + match c { + '&' => buf.push_str("&"), + '<' => buf.push_str("<"), + '>' => buf.push_str(">"), + '\r' => buf.push_str(" "), + c => buf.push(c), + } + } +} diff --git a/crates/rupg-plan/src/lib.rs b/crates/rupg-plan/src/lib.rs index 6013e24c..de4154a8 100644 --- a/crates/rupg-plan/src/lib.rs +++ b/crates/rupg-plan/src/lib.rs @@ -1,5 +1,317 @@ //! The physical plan, its text form, `EXPLAIN` output (document 15 section 15.9). //! -//! This crate first ships in milestone M2. See `spec/22-crate-layout.md` section 22.4 and `spec/23-milestones.md`. +//! A [`Plan`] is a tree of [`Node`]s, as the `Plan` nodes of PostgreSQL. `rupg-opt` makes it from a query. The expressions of the nodes are the expressions of the query, so the text of an expression needs the names of the relations of the query and the deparse of `rupg-func`, which the trait [`Texts`] gives. [`explain`] writes the plan in the text, JSON, XML and YAML forms of `explain.c`. #![forbid(unsafe_code)] + +mod explain; +mod format; + +use std::borrow::Cow; + +use rupg_analyze::{Expr, ExprKind, JoinKind, Query, SubLink}; +use rupg_common::Result; + +pub use explain::{Format, Options, explain}; + +/// The relation of a scope that is the target table of an `INSERT`, which is not a relation of the query. +pub const TARGET: usize = usize::MAX; + +/// A plan: the tree of nodes, the queries that the expressions of the nodes read, and the names of the subqueries in the expressions. +#[derive(Debug)] +pub struct Plan<'q> { + pub root: Node<'q>, + /// The queries of the plan, the query of the statement first. A query comes after the query outside it. + pub scopes: Vec>, + /// The name of the subplan of each subquery in an expression. + pub sublinks: Vec>, + /// The scopes in the order of the range table of PostgreSQL, which gives each relation a name that is unique in the plan, as `set_rtable_names`: the scopes of the statement, then the scopes of each subplan after the subplans in it. A scope that is not in the list does not take part in the unique names. + pub order: Vec, +} + +impl Plan<'_> { + /// The number of entries of the range table of PostgreSQL for the plan, as `list_length(es->rtable)`: the relations and the joins of each query, and the target of an `INSERT`. A query with no relation has one entry of the kind `RTE_RESULT`. + pub fn rtable_size(&self) -> usize { + self.scopes + .iter() + .map(|scope| { + let query = scope.query; + let joins: usize = query.from.iter().map(join_count).sum(); + (query.relations.len() + joins).max(1) + usize::from(scope.target) + }) + .sum() + } + + /// The label of a subquery of an expression of the plan. + pub fn label(&self, link: &SubLink) -> Option<&Label<'_>> { + self.sublinks.iter().find(|l| std::ptr::eq(l.link, link)) + } +} + +/// The number of joins in a part of `FROM`. +fn join_count(item: &rupg_analyze::FromItem) -> usize { + match item { + rupg_analyze::FromItem::Relation(_) => 0, + rupg_analyze::FromItem::Join(join) => 1 + join_count(&join.left) + join_count(&join.right), + } +} + +/// A query of the plan. +#[derive(Clone, Copy, Debug)] +pub struct Scope<'q> { + pub query: &'q Query, + /// The scope of the query outside it, where a `Var` with a `levels_up` of 1 finds its relation. + pub parent: Option, + /// True when the query gives the rows of an `INSERT`, whose target table is one more entry of the range table, [`TARGET`]. + pub target: bool, +} + +/// The name of the subplan of a subquery in an expression, as `SubPlan.plan_name`, such as `InitPlan expr_1` or `SubPlan any_1`. +#[derive(Debug)] +pub struct Label<'q> { + pub link: &'q SubLink, + pub name: String, + /// True when the subplan is an `InitPlan`, which runs once and gives its value as a parameter. + pub init: bool, + /// `useHashTable`: the subplan keeps its rows in a hash table, which the test of `ANY` reads. + pub hashed: bool, +} + +/// A node of the plan. +#[derive(Debug)] +pub struct Node<'q> { + pub kind: Kind<'q>, + /// The index of the query of the expressions of the node in [`Plan::scopes`]. + pub scope: usize, + pub cost: Cost, + /// `targetlist`: the expressions of the rows that the node gives. + pub output: Vec>, + /// `qual`: the conditions that each row of the node must pass. + pub filter: Vec>, + /// The nodes that give the rows of the node: the outer node first, then the inner node, or the members of an `Append`. + pub children: Vec>, + /// `initPlan`: the subplans that run once before the node. + pub init_plans: Vec>, + /// `subPlan`: the subplans that the expressions of the node run for each row. + pub sub_plans: Vec>, +} + +impl<'q> Node<'q> { + /// A node with no expressions and no subplans. + pub fn new(kind: Kind<'q>, scope: usize, children: Vec>) -> Node<'q> { + Node { + kind, + scope, + cost: Cost::default(), + output: Vec::new(), + filter: Vec::new(), + children, + init_plans: Vec::new(), + sub_plans: Vec::new(), + } + } +} + +/// A subplan with its name, such as `InitPlan expr_1`, `SubPlan any_1` or `CTE w`. +#[derive(Debug)] +pub struct SubPlan<'q> { + pub name: String, + pub plan: Node<'q>, +} + +/// The estimates of the planner for a node. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct Cost { + /// `startup_cost`: the cost before the first row. + pub startup: f64, + /// `total_cost`: the cost of all the rows. + pub total: f64, + /// `plan_rows`. + pub rows: f64, + /// `plan_width`: the average width of a row in bytes. + pub width: i32, +} + +/// An expression of a node. +#[derive(Clone, Debug)] +pub struct Item<'q> { + pub expr: Cow<'q, Expr>, + /// True when a node below computes the expression and the node reads it as a column of its input, as `OUTER_VAR`. The text of such an expression that is not a column is in parentheses. + pub below: bool, +} + +impl<'q> Item<'q> { + /// An expression of the query that the node computes. + pub fn of(expr: &'q Expr) -> Item<'q> { + Item { expr: Cow::Borrowed(expr), below: false } + } + + /// An expression that the node reads from the node below. + pub fn below(expr: &'q Expr) -> Item<'q> { + Item { expr: Cow::Borrowed(expr), below: true } + } + + /// An expression that the planner makes. + pub fn owned(expr: Expr) -> Item<'q> { + Item { expr: Cow::Owned(expr), below: false } + } + + /// The same expression as the input of the node above. + pub fn passed(&self) -> Item<'q> { + Item { expr: self.expr.clone(), below: !matches!(self.expr.kind, ExprKind::Var(_)) } + } +} + +/// A key of a sort. +#[derive(Clone, Debug)] +pub struct SortKey<'q> { + pub item: Item<'q>, + /// True when the operator is the `>` of the type, as `DESC`. + pub desc: bool, + /// The name of an operator of `USING` that is not the `<` or the `>` of the type. + pub using: Option, + pub nulls_first: bool, +} + +/// The kinds of node, with the parts that each kind shows. +#[derive(Debug)] +pub enum Kind<'q> { + /// `Result`: a row with no relation, or the rows of the node below when a condition with no column, `resconstantqual`, is true. + Result { replaces: Option, one_time: Vec> }, + /// `SeqScan` of a relation of the scope. + SeqScan(usize), + /// `SampleScan`: a table with `TABLESAMPLE`. + SampleScan(usize), + /// `FunctionScan`: a function in `FROM`. + FunctionScan(usize), + /// `TableFuncScan`: `JSON_TABLE` in `FROM`. + TableFunctionScan(usize), + /// `ValuesScan`: a `VALUES` list. + ValuesScan(usize), + /// `SubqueryScan`: a subquery in `FROM`, whose plan is the child. + SubqueryScan(usize), + /// `CteScan`: a query of `WITH` that the plan runs once. + CteScan(usize), + /// `WorkTableScan`: the reference of the recursive term of a query of `WITH` to itself. + WorkTableScan(usize), + /// `NestLoop`, with the condition of the join, `joinqual`. + NestedLoop { join: JoinKind, join_filter: Vec> }, + /// `MergeJoin`, with the `=` conditions of the merge of the two sorted inputs, `mergeclauses`, and the other conditions of the join. + MergeJoin { join: JoinKind, cond: Vec>, join_filter: Vec> }, + /// `HashJoin`, with the `=` conditions that the hash table tests, `hashclauses`, and the other conditions of the join. + HashJoin { join: JoinKind, cond: Vec>, join_filter: Vec> }, + /// `Hash`: the hash table of the inner side of a hash join. + Hash, + /// `Material`: the rows of the node below, which the node keeps for the next scans. + Materialize, + /// `Agg`, with the keys of the groups. Each grouping set has its keys, and a set with no keys makes one group. + Aggregate { strategy: Strategy, keys: Vec>, sets: Vec<(bool, Vec>)> }, + /// `Sort`. + Sort(Vec>), + /// `Limit`: `LIMIT` and `OFFSET`. + Limit, + /// `Unique`: the first row of each run of equal rows of a sorted input. + Unique, + /// `Append`: the rows of each member. + Append, + /// `SetOp` with a hash table: `INTERSECT` or `EXCEPT`. + SetOp { except: bool, all: bool }, + /// `RecursiveUnion`. + RecursiveUnion, + /// `WindowAgg`, with the name of the window and its index in the windows of the query. + WindowAgg { name: String, window: usize, rows_frame: bool }, + /// `ProjectSet`: a target list with functions that give sets. + ProjectSet, + /// `LockRows`: `FOR UPDATE` and the other locks of rows. + LockRows, + /// `ModifyTable`, with the OID of the table and the relation of the scope that the table is. + Modify { operation: Operation, table: u32, relation: usize, conflict: Option> }, +} + +/// The relation or the node that a `Result` takes the place of, as `result_type`. +#[derive(Clone, Debug)] +pub enum Replaced { + /// A scan of a relation of the scope with a condition that is never true. + Scan(usize), + /// A join of the relations of the scope with a condition that is never true. + Join(Vec), +} + +/// The strategy of an `Agg`, as `AggStrategy`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Strategy { + /// `AGG_PLAIN`: one group of all the rows. + Plain, + /// `AGG_SORTED`: groups of a sorted input. + Sorted, + /// `AGG_HASHED`: groups in a hash table. + Hashed, + /// `AGG_MIXED`: grouping sets, some in hash tables and some of a sorted input. + Mixed, +} + +/// The command of a `ModifyTable`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Operation { + Insert, + Update, + Delete, + Merge, +} + +impl Operation { + /// The name of the command, as `EXPLAIN` shows it. + pub fn name(self) -> &'static str { + match self { + Operation::Insert => "Insert", + Operation::Update => "Update", + Operation::Delete => "Delete", + Operation::Merge => "Merge", + } + } +} + +/// The `ON CONFLICT` clause of an `INSERT`. +#[derive(Debug)] +pub struct Conflict<'q> { + /// True for `DO UPDATE`, false for `DO NOTHING`. + pub update: bool, + /// The OIDs of the arbiter indexes. + pub arbiters: Vec, + /// The condition of `WHERE` of `DO UPDATE`, in the scope of the query of `DO UPDATE`. + pub filter: Vec>, + /// The scope of the query of `DO UPDATE`. + pub scope: Option, +} + +/// The texts that `EXPLAIN` shows for the parts of a plan: the deparse of the expressions and the names of the relations. `rupg-func` gives them. +pub trait Texts { + /// The text of an expression of the query of a scope, with the names of the relations before the columns when `prefix` is true. A column of a query outside the scope always shows the name of its relation. + /// + /// # Errors + /// + /// The errors of the deparse. + fn expr(&mut self, scope: usize, expr: &Expr, prefix: bool) -> Result; + + /// The name of a relation of a scope in the plan, `refname`, which is unique in the plan. [`TARGET`] is the target table of an `INSERT`. + fn refname(&mut self, scope: usize, relation: usize) -> String; + + /// The identifier in double quotes when it needs them, as `quote_identifier`. + fn quote(&self, name: &str) -> String; + + /// The name and the schema of a relation or of a function. + fn object(&mut self, oid: u32, function: bool) -> (String, String); + + /// The text of the definition of a window of the query of a scope, as `(PARTITION BY ...)`. With `rows_frame` the frame is `ROWS UNBOUNDED PRECEDING`, which the planner gives to the windows of `row_number` and the other functions of ranks. + /// + /// # Errors + /// + /// The errors of the deparse. + fn window( + &mut self, + scope: usize, + window: usize, + rows_frame: bool, + prefix: bool, + ) -> Result; +} From e0a67b285ea568ed6b2542960cdfba2df358742a Mon Sep 17 00:00:00 2001 From: Tam Nguyen Duc <1218621+tamnd@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:21:24 +0700 Subject: [PATCH 2/3] Add EXPLAIN to the session --- Cargo.lock | 13 + crates/rupg-exec/src/lib.rs | 5 + crates/rupg-func/Cargo.toml | 1 + crates/rupg-func/src/deparse.rs | 47 +- crates/rupg-func/src/deparse/explain.rs | 274 ++++ crates/rupg-func/src/deparse/query.rs | 32 +- crates/rupg-func/src/lib.rs | 2 +- crates/rupg-opt/src/from.rs | 11 + crates/rupg-opt/src/lib.rs | 29 +- crates/rupg-opt/src/output.rs | 90 +- crates/rupg-opt/src/upper.rs | 93 +- crates/rupg-plan/src/explain.rs | 3 +- crates/rupg-session/Cargo.toml | 2 + crates/rupg-session/src/connection.rs | 16 + crates/rupg-session/src/connection/explain.rs | 185 +++ .../rupg-session/src/connection/explain.test | 1181 +++++++++++++++++ .../src/connection/explain_keys.test | 738 ++++++++++ 17 files changed, 2691 insertions(+), 31 deletions(-) create mode 100644 crates/rupg-func/src/deparse/explain.rs create mode 100644 crates/rupg-session/src/connection/explain.rs create mode 100644 crates/rupg-session/src/connection/explain.test create mode 100644 crates/rupg-session/src/connection/explain_keys.test diff --git a/Cargo.lock b/Cargo.lock index 275f8002..27d3e902 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -382,6 +382,7 @@ dependencies = [ "rupg-catalog", "rupg-common", "rupg-pgcatalog", + "rupg-plan", "rupg-sql", "rupg-types", "sha2", @@ -433,6 +434,12 @@ dependencies = [ [[package]] name = "rupg-opt" version = "0.1.51" +dependencies = [ + "rupg-analyze", + "rupg-pgcatalog", + "rupg-plan", + "rupg-types", +] [[package]] name = "rupg-pgcatalog" @@ -441,6 +448,10 @@ version = "0.1.51" [[package]] name = "rupg-plan" version = "0.1.51" +dependencies = [ + "rupg-analyze", + "rupg-common", +] [[package]] name = "rupg-platform" @@ -479,7 +490,9 @@ dependencies = [ "rupg-common", "rupg-exec", "rupg-func", + "rupg-opt", "rupg-pgcatalog", + "rupg-plan", "rupg-platform", "rupg-snowball", "rupg-sql", diff --git a/crates/rupg-exec/src/lib.rs b/crates/rupg-exec/src/lib.rs index af0896ce..49407c6f 100644 --- a/crates/rupg-exec/src/lib.rs +++ b/crates/rupg-exec/src/lib.rs @@ -1064,6 +1064,11 @@ fn run_query( } impl Plan { + /// The query of the plan after `prepare` and `fold`, which `EXPLAIN` shows. + pub fn query(&self) -> &Query { + &self.query + } + /// The columns of the result. The junk columns of `ORDER BY` do not count. A query that writes has the columns of its `RETURNING`, or none. pub fn columns(&self) -> &[Target] { if let Some(write) = &self.query.write { diff --git a/crates/rupg-func/Cargo.toml b/crates/rupg-func/Cargo.toml index 580d5ee1..2500e6d2 100644 --- a/crates/rupg-func/Cargo.toml +++ b/crates/rupg-func/Cargo.toml @@ -19,6 +19,7 @@ rupg-analyze.workspace = true rupg-catalog.workspace = true rupg-common.workspace = true rupg-pgcatalog.workspace = true +rupg-plan.workspace = true rupg-sql.workspace = true rupg-types.workspace = true sha2.workspace = true diff --git a/crates/rupg-func/src/deparse.rs b/crates/rupg-func/src/deparse.rs index 9d7d4325..f7f2b1d0 100644 --- a/crates/rupg-func/src/deparse.rs +++ b/crates/rupg-func/src/deparse.rs @@ -2,9 +2,11 @@ //! //! The functions read the stored form that `rupg_analyze::node` writes, or the query that the analyzer makes from the text of a view, and show it as PostgreSQL shows its node trees. The rules for parentheses, casts and constants follow `get_rule_expr`. The functions show the objects of the user and the system views. An index or a constraint of a system catalog gives an error that says that the engine cannot show it yet. +mod explain; mod function; mod query; +pub use explain::explain; pub use query::view_statement; use std::ptr; @@ -44,7 +46,7 @@ const NULLS_FIRST: i16 = 2; const FIRST_USER_OID: u32 = 16384; /// The names of the relations of one query and of their columns, as `deparse_namespace` with its `deparse_columns`. -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] struct Namespace { /// The name of each relation of the query, by its index, as `rtable_names`. The name is unique in the query and in the queries outside it. names: Vec, @@ -63,7 +65,7 @@ struct Namespace { } /// The names of a join and of its columns, as `deparse_columns` for an entry of a join. -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] struct JoinNames { /// The name of the join with an alias, which no other relation of the query has, or `None` for a join with no alias. name: Option, @@ -102,9 +104,29 @@ struct Deparser<'a> { windows: Vec, /// The expressions of the targets of the query, which the items of the windows name. window_targets: Vec, + /// For `EXPLAIN`, the subplans of the subqueries in the expressions. A subquery shows the name of its subplan, as `get_rule_expr` shows a `SubPlan`. + subplans: Vec, + /// For `EXPLAIN`, the subplan whose columns the test of `ANY` or `ALL` reads now, as `(SubPlan any_1)`. + subplan: Option, + /// For `EXPLAIN`, the name of each window of the query by its number, which a call of a window function shows after `OVER`. + window_names: Vec>, + /// For `EXPLAIN`, a column of a query outside the query always shows the name of its relation, as `get_parameter` shows the value of a parameter. + outer_prefix: bool, buf: String, } +/// The name of the subplan of a subquery in an expression, for `EXPLAIN`. +#[derive(Clone, Debug)] +struct SubName { + /// The subquery. The deparse only compares the address. + link: *const SubLink, + name: String, + /// The subplan is an `InitPlan`. + init: bool, + /// The subplan keeps its rows in a hash table. + hashed: bool, +} + impl<'a> Deparser<'a> { /// A deparse of an expression of one relation, as `deparse_context_for`: a `Var` shows the name of its column with no name of the relation. fn for_columns(session: &'a dyn Session, columns: Vec, paren: bool) -> Deparser<'a> { @@ -135,6 +157,10 @@ impl<'a> Deparser<'a> { outputs: Vec::new(), windows: Vec::new(), window_targets: Vec::new(), + subplans: Vec::new(), + subplan: None, + window_names: Vec::new(), + outer_prefix: false, buf: String::new(), } } @@ -169,7 +195,7 @@ impl<'a> Deparser<'a> { }; let attname = attname.ok_or_else(|| Error::internal(format!("invalid attnum {}", var.attnum)))?; - let mut prefix = self.prefix; + let mut prefix = self.prefix || (self.outer_prefix && var.levels_up > 0); if in_order_by && !self.in_group_by && !prefix { prefix = self.outputs.iter().any(|(name, v)| *name == attname && *v != Some(*var)); } @@ -446,9 +472,18 @@ impl<'a> Deparser<'a> { self.buf.push(')'); } ExprKind::Window(w) => self.window_func(w)?, - ExprKind::SubLink(sub) => self.sublink(sub)?, - ExprKind::SubColumn(_) => { - return Err(Error::internal("a column of a subquery outside its test")); + ExprKind::SubLink(sub) => { + let link = ptr::from_ref(sub.as_ref()); + match self.subplans.iter().find(|s| s.link == link).cloned() { + Some(name) => self.subplan_link(sub, &name)?, + None => self.sublink(sub)?, + } + } + ExprKind::SubColumn(column) => { + let Some(subplan) = &self.subplan else { + return Err(Error::internal("a column of a subquery outside its test")); + }; + self.buf.push_str(&format!("{subplan}.col{}", column + 1)); } ExprKind::JsonValue(v) => { self.expr(&v.raw, false)?; diff --git a/crates/rupg-func/src/deparse/explain.rs b/crates/rupg-func/src/deparse/explain.rs new file mode 100644 index 00000000..86cfa75f --- /dev/null +++ b/crates/rupg-func/src/deparse/explain.rs @@ -0,0 +1,274 @@ +//! The texts of a plan for `EXPLAIN`: the names of its relations, as `select_rtable_names_for_explain` chooses them, and its expressions, as `deparse_expression` shows them with the context of `set_deparse_context_plan`. + +use std::collections::HashMap; +use std::ptr; + +use rupg_analyze::{Expr, SubLink, SubLinkKind}; +use rupg_plan::{Kind, Node, Plan, TARGET, Texts}; +use rupg_sql::nodes::FRAMEOPTION_NONDEFAULT; + +use super::{ + Deparser, Error, Namespace, Result, SubName, builtin, quote_identifier, reg, sort_operators, +}; +use crate::Session; + +/// The output of `EXPLAIN` for a plan, with the names and the expressions that the session shows. +/// +/// # Errors +/// +/// The errors of the deparse of the expressions. +pub fn explain( + plan: &Plan<'_>, + options: &rupg_plan::Options, + session: &dyn Session, +) -> Result { + let mut texts = PlanTexts::new(plan, session); + rupg_plan::explain(plan, options, &mut texts) +} + +/// The names of the relations of each query of a plan and the names of its subplans and of its windows. +struct PlanTexts<'a, 'q> { + session: &'a dyn Session, + plan: &'a Plan<'q>, + /// The names of each scope of the plan. + spaces: Vec, + /// The name of the target table of each scope of an `INSERT`. + targets: Vec>, + /// The name of each window of each scope, as `WindowAgg.winname`. + windows: Vec>>, + subplans: Vec, +} + +impl<'a, 'q> PlanTexts<'a, 'q> { + /// The names of the relations of the plan. The scopes in [`Plan::order`] take their names in that order, and each name is unique in all of them. The target table of an `INSERT` is the first relation of its query. + fn new(plan: &'a Plan<'q>, session: &'a dyn Session) -> PlanTexts<'a, 'q> { + let d = Deparser::new(session, false, 0); + let count = plan.scopes.len(); + let mut spaces: Vec> = vec![None; count]; + let mut targets = vec![None; count]; + let mut used: HashMap = HashMap::new(); + for &s in &plan.order { + let Some(scope) = plan.scopes.get(s) else { continue }; + if spaces[s].is_some() { + continue; + } + if scope.target + && let Some(write) = &scope.query.write + { + let base = reg::class_name(write.table, session) + .map_or_else(|| write.name.clone(), |(name, _)| name.to_string()); + targets[s] = Some(super::query::unique_name(base, &mut used)); + } + spaces[s] = Some(d.namespace_in(scope.query, &mut used, true)); + } + let spaces = spaces + .into_iter() + .enumerate() + .map(|(s, space)| { + space.unwrap_or_else(|| { + d.namespace_in(plan.scopes[s].query, &mut HashMap::new(), true) + }) + }) + .collect(); + let mut windows: Vec>> = + plan.scopes.iter().map(|s| vec![None; s.query.windows.len()]).collect(); + window_names(&plan.root, &mut windows); + let subplans = plan + .sublinks + .iter() + .map(|l| SubName { + link: ptr::from_ref(l.link), + name: l.name.clone(), + init: l.init, + hashed: l.hashed, + }) + .collect(); + PlanTexts { session, plan, spaces, targets, windows, subplans } + } + + /// A deparse of the expressions of a scope, with the names of the scope and of the scopes outside it. + fn deparser(&self, scope: usize, prefix: bool) -> Result> { + let mut chain = Vec::new(); + let mut at = Some(scope); + while let Some(s) = at { + let Some(found) = self.plan.scopes.get(s) else { + return Err(Error::internal(format!("no scope {s} in the plan"))); + }; + chain.push(s); + at = found.parent; + } + let mut d = Deparser::new(self.session, false, 0); + d.namespaces = chain.iter().rev().map(|s| self.spaces[*s].clone()).collect(); + d.prefix = prefix; + d.outer_prefix = true; + let query = self.plan.scopes[scope].query; + d.windows.clone_from(&query.windows); + d.window_targets = query.targets.iter().map(|t| t.expr.clone()).collect(); + d.window_names.clone_from(&self.windows[scope]); + d.subplans.clone_from(&self.subplans); + Ok(d) + } +} + +/// The names of the windows of the nodes of a plan, by the scope and the number of each window. +fn window_names(node: &Node<'_>, out: &mut [Vec>]) { + if let Kind::WindowAgg { name, window, .. } = &node.kind + && let Some(slot) = out.get_mut(node.scope).and_then(|w| w.get_mut(*window)) + { + *slot = Some(name.clone()); + } + for child in &node.children { + window_names(child, out); + } + for sub in node.init_plans.iter().chain(&node.sub_plans) { + window_names(&sub.plan, out); + } +} + +impl Texts for PlanTexts<'_, '_> { + fn expr(&mut self, scope: usize, expr: &Expr, prefix: bool) -> Result { + let mut d = self.deparser(scope, prefix)?; + d.expr(expr, false)?; + Ok(d.buf) + } + + fn refname(&mut self, scope: usize, relation: usize) -> String { + if relation == TARGET { + return self.targets.get(scope).cloned().flatten().unwrap_or_default(); + } + self.spaces.get(scope).and_then(|s| s.names.get(relation)).cloned().unwrap_or_default() + } + + fn quote(&self, name: &str) -> String { + quote_identifier(name) + } + + fn object(&mut self, oid: u32, function: bool) -> (String, String) { + let (name, namespace) = if function { + match builtin::proc_by_oid(oid) { + Some(proc) => (proc.name, proc.namespace), + None => return (String::new(), String::new()), + } + } else { + match reg::class_name(oid, self.session) { + Some(found) => found, + None => return (String::new(), String::new()), + } + }; + let schema = reg::namespace_name(namespace, self.session).unwrap_or_default(); + (name.to_string(), schema.to_string()) + } + + fn window( + &mut self, + scope: usize, + window: usize, + rows_frame: bool, + prefix: bool, + ) -> Result { + let query = self.plan.scopes[scope].query; + let clause = query + .windows + .get(window) + .ok_or_else(|| Error::internal(format!("no window {window} in the query")))?; + let target = |target: usize| { + query + .targets + .get(target) + .map(|t| &t.expr) + .ok_or_else(|| Error::internal("an item of a window with no expression")) + }; + let mut d = self.deparser(scope, prefix)?; + d.buf.push('('); + if !clause.partition.is_empty() { + d.buf.push_str("PARTITION BY "); + for (i, item) in clause.partition.iter().enumerate() { + if i > 0 { + d.buf.push_str(", "); + } + d.expr(target(item.target)?, false)?; + } + } + if !clause.order.is_empty() { + if !clause.partition.is_empty() { + d.buf.push(' '); + } + d.buf.push_str("ORDER BY "); + for (i, item) in clause.order.iter().enumerate() { + if i > 0 { + d.buf.push_str(", "); + } + let e = target(item.target)?; + d.expr(e, false)?; + // `show_sortorder_options`. + let (lt, gt) = sort_operators(e.ty); + if item.operator == lt { + if item.nulls_first { + d.buf.push_str(" NULLS FIRST"); + } + } else if item.operator == gt { + d.buf.push_str(" DESC"); + if !item.nulls_first { + d.buf.push_str(" NULLS LAST"); + } + } else if let Some(op) = builtin::operator_by_oid(item.operator) { + d.buf.push_str(&format!(" USING {}", op.name)); + d.buf.push_str(if item.nulls_first { " NULLS FIRST" } else { " NULLS LAST" }); + } + } + } + let space = !clause.partition.is_empty() || !clause.order.is_empty(); + if rows_frame { + if space { + d.buf.push(' '); + } + d.buf.push_str("ROWS UNBOUNDED PRECEDING"); + } else if clause.frame.options & FRAMEOPTION_NONDEFAULT != 0 { + if space { + d.buf.push(' '); + } + d.frame(&clause.frame)?; + } + d.buf.push(')'); + Ok(d.buf) + } +} + +impl Deparser<'_> { + /// `get_rule_expr` for a `SubPlan`: the name of the subplan of a subquery, with the test of `ANY`, `ALL` or a comparison of rows, where each column of the subquery shows as `(SubPlan any_1).col1`. The value of an `InitPlan` shows as `(InitPlan expr_1).col1`, as `get_parameter` shows the parameter that the `InitPlan` sets. + pub(super) fn subplan_link(&mut self, sub: &SubLink, name: &SubName) -> Result<()> { + let plan = match (name.init, name.hashed) { + (true, _) => format!("InitPlan {}", name.name), + (false, true) => format!("hashed SubPlan {}", name.name), + (false, false) => format!("SubPlan {}", name.name), + }; + if name.init { + match &sub.test { + Some(test) => self.subplan_test(test, &plan)?, + None => self.buf.push_str(&format!("({plan}).col1")), + } + return Ok(()); + } + self.buf.push_str(match sub.kind { + SubLinkKind::Exists => "EXISTS(", + SubLinkKind::All => "(ALL ", + SubLinkKind::Any => "(ANY ", + SubLinkKind::Array => "ARRAY(", + SubLinkKind::RowCompare | SubLinkKind::Expr => "(", + }); + match &sub.test { + Some(test) => self.subplan_test(test, &plan)?, + None => self.buf.push_str(&plan), + } + self.buf.push(')'); + Ok(()) + } + + /// The test of a subquery, where a column of the subquery shows as a column of the subplan. + fn subplan_test(&mut self, test: &Expr, plan: &str) -> Result<()> { + let saved = self.subplan.replace(format!("({plan})")); + let done = self.expr(test, false); + self.subplan = saved; + done + } +} diff --git a/crates/rupg-func/src/deparse/query.rs b/crates/rupg-func/src/deparse/query.rs index 0efc5d9c..f745439e 100644 --- a/crates/rupg-func/src/deparse/query.rs +++ b/crates/rupg-func/src/deparse/query.rs @@ -86,6 +86,16 @@ impl Deparser<'_> { used.insert(name.clone(), 0); } } + self.namespace_in(query, &mut used, false) + } + + /// [`Deparser::namespace`] with the names that are in use. For `EXPLAIN`, as `select_rtable_names_for_explain`, a join has no name, because no node of the plan reads a join, and a column of `USING` shows as its value. + pub(super) fn namespace_in( + &self, + query: &Query, + used: &mut HashMap, + explain: bool, + ) -> Namespace { let mut joins = Vec::new(); let mut order = Vec::new(); let tops: Vec = @@ -110,11 +120,13 @@ impl Deparser<'_> { Some((name, _)) if rel.alias.is_none() && rel.oid != 0 => name.to_string(), _ => rel.name.clone(), }; - space.names[i] = unique_name(base, &mut used); + space.names[i] = unique_name(base, used); } Slot::Join(k) => { - if let Some(alias) = &joins[k].join.alias { - join_names[k] = Some(unique_name(alias.clone(), &mut used)); + if let Some(alias) = &joins[k].join.alias + && !explain + { + join_names[k] = Some(unique_name(alias.clone(), used)); } } } @@ -200,8 +212,10 @@ impl Deparser<'_> { print_aliases: print_join[k], }); } - for item in &query.from { - merged_columns(item, &mut space.merged); + if !explain { + for item in &query.from { + merged_columns(item, &mut space.merged); + } } space.relations.clone_from(&query.relations); space @@ -511,7 +525,7 @@ impl Deparser<'_> { } /// `get_window_frame_options`: the mode of the frame, its bounds and its exclusion. - fn frame(&mut self, frame: &Frame) -> Result<()> { + pub(super) fn frame(&mut self, frame: &Frame) -> Result<()> { let options = frame.options; self.buf.push_str(if options & FRAMEOPTION_RANGE != 0 { "RANGE " @@ -620,6 +634,10 @@ impl Deparser<'_> { self.buf.push_str("IGNORE NULLS "); } self.buf.push_str("OVER "); + if let Some(Some(name)) = w.winref.checked_sub(1).and_then(|i| self.window_names.get(i)) { + self.buf.push_str("e_identifier(name)); + return Ok(()); + } let window = w.winref.checked_sub(1).and_then(|i| self.windows.get(i)).cloned(); let Some(window) = window else { return Err(Error::internal(format!( @@ -1533,7 +1551,7 @@ fn walk_from<'q>(item: &'q FromItem, joins: &mut Vec>, order: &mut } /// The name, or the name with `_1`, `_2` and so on when another relation of the query or of a query outside it has the name, as `set_rtable_names` chooses it. -fn unique_name(base: String, used: &mut HashMap) -> String { +pub(super) fn unique_name(base: String, used: &mut HashMap) -> String { let Some(mut counter) = used.get(&base).copied() else { used.insert(base.clone(), 0); return base; diff --git a/crates/rupg-func/src/lib.rs b/crates/rupg-func/src/lib.rs index 5d8cdcbd..35f00c44 100644 --- a/crates/rupg-func/src/lib.rs +++ b/crates/rupg-func/src/lib.rs @@ -85,7 +85,7 @@ pub use agg::check_percentile; pub use cast::local_time; pub use compare::{compare, equal}; pub use datetime::{IntervalSum, interval_lerp}; -pub use deparse::{deparse_expression, deparse_relation_expression, view_statement}; +pub use deparse::{deparse_expression, deparse_relation_expression, explain, view_statement}; pub use encoding::{char_to_encoding, encoding, encoding_to_char}; pub use io::{ base_type, input, input_soft, input_supported, is_row_type, output, output_supported, receive, diff --git a/crates/rupg-opt/src/from.rs b/crates/rupg-opt/src/from.rs index ae6e5571..b09dc345 100644 --- a/crates/rupg-opt/src/from.rs +++ b/crates/rupg-opt/src/from.rs @@ -321,6 +321,17 @@ pub(crate) fn function_rows(call: &Expr) -> f64 { } } } + // `array_unnest_support`: the number of the elements of the array, as `estimate_array_length`. + if name == Some("unnest") + && let [array] = f.args.as_slice() + { + return match &array.strip_implicit().kind { + ExprKind::Const(Value::Array(a)) => a.values.len() as f64, + ExprKind::Const(Value::Null) => 0.0, + ExprKind::Array { multidims: false, elements, .. } => elements.len() as f64, + _ => 10.0, + }; + } match name { _ if !f.retset => 1.0, Some( diff --git a/crates/rupg-opt/src/lib.rs b/crates/rupg-opt/src/lib.rs index 36610cc3..85402414 100644 --- a/crates/rupg-opt/src/lib.rs +++ b/crates/rupg-opt/src/lib.rs @@ -45,6 +45,8 @@ struct Builder<'q> { ctes: Vec>, /// The rows of the part of each recursive query that is not recursive, by the number of its work table, for the estimate of a `WorkTableScan`. work: Vec<(usize, f64)>, + /// True when the next query is the subquery of `EXISTS`. + exists: bool, } /// A query of `WITH` in the scope of the planner. @@ -67,6 +69,8 @@ pub(crate) struct Level<'q> { pub(crate) display_query: &'q Query, /// The subplans that run once before the top node: the queries of `WITH` and the subqueries with no reference to the query. pub(crate) init: Vec>, + /// True when the query gives no targets, as `simplify_EXISTS_query` makes the subquery of `EXISTS`. + pub(crate) no_targets: bool, } impl<'q> Level<'q> { @@ -77,6 +81,19 @@ impl<'q> Level<'q> { } /// The query of the rows of a statement with the conditions of check constraints, which `with_checks` of the analyzer puts around the query of the rows. +/// True when `simplify_EXISTS_query` takes out the targets of the subquery of `EXISTS`: a query with no set operation, aggregate, window, `HAVING`, `LIMIT`, `OFFSET` or row lock. +fn simple_exists(query: &Query) -> bool { + query.set_op.is_none() + && !query.grouped + && query.having.is_none() + && query.windows.is_empty() + && query.limit.is_none() + && query.offset.is_none() + && query.row_marks.is_empty() + && query.sort.is_empty() + && query.distinct.is_empty() +} + fn unwrap_checks(query: &Query) -> &Query { let Some(write) = &query.write else { return query }; match query.relations.as_slice() { @@ -96,8 +113,15 @@ impl<'q> Builder<'q> { let query = unwrap_checks(outer); let target = write.is_some_and(|w| w.kind == WriteKind::Insert); let scope = self.push_scope(query, parent, target); - let mut level = - Level { scope, query, display: scope, display_query: query, init: Vec::new() }; + let no_targets = std::mem::take(&mut self.exists) && simple_exists(query); + let mut level = Level { + scope, + query, + display: scope, + display_query: query, + init: Vec::new(), + no_targets, + }; let depth = self.ctes.len(); if !std::ptr::eq(outer, query) { self.ctes_of(outer, &mut level); @@ -192,6 +216,7 @@ impl<'q> Builder<'q> { let name = format!("{prefix}_{}", self.counts[index]); let correlated = exprs::correlated(&link.query, 1); let scope = level.scope; + self.exists = link.kind == SubLinkKind::Exists; let mut plan = self.subplan(|b| b.query(&link.query, Some(scope))); let init = !correlated && !matches!(link.kind, SubLinkKind::Any | SubLinkKind::All); let hashed = !correlated && link.kind == SubLinkKind::Any && hashable_test(link); diff --git a/crates/rupg-opt/src/output.rs b/crates/rupg-opt/src/output.rs index 89b2869e..4dab0678 100644 --- a/crates/rupg-opt/src/output.rs +++ b/crates/rupg-opt/src/output.rs @@ -3,13 +3,17 @@ //! The width of a node is the width of the values that the nodes above it read, as the width of the `PathTarget` of its path. A scan below a join or an `Agg` gives all the columns of its relation, as a physical target list, but its width counts only the columns that the nodes above read. use rupg_analyze::{Expr, ExprKind, Var}; -use rupg_plan::{Item, Kind, Node}; +use rupg_plan::{Item, Kind, Node, Operation}; use crate::{Level, cost, exprs, from}; /// Sets the target lists and the widths of the nodes of the plan of a query level. pub(crate) fn level<'q>(level: &Level<'q>, node: &mut Node<'q>) { - let want = level.display_query.targets.iter().map(|t| Item::of(&t.expr)).collect(); + let want = if level.no_targets { + Vec::new() + } else { + level.display_query.targets.iter().map(|t| Item::of(&t.expr)).collect() + }; set(level, node, want, false); } @@ -52,8 +56,8 @@ fn set<'q>(level: &Level<'q>, node: &mut Node<'q>, want: Vec>, physical node.output = want.iter().map(Item::passed).collect(); node.cost.width = node.children.first().map_or(0, |c| c.cost.width); } - Kind::Modify { .. } => { - let want = level.query.targets.iter().map(|t| Item::of(&t.expr)).collect(); + Kind::Modify { operation, .. } => { + let want = modify_targets(level, *operation); if let Some(child) = node.children.first_mut() { set(level, child, want, false); } @@ -94,6 +98,7 @@ fn set<'q>(level: &Level<'q>, node: &mut Node<'q>, want: Vec>, physical let vars = keys.iter().all(is_var) && keys.iter().enumerate().all(|(i, k)| !keys[..i].iter().any(|o| o.same(k))); grouped(level, node, want, &keys, vars); + keys_below(node); } Kind::WindowAgg { window, .. } => { let clause = &level.query.windows[*window]; @@ -139,6 +144,13 @@ fn set<'q>(level: &Level<'q>, node: &mut Node<'q>, want: Vec>, physical | Kind::WorkTableScan(r) => { let r = *r; node.cost.width = width(level, node.scope, &want); + if let Some(output) = simple_values(level, node, r, &want) { + node.kind = Kind::Result { replaces: None, one_time: Vec::new() }; + node.cost.startup = 0.0; + node.cost.total = cost::CPU_TUPLE; + node.output = output; + return; + } let plain = want.iter().all(|w| matches!(w.expr.kind, ExprKind::Var(v) if v.attnum > 0)); node.output = if physical && plain { all_columns(level, r) } else { want }; @@ -152,6 +164,60 @@ fn set<'q>(level: &Level<'q>, node: &mut Node<'q>, want: Vec>, physical } } +/// The items that the rows below a `ModifyTable` give, as `preprocess_targetlist`. An `UPDATE` gives the new values of the columns of `SET` and the ID of the version of the row, and a `DELETE` gives only the ID. The node computes `RETURNING` from the old and the new rows. +fn modify_targets<'q>(level: &Level<'q>, operation: Operation) -> Vec> { + let query = level.query; + let all = || query.targets.iter().map(|t| Item::of(&t.expr)).collect(); + let (Some(write), Some(tid)) = (&query.write, query.targets.last()) else { return all() }; + match operation { + Operation::Update if write.width < query.targets.len() => { + let mut out: Vec> = query.targets[..write.width] + .iter() + .enumerate() + .filter(|(i, t)| { + !matches!(t.expr.kind, ExprKind::Var(v) if v.relation == 0 && v.levels_up == 0 && usize::try_from(v.attnum).ok() == Some(i + 1)) + }) + .map(|(_, t)| Item::of(&t.expr)) + .collect(); + out.push(Item::of(&tid.expr)); + out + } + Operation::Delete => vec![Item::of(&tid.expr)], + _ => all(), + } +} + +/// `pull_up_simple_values`: the expressions of the row of a `VALUES` with one row that is the only relation of a query, which a `Result` gives in place of the `Values Scan`. The items must be columns of the row. +fn simple_values<'q>( + level: &Level<'q>, + node: &Node<'q>, + r: usize, + want: &[Item<'q>], +) -> Option>> { + let query = level.query; + if !matches!(node.kind, Kind::ValuesScan(_)) + || node.scope != level.scope + || query.relations.len() != 1 + || query.write.is_some() + || !node.filter.is_empty() + { + return None; + } + let [row] = query.relations[r].values.as_deref()? else { return None }; + if row.iter().any(exprs::volatile) { + return None; + } + want.iter() + .map(|w| match w.expr.kind { + ExprKind::Var(v) if v.relation == r && v.levels_up == 0 => { + let column = usize::try_from(v.attnum).ok()?.checked_sub(1)?; + row.get(column).map(Item::of) + } + _ => None, + }) + .collect() +} + /// The columns of a relation of the query, as `build_physical_tlist`. fn all_columns<'q>(level: &Level<'q>, r: usize) -> Vec> { let relation = &level.query.relations[r]; @@ -228,6 +294,22 @@ fn grouped<'q>( node.output = output; } +/// The keys of an `Agg` show as the expressions of the node below it, as `show_sort_group_keys` finds them in its target list. A key is in parentheses only when that node reads it from a node below it. +fn keys_below(node: &mut Node<'_>) { + let Some(child) = node.children.first() else { return }; + let below = |item: &Item<'_>| { + child.output.iter().find(|o| o.expr.same(&item.expr)).map_or(item.below, |o| o.below) + }; + let Kind::Aggregate { keys, sets, .. } = &node.kind else { return }; + let flags: Vec = keys.iter().chain(sets.iter().flat_map(|(_, s)| s)).map(below).collect(); + if let Kind::Aggregate { keys, sets, .. } = &mut node.kind { + let items = keys.iter_mut().chain(sets.iter_mut().flat_map(|(_, s)| s)); + for (item, flag) in items.zip(flags) { + item.below = flag; + } + } +} + /// The target list of a join: the items that the parent reads. Each side gives the columns of its relations that the join and the nodes above it read, and the columns that a lateral reference of the other side reads. fn join<'q>(level: &Level<'q>, node: &mut Node<'q>, want: Vec>, parts: &[Expr]) { let mut needed: Vec> = Vec::new(); diff --git a/crates/rupg-opt/src/upper.rs b/crates/rupg-opt/src/upper.rs index 547120b1..e24bf56a 100644 --- a/crates/rupg-opt/src/upper.rs +++ b/crates/rupg-opt/src/upper.rs @@ -308,13 +308,10 @@ impl<'q> Builder<'q> { /// The plan of a set operation, as `plan_set_operations`. The nodes above it show the expressions of the leftmost `SELECT`. pub(crate) fn set_op(&mut self, level: &mut Level<'q>, op: &'q SetOp) -> Node<'q> { - let leftmost = op.left.leftmost(); let node = self.set_node(level, op); - if let Some(sub) = level.query.relations.get(leftmost).and_then(|r| r.subquery.as_deref()) - && let Some(scope) = self.scopes.iter().position(|s| std::ptr::eq(s.query, sub)) - { + if let Some(scope) = self.leftmost_scope(level, op) { level.display = scope; - level.display_query = sub; + level.display_query = self.scopes[scope].query; } node } @@ -398,6 +395,8 @@ impl<'q> Builder<'q> { let (sort_startup, sort_total) = cost::sort((c.startup, c.total), c.rows, None); let unique_total = sort_total + cost::CPU_OPERATOR * c.rows * columns as f64; let keys = self.leaf_items(level, op); + // The keys show with the names of the leftmost `SELECT`. + let scope = self.leftmost_scope(level, op).unwrap_or(scope); if hashable && hash_total <= unique_total { let mut node = Node::new( Kind::Aggregate { strategy: Strategy::Hashed, keys, sets: Vec::new() }, @@ -453,6 +452,12 @@ impl<'q> Builder<'q> { } } + /// The scope of the leftmost `SELECT` of a set operation. + fn leftmost_scope(&self, level: &Level<'q>, op: &'q SetOp) -> Option { + let sub = level.query.relations.get(op.left.leftmost())?.subquery.as_deref()?; + self.scopes.iter().position(|s| std::ptr::eq(s.query, sub)) + } + /// The columns of the leftmost `SELECT` of a set operation, as the keys of the node that finds the equal rows. fn leaf_items(&self, level: &Level<'q>, op: &'q SetOp) -> Vec> { let leftmost = op.left.leftmost(); @@ -598,8 +603,9 @@ fn grouping_sets<'q>( }; let c = input.cost; let n = c.rows; + // The sets with the most items come first, as `preprocess_grouping_sets` sorts them. let mut ordered: Vec<&Vec> = sets.iter().collect(); - ordered.reverse(); + ordered.sort_by_key(|s| std::cmp::Reverse(s.len())); let mut groups = 0.0; let mut units_all = 0.0; let mut out_sets = Vec::new(); @@ -713,11 +719,60 @@ fn startup_tuples(level: &Level<'_>, window: usize, rows: f64) -> f64 { cost::clamp_rows(out.min(partition_rows)) } -/// The `WindowAgg` nodes of the query, each with a `Sort` below it when the rows are not in the order of its window. The windows with the most keys come first, as `select_active_windows`. +/// `tleSortGroupRef`: the number of each target that an item of `ORDER BY`, `GROUP BY`, `DISTINCT` or a window names, in the order that the analyzer gives the numbers. +fn sort_refs(query: &Query) -> Vec { + let mut refs = vec![0; query.targets.len()]; + let mut next = 0; + let windows = query.windows.iter().flat_map(|w| w.partition.iter().chain(&w.order)); + for item in query.sort.iter().chain(&query.group).chain(&query.distinct).chain(windows) { + if let Some(slot) = refs.get_mut(item.target) + && *slot == 0 + { + next += 1; + *slot = next; + } + } + refs +} + +/// `select_active_windows`: the windows that a call uses, in the order of their `WindowAgg` nodes from the bottom up. `common_prefix_cmp` puts the items with the higher numbers first, so that the windows that share a prefix of their keys come next to each other, and a window with more keys comes before a window with a prefix of its keys. +fn active_windows(level: &Level<'_>, calls: &[Vec<&WindowFunc>]) -> Vec { + let query = level.query; + let refs = sort_refs(query); + let unique = |w: usize| { + let clause = &query.windows[w]; + let mut out: Vec<&SortGroup> = Vec::new(); + for item in clause.partition.iter().chain(&clause.order) { + if !out.contains(&item) { + out.push(item); + } + } + out + }; + let mut active: Vec<(usize, Vec<&SortGroup>)> = + (0..calls.len()).filter(|w| !calls[*w].is_empty()).map(|w| (w, unique(w))).collect(); + active.sort_by(|(_, a), (_, b)| { + for (x, y) in a.iter().zip(b) { + let rx = refs.get(x.target).copied().unwrap_or(0); + let ry = refs.get(y.target).copied().unwrap_or(0); + let found = ry + .cmp(&rx) + .then(y.operator.cmp(&x.operator)) + .then(y.nulls_first.cmp(&x.nulls_first)); + if found.is_ne() { + return found; + } + } + b.len().cmp(&a.len()) + }); + active.into_iter().map(|(w, _)| w).collect() +} + +/// The `WindowAgg` nodes of the query, each with a `Sort` below it when the rows are not in the order of its window, in the order of `select_active_windows`. A window with no name takes the name `w1`, `w2` and so on in that order, as `name_active_windows`, with a number that no named window of the query uses. fn windows<'q>(level: &Level<'q>, mut node: Node<'q>, order: &mut Vec) -> Node<'q> { let calls = window_calls(level.query); - let mut active: Vec = (0..calls.len()).filter(|w| !calls[*w].is_empty()).collect(); - active.sort_by_key(|w| std::cmp::Reverse(window_keys(level, *w).len())); + let active = active_windows(level, &calls); + let mut number = 0; for window in active { let clause = &level.query.windows[window]; node = sort_if(node, window_keys(level, window), order, None); @@ -749,7 +804,15 @@ fn windows<'q>(level: &Level<'q>, mut node: Node<'q>, order: &mut Vec) ) }) }); - let name = clause.name.clone().unwrap_or_else(|| format!("w{}", window + 1)); + let name = clause.name.clone().unwrap_or_else(|| { + loop { + number += 1; + let name = format!("w{number}"); + if !level.query.windows.iter().any(|w| w.name.as_deref() == Some(name.as_str())) { + break name; + } + } + }); node = Node::new(Kind::WindowAgg { name, window, rows_frame }, level.scope, vec![node]); node.cost = Cost { startup, total, ..c }; } @@ -775,6 +838,16 @@ fn distinct<'q>(level: &Level<'q>, input: Node<'q>, order: &mut Vec) - keys.push(key); } } + // `DISTINCT ON` sorts on all the items of `ORDER BY` when it has more items, as `create_final_distinct_paths`. + if query.distinct_on && query.sort.len() > keys.len() { + keys.clear(); + for s in &query.sort { + let key = sort_key(level.target(s.target), s); + if !keys.iter().any(|k| k.item.expr.same(&key.item.expr)) { + keys.push(key); + } + } + } let presorted = satisfies(order, &keys); let (sort_startup, sort_total) = if presorted { (c.startup, c.total) diff --git a/crates/rupg-plan/src/explain.rs b/crates/rupg-plan/src/explain.rs index 2d3c19db..01a6a5ac 100644 --- a/crates/rupg-plan/src/explain.rs +++ b/crates/rupg-plan/src/explain.rs @@ -262,8 +262,9 @@ impl Explainer<'_, '_> { if matches!( node.kind, Kind::NestedLoop { .. } | Kind::MergeJoin { .. } | Kind::HashJoin { .. } - ) && (!text || self.options.verbose) + ) && !text { + // The planner does not prove that the inner side is unique yet, and the text form shows only a true value. self.w.boolean("Inner Unique", false); } self.parts(node)?; diff --git a/crates/rupg-session/Cargo.toml b/crates/rupg-session/Cargo.toml index 3c1c98b9..31865028 100644 --- a/crates/rupg-session/Cargo.toml +++ b/crates/rupg-session/Cargo.toml @@ -19,7 +19,9 @@ rupg-catalog.workspace = true rupg-common.workspace = true rupg-exec.workspace = true rupg-func.workspace = true +rupg-opt.workspace = true rupg-pgcatalog.workspace = true +rupg-plan.workspace = true rupg-platform.workspace = true rupg-snowball.workspace = true rupg-sql.workspace = true diff --git a/crates/rupg-session/src/connection.rs b/crates/rupg-session/src/connection.rs index 232753d1..7b8bc7fb 100644 --- a/crates/rupg-session/src/connection.rs +++ b/crates/rupg-session/src/connection.rs @@ -37,6 +37,7 @@ mod copy_from; mod cursor; mod databases; mod deferred; +mod explain; mod extended; mod foreign; mod merge; @@ -1234,6 +1235,9 @@ impl Connection { } else if prepare::is_prepare(node) { drop(self.portals.close(b"")); self.run_prepare(node, text, &[], &mut notices, 0) + } else if let Node::ExplainStmt(stmt) = node { + drop(self.portals.close(b"")); + self.explain(stmt, &mut notices) } else if query::is_query(node) { drop(self.portals.close(b"")); self.select(node, &mut notices, &mut late, &mut described) @@ -2543,6 +2547,18 @@ mod tests { big_stack(|| script(include_str!("connection/prepared_statements.test"))); } + /// `EXPLAIN`: the options and their errors, the text, JSON, XML and YAML forms, the costs of the scans, the joins, the aggregates, the windows, the set operations, the sorts, `LIMIT`, the subplans and the init plans of subqueries, the statements that write a table, and `VERBOSE` with the outputs of the nodes. `connection/explain.test` is the output of PostgreSQL 19 for the same script in a new database. + #[test] + fn explain() { + big_stack(|| script(include_str!("connection/explain.test"))); + } + + /// `EXPLAIN` of the queries on a table with a primary key, whose plans do not use the index. `connection/explain_keys.test` is the output of PostgreSQL 19 for the same script in a new database. + #[test] + fn explain_keys() { + big_stack(|| script(include_str!("connection/explain_keys.test"))); + } + /// Cursors: `DECLARE` with each option, `FETCH` and `MOVE` in each direction on cursors that can and cannot scan backward, `CLOSE` and `CLOSE ALL`, the view `pg_cursors`, a cursor `WITH HOLD` after a commit and after a rollback, the errors of the statements and of the grammar, `Describe` of a statement of `FETCH` and of the portal of a cursor, and `Close` of the portal. `connection/cursors.test` is the output of PostgreSQL 19 for the same script in a new database, without the rows of a `BINARY` cursor and without a query that fails after the first row. #[test] fn cursors() { diff --git a/crates/rupg-session/src/connection/explain.rs b/crates/rupg-session/src/connection/explain.rs new file mode 100644 index 00000000..53a18de5 --- /dev/null +++ b/crates/rupg-session/src/connection/explain.rs @@ -0,0 +1,185 @@ +//! `EXPLAIN`, as `ExplainQuery` of `explain.c` runs it: the options, the plan of the query and the rows of its output. `EXPLAIN ANALYZE` is not supported yet. + +use rupg_analyze::Params; +use rupg_common::{Error, SqlState}; +use rupg_plan::{Format, Options}; +use rupg_sql::nodes::{DefElem, ExplainStmt, Node}; +use rupg_types::oid; +use rupg_wire::CommandTag; + +use super::copy::{def_boolean, def_string}; +use super::extended::protocol_error; +use super::{Connection, Done}; +use crate::query::{self, Column}; +use crate::utility::Notice; + +/// The options of `EXPLAIN`, as `ParseExplainOptionList` reads them. +#[derive(Debug, Default)] +struct Parsed { + analyze: bool, + verbose: bool, + costs: Option, + format: Format, + summary: Option, + memory: bool, + generic: bool, +} + +/// `ParseExplainOptionList`: the options of the statement and the checks of the options that need `ANALYZE`. +fn parse_options(stmt: &ExplainStmt) -> Result { + let mut parsed = Parsed::default(); + let mut wal = false; + let mut timing = None; + let mut serialize = false; + for def in stmt.options.iter().flatten() { + let Node::DefElem(def) = def else { continue }; + let name = def.defname.as_deref().unwrap_or_default(); + let at = usize::try_from(def.location).ok(); + match name { + "analyze" => parsed.analyze = def_boolean(def)?, + "verbose" => parsed.verbose = def_boolean(def)?, + "costs" => parsed.costs = Some(def_boolean(def)?), + "buffers" | "settings" => { + def_boolean(def)?; + } + "wal" => wal = def_boolean(def)?, + "generic_plan" => parsed.generic = def_boolean(def)?, + "timing" => timing = Some(def_boolean(def)?), + "summary" => parsed.summary = Some(def_boolean(def)?), + "memory" => parsed.memory = def_boolean(def)?, + "serialize" => { + serialize = match def.arg { + None => true, + Some(_) => match def_string(def)?.as_str() { + "off" | "none" => false, + "text" | "binary" => true, + other => return Err(bad_value(def, other).with_position(at)), + }, + } + } + "format" => { + parsed.format = match def_string(def)?.as_str() { + "text" => Format::Text, + "xml" => Format::Xml, + "json" => Format::Json, + "yaml" => Format::Yaml, + other => return Err(bad_value(def, other).with_position(at)), + } + } + _ => { + return Err(Error::new( + SqlState::SYNTAX_ERROR, + format!("unrecognized EXPLAIN option \"{name}\""), + ) + .with_position(at)); + } + } + } + let needs = |what: &str| { + Error::new( + SqlState::INVALID_PARAMETER_VALUE, + format!("EXPLAIN option {what} requires ANALYZE"), + ) + }; + if wal && !parsed.analyze { + return Err(needs("WAL")); + } + if timing.unwrap_or(parsed.analyze) && !parsed.analyze { + return Err(needs("TIMING")); + } + if serialize && !parsed.analyze { + return Err(needs("SERIALIZE")); + } + if parsed.generic && parsed.analyze { + return Err(Error::new( + SqlState::INVALID_PARAMETER_VALUE, + "EXPLAIN options ANALYZE and GENERIC_PLAN cannot be used together", + )); + } + Ok(parsed) +} + +/// The error of an option of `EXPLAIN` with a value that it does not take. +fn bad_value(def: &DefElem, value: &str) -> Error { + let name = def.defname.as_deref().unwrap_or_default(); + Error::new( + SqlState::INVALID_PARAMETER_VALUE, + format!("unrecognized value for EXPLAIN option \"{name}\": \"{value}\""), + ) +} + +/// The error of a feature of `EXPLAIN` that the engine does not have yet. +fn not_yet(what: &str) -> Error { + Error::new(SqlState::FEATURE_NOT_SUPPORTED, format!("{what} is not supported yet")) +} + +impl Connection { + /// `ExplainQuery`: the plan of the query of the statement. The text form gives a row for each line, and the other forms give one row with the whole value. + pub(super) fn explain( + &self, + stmt: &ExplainStmt, + notices: &mut Vec, + ) -> Result { + let parsed = parse_options(stmt)?; + if parsed.analyze { + return Err(not_yet("EXPLAIN ANALYZE")); + } + if parsed.generic { + return Err(not_yet("EXPLAIN (GENERIC_PLAN)")); + } + if parsed.memory { + return Err(not_yet("EXPLAIN (MEMORY)")); + } + let Some(node) = &stmt.query else { + return Err(Error::internal("EXPLAIN with no statement")); + }; + // `ExplainOneUtility`: `CREATE TABLE AS` and `DECLARE CURSOR` show the plan of their query. + let inner = match node { + Node::CreateTableAsStmt(s) => s.query.as_ref(), + Node::DeclareCursorStmt(s) => s.query.as_ref(), + Node::ExecuteStmt(s) => { + let name = s.name.as_deref().unwrap_or(""); + self.statements.get(name.as_bytes()).map_err(protocol_error)?; + return Err(not_yet("EXPLAIN EXECUTE")); + } + _ => Some(node), + }; + let Some(inner) = inner.filter(|n| query::is_query(n)) else { + return Err(not_yet("EXPLAIN of this statement")); + }; + let started = self.clock.monotonic(); + self.settings.borrow_mut().take_snapshot(); + let reader = self.reader(); + let plan = query::plan(inner, &reader, &Params::default()); + notices.extend(reader.take_warnings().into_iter().map(Notice::of_function)); + let plan = plan?; + notices.extend(plan.notices().iter().map(|error| Notice::warning(error.clone()))); + let plan = plan.fold(None, &reader); + notices.extend(reader.take_warnings().into_iter().map(Notice::of_function)); + let plan = plan?; + let tree = rupg_opt::plan(plan.query()); + let planning_time = parsed.summary.unwrap_or(false).then(|| { + let spent = self.clock.monotonic().saturating_sub(started); + spent.as_secs_f64() * 1000.0 + }); + let options = Options { + verbose: parsed.verbose, + costs: parsed.costs.unwrap_or(true), + format: parsed.format, + planning_time, + }; + let text = rupg_func::explain(&tree, &options, &reader)?; + let ty = match parsed.format { + Format::Json => oid::JSON, + Format::Xml => oid::XML, + Format::Text | Format::Yaml => oid::TEXT, + }; + let columns = vec![Column { ty, ..Column::text("QUERY PLAN".to_string()) }]; + let rows = if parsed.format == Format::Text { + text.split('\n').map(|line| vec![Some(line.as_bytes().to_vec())]).collect() + } else { + vec![vec![Some(text.into_bytes())]] + }; + Ok(Done::Rows { columns, rows, tag: CommandTag::Explain }) + } +} diff --git a/crates/rupg-session/src/connection/explain.test b/crates/rupg-session/src/connection/explain.test new file mode 100644 index 00000000..d589bd5f --- /dev/null +++ b/crates/rupg-session/src/connection/explain.test @@ -0,0 +1,1181 @@ +!details +> SET TimeZone = 'UTC' +SET +> CREATE TABLE t (a int, b text, c int NOT NULL); +CREATE TABLE +> CREATE TABLE u (a int, d text); +CREATE TABLE +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE a NOT IN (SELECT a FROM u); +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row Filter: (NOT (ANY (a = (hashed SubPlan any_1).col1))) +row SubPlan any_1 +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE a = 1 OR EXISTS (SELECT 1 FROM u); +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row Filter: ((a = 1) OR (InitPlan exists_1).col1) +row InitPlan exists_1 +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT (SELECT d FROM u LIMIT 1), a FROM t; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row InitPlan expr_1 +row -> Limit +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE a = ANY (ARRAY(SELECT a FROM u)); +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row Filter: (a = ANY ((InitPlan array_1).col1)) +row InitPlan array_1 +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t t1 JOIN t t2 ON t1.a = t2.c; +fields QUERY PLAN:25:-1:-1 +row Merge Join +row Merge Cond: (t1.a = t2.c) +row -> Sort +row Sort Key: t1.a +row -> Seq Scan on t t1 +row -> Sort +row Sort Key: t2.c +row -> Seq Scan on t t2 +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t CROSS JOIN u; +fields QUERY PLAN:25:-1:-1 +row Nested Loop +row -> Seq Scan on u +row -> Materialize +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t LEFT JOIN u ON t.a = u.a; +fields QUERY PLAN:25:-1:-1 +row Merge Left Join +row Merge Cond: (t.a = u.a) +row -> Sort +row Sort Key: t.a +row -> Seq Scan on t +row -> Sort +row Sort Key: u.a +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t RIGHT JOIN u ON t.a = u.a AND t.b = 'x'; +fields QUERY PLAN:25:-1:-1 +row Hash Left Join +row Hash Cond: (u.a = t.a) +row -> Seq Scan on u +row -> Hash +row -> Seq Scan on t +row Filter: (b = 'x'::text) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t JOIN u ON t.a < u.a; +fields QUERY PLAN:25:-1:-1 +row Nested Loop +row Join Filter: (t.a < u.a) +row -> Seq Scan on u +row -> Materialize +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t LEFT JOIN u ON t.a < u.a; +fields QUERY PLAN:25:-1:-1 +row Nested Loop Left Join +row Join Filter: (t.a < u.a) +row -> Seq Scan on t +row -> Materialize +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t JOIN u ON t.a = u.a WHERE t.b || u.d = 'x'; +fields QUERY PLAN:25:-1:-1 +row Merge Join +row Merge Cond: (t.a = u.a) +row Join Filter: ((t.b || u.d) = 'x'::text) +row -> Sort +row Sort Key: t.a +row -> Seq Scan on t +row -> Sort +row Sort Key: u.a +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM (SELECT a FROM t LIMIT 3) s; +fields QUERY PLAN:25:-1:-1 +row Limit +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t LIMIT 3 OFFSET 2; +fields QUERY PLAN:25:-1:-1 +row Limit +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT DISTINCT ON (a) a, b FROM t ORDER BY a, b; +fields QUERY PLAN:25:-1:-1 +row Unique +row -> Sort +row Sort Key: a, b +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a, count(*) FROM t GROUP BY a HAVING count(*) > 1 ORDER BY a; +fields QUERY PLAN:25:-1:-1 +row Sort +row Sort Key: a +row -> HashAggregate +row Group Key: a +row Filter: (count(*) > 1) +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a FROM t INTERSECT SELECT a FROM u; +fields QUERY PLAN:25:-1:-1 +row HashSetOp Intersect +row -> Seq Scan on t +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a FROM t EXCEPT ALL SELECT a FROM u; +fields QUERY PLAN:25:-1:-1 +row HashSetOp Except All +row -> Seq Scan on t +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a, b, count(*) FROM t GROUP BY GROUPING SETS ((a), (b), ()); +fields QUERY PLAN:25:-1:-1 +row MixedAggregate +row Hash Key: a +row Hash Key: b +row Group Key: () +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a, b, count(*) FROM t GROUP BY ROLLUP (a, b); +fields QUERY PLAN:25:-1:-1 +row MixedAggregate +row Hash Key: a, b +row Hash Key: a +row Group Key: () +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) WITH RECURSIVE r(n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM r WHERE n < 5) SELECT * FROM r; +fields QUERY PLAN:25:-1:-1 +row CTE Scan on r +row CTE r +row -> Recursive Union +row -> Result +row -> WorkTable Scan on r r_1 +row Filter: (n < 5) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a, sum(c) OVER (PARTITION BY b ORDER BY a), rank() OVER (ORDER BY c) FROM t; +fields QUERY PLAN:25:-1:-1 +row WindowAgg +row Window: w2 AS (PARTITION BY b ORDER BY a) +row -> Sort +row Sort Key: b, a +row -> WindowAgg +row Window: w1 AS (ORDER BY c ROWS UNBOUNDED PRECEDING) +row -> Sort +row Sort Key: c +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t, LATERAL generate_series(1, t.a) g; +fields QUERY PLAN:25:-1:-1 +row Nested Loop +row -> Seq Scan on t +row -> Function Scan on generate_series g +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t FOR UPDATE; +fields QUERY PLAN:25:-1:-1 +row LockRows +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) VALUES (1, 'a'), (2, 'b') ORDER BY 1; +fields QUERY PLAN:25:-1:-1 +row Sort +row Sort Key: column1 +row -> Values Scan on "*VALUES*" +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT 1 WHERE false; +fields QUERY PLAN:25:-1:-1 +row Result +row One-Time Filter: false +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE false; +fields QUERY PLAN:25:-1:-1 +row Result +row Replaces: Scan on t +row One-Time Filter: false +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT now() WHERE random() > 0.5; +fields QUERY PLAN:25:-1:-1 +row Result +row One-Time Filter: (random() > '0.5'::double precision) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a, count(DISTINCT b) FROM t GROUP BY a; +fields QUERY PLAN:25:-1:-1 +row GroupAggregate +row Group Key: a +row -> Sort +row Sort Key: a, b +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT sum(a) FILTER (WHERE c > 1), string_agg(b, ',' ORDER BY b) FROM t; +fields QUERY PLAN:25:-1:-1 +row Aggregate +row -> Sort +row Sort Key: b +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE c > 1 ORDER BY b DESC NULLS LAST, a USING >; +fields QUERY PLAN:25:-1:-1 +row Sort +row Sort Key: b DESC NULLS LAST, a DESC +row -> Seq Scan on t +row Filter: (c > 1) +EXPLAIN +> EXPLAIN (COSTS OFF) INSERT INTO t SELECT * FROM t WHERE a > 1; +fields QUERY PLAN:25:-1:-1 +row Insert on t +row -> Seq Scan on t t_1 +row Filter: (a > 1) +EXPLAIN +> EXPLAIN (COSTS OFF) INSERT INTO t VALUES (1, 'x', 1), (2, 'y', 2); +fields QUERY PLAN:25:-1:-1 +row Insert on t +row -> Values Scan on "*VALUES*" +EXPLAIN +> EXPLAIN (COSTS OFF) INSERT INTO t VALUES (1, 'x', 1) RETURNING a; +fields QUERY PLAN:25:-1:-1 +row Insert on t +row -> Result +EXPLAIN +> EXPLAIN (COSTS OFF) DELETE FROM t USING u WHERE t.a = u.a; +fields QUERY PLAN:25:-1:-1 +row Delete on t +row -> Merge Join +row Merge Cond: (t.a = u.a) +row -> Sort +row Sort Key: t.a +row -> Seq Scan on t +row -> Sort +row Sort Key: u.a +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT t1.a, t2.b FROM t t1 JOIN t t2 ON t1.a = t2.c ORDER BY 1; +fields QUERY PLAN:25:-1:-1 +row Merge Join +row Output: t1.a, t2.b +row Merge Cond: (t1.a = t2.c) +row -> Sort +row Output: t1.a +row Sort Key: t1.a +row -> Seq Scan on public.t t1 +row Output: t1.a +row -> Sort +row Output: t2.b, t2.c +row Sort Key: t2.c +row -> Seq Scan on public.t t2 +row Output: t2.b, t2.c +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT a, count(*) FROM t GROUP BY a; +fields QUERY PLAN:25:-1:-1 +row HashAggregate +row Output: a, count(*) +row Group Key: t.a +row -> Seq Scan on public.t +row Output: a, b, c +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM generate_series(1, 3) g(x) WHERE x > 1; +fields QUERY PLAN:25:-1:-1 +row Function Scan on pg_catalog.generate_series g +row Output: x +row Function Call: generate_series(1, 3) +row Filter: (g.x > 1) +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) VALUES (1, 'a'); +fields QUERY PLAN:25:-1:-1 +row Result +row Output: 1, 'a'::text +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT 1 AS x, 'a' AS y; +fields QUERY PLAN:25:-1:-1 +row Result +row Output: 1, 'a'::text +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) UPDATE t SET a = 2 WHERE c = 3; +fields QUERY PLAN:25:-1:-1 +row Update on public.t +row -> Seq Scan on public.t +row Output: 2, ctid +row Filter: (t.c = 3) +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) INSERT INTO t VALUES (1, 'x', 2); +fields QUERY PLAN:25:-1:-1 +row Insert on public.t +row -> Result +row Output: 1, 'x'::text, 2 +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT a FROM t UNION ALL SELECT a FROM u; +fields QUERY PLAN:25:-1:-1 +row Append +row -> Seq Scan on public.t +row Output: t.a +row -> Seq Scan on public.u +row Output: u.a +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT a, row_number() OVER (ORDER BY c) FROM t; +fields QUERY PLAN:25:-1:-1 +row WindowAgg +row Output: a, row_number() OVER w1, c +row Window: w1 AS (ORDER BY t.c ROWS UNBOUNDED PRECEDING) +row -> Sort +row Output: c, a +row Sort Key: t.c +row -> Seq Scan on public.t +row Output: c, a +EXPLAIN +> EXPLAIN SELECT * FROM t JOIN u ON t.a = u.a; +fields QUERY PLAN:25:-1:-1 +row Merge Join (cost=171.55..291.85 rows=7620 width=76) +row Merge Cond: (t.a = u.a) +row -> Sort (cost=83.37..86.37 rows=1200 width=40) +row Sort Key: t.a +row -> Seq Scan on t (cost=0.00..22.00 rows=1200 width=40) +row -> Sort (cost=88.17..91.35 rows=1270 width=36) +row Sort Key: u.a +row -> Seq Scan on u (cost=0.00..22.70 rows=1270 width=36) +EXPLAIN +> EXPLAIN SELECT count(*) FROM t WHERE a = 1; +fields QUERY PLAN:25:-1:-1 +row Aggregate (cost=25.02..25.03 rows=1 width=8) +row -> Seq Scan on t (cost=0.00..25.00 rows=6 width=0) +row Filter: (a = 1) +EXPLAIN +> EXPLAIN SELECT a FROM t ORDER BY a LIMIT 3; +fields QUERY PLAN:25:-1:-1 +row Limit (cost=37.51..37.52 rows=3 width=4) +row -> Sort (cost=37.51..40.51 rows=1200 width=4) +row Sort Key: a +row -> Seq Scan on t (cost=0.00..22.00 rows=1200 width=4) +EXPLAIN +> EXPLAIN (FORMAT JSON) SELECT a, count(*) FROM t GROUP BY a ORDER BY 1; +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "Sort", + "Parallel Aware": false, + "Async Capable": false, + "Startup Cost": 37.64, + "Total Cost": 38.14, + "Plan Rows": 200, + "Plan Width": 12, + "Disabled": false, + "Sort Key": ["a"], + "Plans": [ + { + "Node Type": "Aggregate", + "Strategy": "Hashed", + "Partial Mode": "Simple", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Startup Cost": 28.00, + "Total Cost": 30.00, + "Plan Rows": 200, + "Plan Width": 12, + "Disabled": false, + "Group Key": ["a"], + "Planned Partitions": 0, + "Plans": [ + { + "Node Type": "Seq Scan", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Startup Cost": 0.00, + "Total Cost": 22.00, + "Plan Rows": 1200, + "Plan Width": 4, + "Disabled": false + } + ] + } + ] + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT YAML) SELECT * FROM t JOIN u ON t.a = u.a WHERE t.c > 1; +fields QUERY PLAN:25:-1:-1 +row - Plan: + Node Type: "Hash Join" + Parallel Aware: false + Async Capable: false + Join Type: "Inner" + Disabled: false + Inner Unique: false + Hash Cond: "(u.a = t.a)" + Plans: + - Node Type: "Seq Scan" + Parent Relationship: "Outer" + Parallel Aware: false + Async Capable: false + Relation Name: "u" + Alias: "u" + Disabled: false + - Node Type: "Hash" + Parent Relationship: "Inner" + Parallel Aware: false + Async Capable: false + Disabled: false + Plans: + - Node Type: "Seq Scan" + Parent Relationship: "Outer" + Parallel Aware: false + Async Capable: false + Relation Name: "t" + Alias: "t" + Disabled: false + Filter: "(c > 1)" +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT XML, VERBOSE) SELECT * FROM t JOIN u ON t.a = u.a; +fields QUERY PLAN:142:-1:-1 +row + + + Merge Join + false + false + Inner + false + + t.a + t.b + t.c + u.a + u.d + + false + (t.a = u.a) + + + Sort + Outer + false + false + false + + t.a + t.b + t.c + + + t.a + + + + Seq Scan + Outer + false + false + t + public + t + false + + t.a + t.b + t.c + + + + + + Sort + Inner + false + false + false + + u.a + u.d + + + u.a + + + + Seq Scan + Outer + false + false + u + public + u + false + + u.a + u.d + + + + + + + + +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) SELECT * FROM t WHERE a = (SELECT max(a) FROM u); +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "Seq Scan", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false, + "Filter": "(a = (InitPlan expr_1).col1)", + "Plans": [ + { + "Node Type": "Aggregate", + "Strategy": "Plain", + "Partial Mode": "Simple", + "Parent Relationship": "InitPlan", + "Subplan Name": "InitPlan expr_1", + "Parallel Aware": false, + "Async Capable": false, + "Disabled": false, + "Plans": [ + { + "Node Type": "Seq Scan", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "u", + "Alias": "u", + "Disabled": false + } + ] + } + ] + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) SELECT a FROM t UNION SELECT a FROM u ORDER BY 1 LIMIT 2; +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "Limit", + "Parallel Aware": false, + "Async Capable": false, + "Disabled": false, + "Plans": [ + { + "Node Type": "Sort", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Disabled": false, + "Sort Key": ["t.a"], + "Plans": [ + { + "Node Type": "Aggregate", + "Strategy": "Hashed", + "Partial Mode": "Simple", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Disabled": false, + "Group Key": ["t.a"], + "Plans": [ + { + "Node Type": "Append", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Disabled": false, + "Subplans Removed": 0, + "Plans": [ + { + "Node Type": "Seq Scan", + "Parent Relationship": "Member", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false + }, + { + "Node Type": "Seq Scan", + "Parent Relationship": "Member", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "u", + "Alias": "u", + "Disabled": false + } + ] + } + ] + } + ] + } + ] + } + } +] +EXPLAIN +> EXPLAIN (FOO) SELECT 1; +ERROR 42601 unrecognized EXPLAIN option "foo" at 10 +> EXPLAIN (COSTS maybe) SELECT 1; +ERROR 42601 costs requires a Boolean value +> EXPLAIN (FORMAT csv) SELECT 1; +ERROR 22023 unrecognized value for EXPLAIN option "format": "csv" at 10 +> EXPLAIN (WAL) SELECT 1; +ERROR 22023 EXPLAIN option WAL requires ANALYZE +> EXPLAIN (TIMING) SELECT 1; +ERROR 22023 EXPLAIN option TIMING requires ANALYZE +> EXPLAIN (SERIALIZE) SELECT 1; +ERROR 22023 EXPLAIN option SERIALIZE requires ANALYZE +> EXPLAIN (ANALYZE, GENERIC_PLAN) SELECT 1; +ERROR 22023 EXPLAIN options ANALYZE and GENERIC_PLAN cannot be used together +> EXPLAIN (COSTS, COSTS OFF) SELECT 1; +fields QUERY PLAN:25:-1:-1 +row Result +EXPLAIN +> EXPLAIN SELECT * FROM t WHERE a = $1; +ERROR 42P02 there is no parameter $1 at 35 +> EXPLAIN (COSTS OFF, BUFFERS) SELECT 1; +fields QUERY PLAN:25:-1:-1 +row Result +EXPLAIN +> EXPLAIN (COSTS false, VERBOSE 1, FORMAT text) SELECT 1; +fields QUERY PLAN:25:-1:-1 +row Result +row Output: 1 +EXPLAIN +> EXPLAIN VERBOSE SELECT 1; +fields QUERY PLAN:25:-1:-1 +row Result (cost=0.00..0.01 rows=1 width=4) +row Output: 1 +EXPLAIN +> EXPLAIN CREATE TABLE z (a int); +ERROR 42601 syntax error at or near "int" at 27 +> EXPLAIN (COSTS OFF) CREATE TABLE z AS SELECT 1; +fields QUERY PLAN:25:-1:-1 +row Result +EXPLAIN +> EXPLAIN (COSTS OFF) DECLARE c CURSOR FOR SELECT 1; +fields QUERY PLAN:25:-1:-1 +row Result +EXPLAIN +> EXPLAIN (COSTS OFF) EXECUTE p; +ERROR 26000 prepared statement "p" does not exist +> EXPLAIN (COSTS OFF) SELECT * FROM nosuch; +ERROR 42P01 relation "nosuch" does not exist at 35 +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE a = 1 OR EXISTS (SELECT 1 FROM u WHERE u.a > t.c); +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row Filter: ((a = 1) OR EXISTS(SubPlan exists_1)) +row SubPlan exists_1 +row -> Seq Scan on u +row Filter: (a > t.c) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE a = 1 OR a IN (SELECT a FROM u WHERE u.d = t.b); +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row Filter: ((a = 1) OR (ANY (a = (SubPlan any_1).col1))) +row SubPlan any_1 +row -> Seq Scan on u +row Filter: (d = t.b) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE a > ALL (SELECT a FROM u); +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row Filter: (ALL (a > (SubPlan all_1).col1)) +row SubPlan all_1 +row -> Materialize +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT ARRAY(SELECT a FROM u WHERE u.d = t.b) FROM t; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row SubPlan array_1 +row -> Seq Scan on u +row Filter: (d = t.b) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT (SELECT max(d) FROM u WHERE u.a = t.a) FROM t; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row SubPlan expr_1 +row -> Aggregate +row -> Seq Scan on u +row Filter: (a = t.a) +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT (SELECT max(d) FROM u WHERE u.a = t.a) FROM t; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on public.t +row Output: (SubPlan expr_1) +row SubPlan expr_1 +row -> Aggregate +row Output: max(u.d) +row -> Seq Scan on public.u +row Output: u.a, u.d +row Filter: (u.a = t.a) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE c > (SELECT count(*) FROM u); +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row Filter: (c > (InitPlan expr_1).col1) +row InitPlan expr_1 +row -> Aggregate +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) WITH w AS (SELECT * FROM t) SELECT * FROM w a, w b; +fields QUERY PLAN:25:-1:-1 +row Nested Loop +row CTE w +row -> Seq Scan on t +row -> CTE Scan on w a +row -> CTE Scan on w b +EXPLAIN +> EXPLAIN (COSTS OFF) WITH w AS (SELECT * FROM t) SELECT * FROM w; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t TABLESAMPLE system (10); +fields QUERY PLAN:25:-1:-1 +row Sample Scan on t +row Sampling: system ('10'::real) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE a > 1 AND random() > 0.5 AND b LIKE 'x%'; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row Filter: ((a > 1) AND (b ~~ 'x%'::text) AND (random() > '0.5'::double precision)) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t JOIN u ON true; +fields QUERY PLAN:25:-1:-1 +row Nested Loop +row -> Seq Scan on u +row -> Materialize +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM rows from (generate_series(1, 2), generate_series(1, 3)) WITH ORDINALITY; +fields QUERY PLAN:25:-1:-1 +row Function Scan on generate_series +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM json_table('[1]', '$[*]' COLUMNS (x int PATH '$')); +fields QUERY PLAN:25:-1:-1 +row Table Function Scan on "json_table" +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT DISTINCT b FROM t ORDER BY b; +fields QUERY PLAN:25:-1:-1 +row Sort +row Sort Key: b +row -> HashAggregate +row Group Key: b +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a FROM t ORDER BY a FETCH FIRST 2 ROWS WITH TIES; +fields QUERY PLAN:25:-1:-1 +row Limit +row -> Sort +row Sort Key: a +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a FROM t UNION SELECT a FROM u UNION SELECT c FROM t; +fields QUERY PLAN:25:-1:-1 +row HashAggregate +row Group Key: t.a +row -> Append +row -> Seq Scan on t +row -> Seq Scan on u +row -> Seq Scan on t t_1 +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT 1 UNION ALL SELECT 2; +fields QUERY PLAN:25:-1:-1 +row Append +row -> Result +row -> Result +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE c = 1 FOR SHARE LIMIT 1; +fields QUERY PLAN:25:-1:-1 +row Limit +row -> LockRows +row -> Seq Scan on t +row Filter: (c = 1) +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM t WHERE a = 1 OR EXISTS (SELECT 1 FROM u WHERE u.a > t.c); +fields QUERY PLAN:25:-1:-1 +row Seq Scan on public.t +row Output: t.a, t.b, t.c +row Filter: ((t.a = 1) OR EXISTS(SubPlan exists_1)) +row SubPlan exists_1 +row -> Seq Scan on public.u +row Filter: (u.a > t.c) +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT count(*) FROM t; +fields QUERY PLAN:25:-1:-1 +row Aggregate +row Output: count(*) +row -> Seq Scan on public.t +row Output: a, b, c +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM t ORDER BY a DESC LIMIT 5; +fields QUERY PLAN:25:-1:-1 +row Limit +row Output: a, b, c +row -> Sort +row Output: a, b, c +row Sort Key: t.a DESC +row -> Seq Scan on public.t +row Output: a, b, c +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT DISTINCT b FROM t; +fields QUERY PLAN:25:-1:-1 +row HashAggregate +row Output: b +row Group Key: t.b +row -> Seq Scan on public.t +row Output: a, b, c +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM t LEFT JOIN u ON t.c = u.a + 1 WHERE t.b IS NULL; +fields QUERY PLAN:25:-1:-1 +row Hash Right Join +row Output: t.a, t.b, t.c, u.a, u.d +row Hash Cond: ((u.a + 1) = t.c) +row -> Seq Scan on public.u +row Output: u.a, u.d +row -> Hash +row Output: t.a, t.b, t.c +row -> Seq Scan on public.t +row Output: t.a, t.b, t.c +row Filter: (t.b IS NULL) +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) INSERT INTO t VALUES (1, 'x', 1); +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "ModifyTable", + "Operation": "Insert", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false, + "Plans": [ + { + "Node Type": "Result", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Disabled": false + } + ] + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) WITH w AS MATERIALIZED (SELECT * FROM t) SELECT * FROM w; +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "CTE Scan", + "Parallel Aware": false, + "Async Capable": false, + "CTE Name": "w", + "Alias": "w", + "Disabled": false, + "Plans": [ + { + "Node Type": "Seq Scan", + "Parent Relationship": "InitPlan", + "Subplan Name": "CTE w", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false + } + ] + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON, VERBOSE) SELECT * FROM generate_series(1, 3) g(x); +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "Function Scan", + "Parallel Aware": false, + "Async Capable": false, + "Function Name": "generate_series", + "Schema": "pg_catalog", + "Alias": "g", + "Disabled": false, + "Output": ["x"], + "Function Call": "generate_series(1, 3)" + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT YAML) SELECT a FROM t UNION ALL SELECT a FROM u; +fields QUERY PLAN:25:-1:-1 +row - Plan: + Node Type: "Append" + Parallel Aware: false + Async Capable: false + Disabled: false + Subplans Removed: 0 + Plans: + - Node Type: "Seq Scan" + Parent Relationship: "Member" + Parallel Aware: false + Async Capable: false + Relation Name: "t" + Alias: "t" + Disabled: false + - Node Type: "Seq Scan" + Parent Relationship: "Member" + Parallel Aware: false + Async Capable: false + Relation Name: "u" + Alias: "u" + Disabled: false +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT XML) SELECT a FROM t ORDER BY a; +fields QUERY PLAN:142:-1:-1 +row + + + Sort + false + false + false + + a + + + + Seq Scan + Outer + false + false + t + t + false + + + + + +EXPLAIN +> EXPLAIN (FORMAT YAML) SELECT a FROM t ORDER BY a; +fields QUERY PLAN:25:-1:-1 +row - Plan: + Node Type: "Sort" + Parallel Aware: false + Async Capable: false + Startup Cost: 83.37 + Total Cost: 86.37 + Plan Rows: 1200 + Plan Width: 4 + Disabled: false + Sort Key: + - "a" + Plans: + - Node Type: "Seq Scan" + Parent Relationship: "Outer" + Parallel Aware: false + Async Capable: false + Relation Name: "t" + Alias: "t" + Startup Cost: 0.00 + Total Cost: 22.00 + Plan Rows: 1200 + Plan Width: 4 + Disabled: false +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) SELECT a, row_number() OVER (ORDER BY c) FROM t; +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "WindowAgg", + "Parallel Aware": false, + "Async Capable": false, + "Disabled": false, + "Window": "w1 AS (ORDER BY c ROWS UNBOUNDED PRECEDING)", + "Plans": [ + { + "Node Type": "Sort", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Disabled": false, + "Sort Key": ["c"], + "Plans": [ + { + "Node Type": "Seq Scan", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false + } + ] + } + ] + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) SELECT a FROM t INTERSECT SELECT a FROM u; +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "SetOp", + "Strategy": "Hashed", + "Parallel Aware": false, + "Async Capable": false, + "Command": "Intersect", + "Disabled": false, + "Plans": [ + { + "Node Type": "Seq Scan", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false + }, + { + "Node Type": "Seq Scan", + "Parent Relationship": "Inner", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "u", + "Alias": "u", + "Disabled": false + } + ] + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) SELECT * FROM t JOIN u ON t.a < u.a; +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "Nested Loop", + "Parallel Aware": false, + "Async Capable": false, + "Join Type": "Inner", + "Disabled": false, + "Inner Unique": false, + "Join Filter": "(t.a < u.a)", + "Plans": [ + { + "Node Type": "Seq Scan", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "u", + "Alias": "u", + "Disabled": false + }, + { + "Node Type": "Materialize", + "Parent Relationship": "Inner", + "Parallel Aware": false, + "Async Capable": false, + "Disabled": false, + "Plans": [ + { + "Node Type": "Seq Scan", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false + } + ] + } + ] + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE a = (SELECT max(a) + 0 FROM u WHERE u.d = (SELECT max(b) FROM t)) AND c = (SELECT 1 + 0); +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row Filter: ((a = (InitPlan expr_1).col1) AND (c = (InitPlan expr_3).col1)) +row InitPlan expr_1 +row -> Aggregate +row InitPlan expr_2 +row -> Aggregate +row -> Seq Scan on t t_1 +row -> Seq Scan on u +row Filter: (d = (InitPlan expr_2).col1) +row InitPlan expr_3 +row -> Result +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT (SELECT 1 + 0), (SELECT a FROM u WHERE u.d = (SELECT b FROM t LIMIT 1) LIMIT 1) FROM t; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row InitPlan expr_1 +row -> Result +row InitPlan expr_2 +row -> Limit +row InitPlan expr_3 +row -> Limit +row -> Seq Scan on t t_1 +row -> Seq Scan on u +row Filter: (d = (InitPlan expr_3).col1) +EXPLAIN +> EXPLAIN (COSTS OFF) WITH w AS MATERIALIZED (SELECT a FROM t WHERE c = (SELECT 2 + 0)) SELECT * FROM w WHERE a = (SELECT 3 + 0); +fields QUERY PLAN:25:-1:-1 +row CTE Scan on w +row Filter: (a = (InitPlan expr_2).col1) +row CTE w +row -> Seq Scan on t +row Filter: (c = (InitPlan expr_1).col1) +row InitPlan expr_1 +row -> Result +row InitPlan expr_2 +row -> Result +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM (SELECT a FROM t WHERE c = (SELECT 4 + 0) LIMIT 2) s, u WHERE u.a = (SELECT 5 + 0); +fields QUERY PLAN:25:-1:-1 +row Nested Loop +row InitPlan expr_1 +row -> Result +row -> Seq Scan on u +row Filter: (a = (InitPlan expr_1).col1) +row -> Materialize +row -> Limit +row InitPlan expr_2 +row -> Result +row -> Seq Scan on t +row Filter: (c = (InitPlan expr_2).col1) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t x WHERE x.a IN (SELECT 1 + 0) OR x.c = (SELECT 2 + 0); +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t x +row Filter: ((ANY (a = (hashed SubPlan any_1).col1)) OR (c = (InitPlan expr_1).col1)) +row InitPlan expr_1 +row -> Result +row SubPlan any_1 +row -> Result +EXPLAIN diff --git a/crates/rupg-session/src/connection/explain_keys.test b/crates/rupg-session/src/connection/explain_keys.test new file mode 100644 index 00000000..fbfb80c3 --- /dev/null +++ b/crates/rupg-session/src/connection/explain_keys.test @@ -0,0 +1,738 @@ +!details +> SET TimeZone = 'UTC' +SET +> CREATE TABLE t (a int, b text, c int NOT NULL); +CREATE TABLE +> CREATE TABLE u (a int PRIMARY KEY, d text); +CREATE TABLE +> EXPLAIN (COSTS OFF) SELECT 1; +fields QUERY PLAN:25:-1:-1 +row Result +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a FROM t WHERE a = 1 AND b <> 'x'; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row Filter: ((b <> 'x'::text) AND (a = 1)) +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT a, b || 'x' FROM t WHERE a = 1; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on public.t +row Output: a, (b || 'x'::text) +row Filter: (t.a = 1) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t LEFT JOIN u ON t.c = u.a + 1 WHERE t.b IS NULL; +fields QUERY PLAN:25:-1:-1 +row Hash Right Join +row Hash Cond: ((u.a + 1) = t.c) +row -> Seq Scan on u +row -> Hash +row -> Seq Scan on t +row Filter: (b IS NULL) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a, count(*) FROM t GROUP BY a; +fields QUERY PLAN:25:-1:-1 +row HashAggregate +row Group Key: a +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT count(*) FROM t; +fields QUERY PLAN:25:-1:-1 +row Aggregate +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t ORDER BY a DESC LIMIT 5; +fields QUERY PLAN:25:-1:-1 +row Limit +row -> Sort +row Sort Key: a DESC +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT DISTINCT b FROM t; +fields QUERY PLAN:25:-1:-1 +row HashAggregate +row Group Key: b +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a FROM t UNION SELECT a FROM u; +fields QUERY PLAN:25:-1:-1 +row HashAggregate +row Group Key: t.a +row -> Append +row -> Seq Scan on t +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a FROM t UNION ALL SELECT a FROM u; +fields QUERY PLAN:25:-1:-1 +row Append +row -> Seq Scan on t +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a, row_number() OVER (ORDER BY c) FROM t; +fields QUERY PLAN:25:-1:-1 +row WindowAgg +row Window: w1 AS (ORDER BY c ROWS UNBOUNDED PRECEDING) +row -> Sort +row Sort Key: c +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM generate_series(1, 3); +fields QUERY PLAN:25:-1:-1 +row Function Scan on generate_series +EXPLAIN +> EXPLAIN (COSTS OFF) VALUES (1), (2); +fields QUERY PLAN:25:-1:-1 +row Values Scan on "*VALUES*" +EXPLAIN +> EXPLAIN (COSTS OFF) WITH w AS MATERIALIZED (SELECT * FROM t) SELECT * FROM w; +fields QUERY PLAN:25:-1:-1 +row CTE Scan on w +row CTE w +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT generate_series(1, a) FROM t; +fields QUERY PLAN:25:-1:-1 +row ProjectSet +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) INSERT INTO t VALUES (1, 'x', 2); +fields QUERY PLAN:25:-1:-1 +row Insert on t +row -> Result +EXPLAIN +> EXPLAIN (COSTS OFF) UPDATE t SET a = 2 WHERE c = 3; +fields QUERY PLAN:25:-1:-1 +row Update on t +row -> Seq Scan on t +row Filter: (c = 3) +EXPLAIN +> EXPLAIN (COSTS OFF) DELETE FROM t WHERE c = 3; +fields QUERY PLAN:25:-1:-1 +row Delete on t +row -> Seq Scan on t +row Filter: (c = 3) +EXPLAIN +> EXPLAIN SELECT * FROM t; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t (cost=0.00..22.00 rows=1200 width=40) +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) SELECT a FROM t WHERE a = 1; +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "Seq Scan", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false, + "Filter": "(a = 1)" + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT YAML) SELECT a FROM t WHERE a = 1; +fields QUERY PLAN:25:-1:-1 +row - Plan: + Node Type: "Seq Scan" + Parallel Aware: false + Async Capable: false + Relation Name: "t" + Alias: "t" + Disabled: false + Filter: "(a = 1)" +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT XML) SELECT a FROM t WHERE a = 1; +fields QUERY PLAN:142:-1:-1 +row + + + Seq Scan + false + false + t + t + false + (a = 1) + + + +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT lower(b), count(*) FROM t GROUP BY lower(b); +fields QUERY PLAN:25:-1:-1 +row HashAggregate +row Group Key: lower(b) +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a + 1, count(*) FROM t GROUP BY a + 1 ORDER BY count(*) DESC; +fields QUERY PLAN:25:-1:-1 +row Sort +row Sort Key: (count(*)) DESC +row -> HashAggregate +row Group Key: (a + 1) +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t ORDER BY a + 1, lower(b); +fields QUERY PLAN:25:-1:-1 +row Sort +row Sort Key: ((a + 1)), (lower(b)) +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT a, count(*) FROM t GROUP BY a ORDER BY 2; +fields QUERY PLAN:25:-1:-1 +row Sort +row Output: a, (count(*)) +row Sort Key: (count(*)) +row -> HashAggregate +row Output: a, count(*) +row Group Key: t.a +row -> Seq Scan on public.t +row Output: a, b, c +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM t ORDER BY a + 1; +fields QUERY PLAN:25:-1:-1 +row Sort +row Output: a, b, c, ((a + 1)) +row Sort Key: ((t.a + 1)) +row -> Seq Scan on public.t +row Output: a, b, c, (a + 1) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE a = (SELECT 1) ORDER BY b; +fields QUERY PLAN:25:-1:-1 +row Sort +row Sort Key: t.b +row InitPlan expr_1 +row -> Result +row -> Seq Scan on t +row Filter: (a = (InitPlan expr_1).col1) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT (SELECT 1), a FROM t ORDER BY b LIMIT 3; +fields QUERY PLAN:25:-1:-1 +row Limit +row InitPlan expr_1 +row -> Result +row -> Sort +row Sort Key: t.b +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT sum(a + 1), max(b) FROM t; +fields QUERY PLAN:25:-1:-1 +row Aggregate +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT count(DISTINCT a) FROM t; +fields QUERY PLAN:25:-1:-1 +row Aggregate +row -> Sort +row Sort Key: a +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT b, count(DISTINCT a) FROM t GROUP BY b; +fields QUERY PLAN:25:-1:-1 +row GroupAggregate +row Group Key: b +row -> Sort +row Sort Key: b, a +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a, sum(c) OVER (PARTITION BY b ORDER BY a), avg(c) OVER () FROM t; +fields QUERY PLAN:25:-1:-1 +row WindowAgg +row Window: w2 AS () +row -> WindowAgg +row Window: w1 AS (PARTITION BY b ORDER BY a) +row -> Sort +row Sort Key: b, a +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t LIMIT 5 OFFSET 2; +fields QUERY PLAN:25:-1:-1 +row Limit +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t OFFSET 2; +fields QUERY PLAN:25:-1:-1 +row Limit +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a FROM t INTERSECT SELECT a FROM u; +fields QUERY PLAN:25:-1:-1 +row HashSetOp Intersect +row -> Seq Scan on t +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT a FROM t EXCEPT ALL SELECT a FROM u; +fields QUERY PLAN:25:-1:-1 +row HashSetOp Except All +row -> Seq Scan on t +row -> Seq Scan on u +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM (SELECT a FROM t LIMIT 3) s; +fields QUERY PLAN:25:-1:-1 +row Limit +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t FOR UPDATE; +fields QUERY PLAN:25:-1:-1 +row LockRows +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE b LIKE 'x%' OR a IS NOT NULL; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +row Filter: ((b ~~ 'x%'::text) OR (a IS NOT NULL)) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE false; +fields QUERY PLAN:25:-1:-1 +row Result +row Replaces: Scan on t +row One-Time Filter: false +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t WHERE 1 = 2; +fields QUERY PLAN:25:-1:-1 +row Result +row Replaces: Scan on t +row One-Time Filter: false +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM generate_series(1, 3) g WHERE g > 1; +fields QUERY PLAN:25:-1:-1 +row Function Scan on generate_series g +row Filter: (g > 1) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM generate_series(1, 3) WITH ORDINALITY AS g(x, n); +fields QUERY PLAN:25:-1:-1 +row Function Scan on generate_series g +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM unnest(ARRAY[1,2]) u, generate_series(1, 2) g; +fields QUERY PLAN:25:-1:-1 +row Nested Loop +row -> Function Scan on unnest u +row -> Function Scan on generate_series g +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t CROSS JOIN u; +fields QUERY PLAN:25:-1:-1 +row Nested Loop +row -> Seq Scan on u +row -> Materialize +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t t1 JOIN t t2 ON t1.b = t2.b; +fields QUERY PLAN:25:-1:-1 +row Merge Join +row Merge Cond: (t1.b = t2.b) +row -> Sort +row Sort Key: t1.b +row -> Seq Scan on t t1 +row -> Sort +row Sort Key: t2.b +row -> Seq Scan on t t2 +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t t1 JOIN t t2 ON t1.a < t2.a; +fields QUERY PLAN:25:-1:-1 +row Nested Loop +row Join Filter: (t1.a < t2.a) +row -> Seq Scan on t t1 +row -> Materialize +row -> Seq Scan on t t2 +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t, LATERAL (SELECT * FROM u WHERE u.a > t.a LIMIT 1) s; +fields QUERY PLAN:25:-1:-1 +row Nested Loop +row -> Seq Scan on t +row -> Limit +row -> Seq Scan on u +row Filter: (a > t.a) +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t, LATERAL generate_series(1, t.a) g; +fields QUERY PLAN:25:-1:-1 +row Nested Loop +row -> Seq Scan on t +row -> Function Scan on generate_series g +EXPLAIN +> EXPLAIN (COSTS OFF) WITH w AS (SELECT * FROM t) SELECT * FROM w a1, w a2; +fields QUERY PLAN:25:-1:-1 +row Nested Loop +row CTE w +row -> Seq Scan on t +row -> CTE Scan on w a1 +row -> CTE Scan on w a2 +EXPLAIN +> EXPLAIN (COSTS OFF) WITH w AS (SELECT * FROM t) SELECT * FROM w; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF) WITH w AS (INSERT INTO t VALUES (1, 'x', 2) RETURNING *) SELECT * FROM w; +fields QUERY PLAN:25:-1:-1 +row CTE Scan on w +row CTE w +row -> Insert on t +row -> Result +EXPLAIN +> EXPLAIN (COSTS OFF) INSERT INTO t SELECT * FROM t; +fields QUERY PLAN:25:-1:-1 +row Insert on t +row -> Seq Scan on t t_1 +EXPLAIN +> EXPLAIN (COSTS OFF) INSERT INTO t VALUES (1, 'x', 2), (3, 'y', 4); +fields QUERY PLAN:25:-1:-1 +row Insert on t +row -> Values Scan on "*VALUES*" +EXPLAIN +> EXPLAIN (COSTS OFF) INSERT INTO u VALUES (1, 'x') ON CONFLICT (a) DO NOTHING; +fields QUERY PLAN:25:-1:-1 +row Insert on u +row Conflict Resolution: NOTHING +row Conflict Arbiter Indexes: u_pkey +row -> Result +EXPLAIN +> EXPLAIN (COSTS OFF) INSERT INTO u VALUES (1, 'x') ON CONFLICT (a) DO UPDATE SET d = excluded.d; +fields QUERY PLAN:25:-1:-1 +row Insert on u +row Conflict Resolution: UPDATE +row Conflict Arbiter Indexes: u_pkey +row -> Result +EXPLAIN +> EXPLAIN (COSTS OFF) DELETE FROM t RETURNING *; +fields QUERY PLAN:25:-1:-1 +row Delete on t +row -> Seq Scan on t +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) INSERT INTO t VALUES (1, 'x', 2); +fields QUERY PLAN:25:-1:-1 +row Insert on public.t +row -> Result +row Output: 1, 'x'::text, 2 +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT * FROM generate_series(1, 3); +fields QUERY PLAN:25:-1:-1 +row Function Scan on pg_catalog.generate_series +row Output: generate_series +row Function Call: generate_series(1, 3) +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT 1 AS x, 'a' AS y; +fields QUERY PLAN:25:-1:-1 +row Result +row Output: 1, 'a'::text +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) VALUES (1, 'a'), (2, 'b'); +fields QUERY PLAN:25:-1:-1 +row Values Scan on "*VALUES*" +row Output: column1, column2 +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT a, row_number() OVER w FROM t WINDOW w AS (PARTITION BY b); +fields QUERY PLAN:25:-1:-1 +row WindowAgg +row Output: a, row_number() OVER w, b +row Window: w AS (PARTITION BY t.b ROWS UNBOUNDED PRECEDING) +row -> Sort +row Output: b, a +row Sort Key: t.b +row -> Seq Scan on public.t +row Output: b, a +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT DISTINCT a, b FROM t; +fields QUERY PLAN:25:-1:-1 +row HashAggregate +row Output: a, b +row Group Key: t.a, t.b +row -> Seq Scan on public.t +row Output: a, b, c +EXPLAIN +> EXPLAIN (COSTS OFF, VERBOSE) SELECT DISTINCT ON (a) a, b FROM t ORDER BY a, b; +fields QUERY PLAN:25:-1:-1 +row Unique +row Output: a, b +row -> Sort +row Output: a, b +row Sort Key: t.a, t.b +row -> Seq Scan on public.t +row Output: a, b +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) SELECT a, count(*) FROM t GROUP BY a; +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "Aggregate", + "Strategy": "Hashed", + "Partial Mode": "Simple", + "Parallel Aware": false, + "Async Capable": false, + "Disabled": false, + "Group Key": ["a"], + "Plans": [ + { + "Node Type": "Seq Scan", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false + } + ] + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT YAML) SELECT * FROM t ORDER BY a LIMIT 1; +fields QUERY PLAN:25:-1:-1 +row - Plan: + Node Type: "Limit" + Parallel Aware: false + Async Capable: false + Disabled: false + Plans: + - Node Type: "Sort" + Parent Relationship: "Outer" + Parallel Aware: false + Async Capable: false + Disabled: false + Sort Key: + - "a" + Plans: + - Node Type: "Seq Scan" + Parent Relationship: "Outer" + Parallel Aware: false + Async Capable: false + Relation Name: "t" + Alias: "t" + Disabled: false +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT XML) SELECT a FROM t UNION ALL SELECT a FROM u; +fields QUERY PLAN:142:-1:-1 +row + + + Append + false + false + false + 0 + + + Seq Scan + Member + false + false + t + t + false + + + Seq Scan + Member + false + false + u + u + false + + + + + +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) SELECT a FROM t INTERSECT SELECT a FROM u; +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "SetOp", + "Strategy": "Hashed", + "Parallel Aware": false, + "Async Capable": false, + "Command": "Intersect", + "Disabled": false, + "Plans": [ + { + "Node Type": "Seq Scan", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false + }, + { + "Node Type": "Seq Scan", + "Parent Relationship": "Inner", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "u", + "Alias": "u", + "Disabled": false + } + ] + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) SELECT * FROM t WHERE a = (SELECT 1); +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "Seq Scan", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false, + "Filter": "(a = (InitPlan expr_1).col1)", + "Plans": [ + { + "Node Type": "Result", + "Parent Relationship": "InitPlan", + "Subplan Name": "InitPlan expr_1", + "Parallel Aware": false, + "Async Capable": false, + "Disabled": false + } + ] + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT YAML, VERBOSE) SELECT * FROM t WHERE a = 1; +fields QUERY PLAN:25:-1:-1 +row - Plan: + Node Type: "Seq Scan" + Parallel Aware: false + Async Capable: false + Relation Name: "t" + Schema: "public" + Alias: "t" + Disabled: false + Output: + - "a" + - "b" + - "c" + Filter: "(t.a = 1)" +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) INSERT INTO t VALUES (1, 'x', 2); +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "ModifyTable", + "Operation": "Insert", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false, + "Plans": [ + { + "Node Type": "Result", + "Parent Relationship": "Outer", + "Parallel Aware": false, + "Async Capable": false, + "Disabled": false + } + ] + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT JSON) WITH w AS MATERIALIZED (SELECT * FROM t) SELECT * FROM w; +fields QUERY PLAN:114:-1:-1 +row [ + { + "Plan": { + "Node Type": "CTE Scan", + "Parallel Aware": false, + "Async Capable": false, + "CTE Name": "w", + "Alias": "w", + "Disabled": false, + "Plans": [ + { + "Node Type": "Seq Scan", + "Parent Relationship": "InitPlan", + "Subplan Name": "CTE w", + "Parallel Aware": false, + "Async Capable": false, + "Relation Name": "t", + "Alias": "t", + "Disabled": false + } + ] + } + } +] +EXPLAIN +> EXPLAIN (COSTS OFF, FORMAT XML, VERBOSE) SELECT a, b FROM t ORDER BY a DESC NULLS LAST, b USING >; +fields QUERY PLAN:142:-1:-1 +row + + + Sort + false + false + false + + a + b + + + t.a DESC NULLS LAST + t.b DESC + + + + Seq Scan + Outer + false + false + t + public + t + false + + a + b + + + + + + +EXPLAIN +> EXPLAIN (COSTS OFF) SELECT * FROM t ORDER BY a NULLS FIRST, b DESC NULLS LAST, c DESC NULLS FIRST; +fields QUERY PLAN:25:-1:-1 +row Sort +row Sort Key: a NULLS FIRST, b DESC NULLS LAST, c DESC +row -> Seq Scan on t +EXPLAIN +> EXPLAIN SELECT a, count(*) FROM t GROUP BY a; +fields QUERY PLAN:25:-1:-1 +row HashAggregate (cost=28.00..30.00 rows=200 width=12) +row Group Key: a +row -> Seq Scan on t (cost=0.00..22.00 rows=1200 width=4) +EXPLAIN +> EXPLAIN SELECT * FROM t WHERE a = 1; +fields QUERY PLAN:25:-1:-1 +row Seq Scan on t (cost=0.00..25.00 rows=6 width=40) +row Filter: (a = 1) +EXPLAIN +> EXPLAIN SELECT * FROM t ORDER BY a; +fields QUERY PLAN:25:-1:-1 +row Sort (cost=83.37..86.37 rows=1200 width=40) +row Sort Key: a +row -> Seq Scan on t (cost=0.00..22.00 rows=1200 width=40) +EXPLAIN +> EXPLAIN SELECT count(*) FROM t; +fields QUERY PLAN:25:-1:-1 +row Aggregate (cost=25.00..25.01 rows=1 width=8) +row -> Seq Scan on t (cost=0.00..22.00 rows=1200 width=0) +EXPLAIN +> EXPLAIN SELECT * FROM t LIMIT 10; +fields QUERY PLAN:25:-1:-1 +row Limit (cost=0.00..0.18 rows=10 width=40) +row -> Seq Scan on t (cost=0.00..22.00 rows=1200 width=40) +EXPLAIN From 1cc9c0a0dc21694b2d9c30e338665d4d1b16b8fe Mon Sep 17 00:00:00 2001 From: Tam Nguyen Duc <1218621+tamnd@users.noreply.github.com> Date: Tue, 20 Oct 2026 12:31:12 +0700 Subject: [PATCH 3/3] Add the changelog entry for #439 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 288b472c..a431be35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All changes that a user can see are in this file. The version is 0.M.patch, wher ## Unreleased +### Added + +- `EXPLAIN` in the text, JSON, XML and YAML forms, with a planner that makes the plan of a query with the costs of PostgreSQL (#439). + ## 0.1.51 (2026-10-20) ### Added