use crate::optimizer::cost::config::{
DEDUP_SELECTIVITY, GET_EDGES_DEFAULT_ROWS, GET_VERTICES_DEFAULT_ROWS, UNKNOWN_SCAN_ROWS,
};
use crate::optimizer::cost::SelectivityEstimator;
use crate::optimizer::cost_based::ndv as factor_cost;
use crate::optimizer::cost_based::ndv::DEFAULT_JOIN_SELECTIVITY;
use crate::optimizer::stats::StatsView;
use crate::planning::plan::logical::logical_node_traits::LogicalSingleInputNode;
use crate::planning::plan::logical::LogicalNodeEnum;
use super::stats::{stats_fanout_skewed, DEFAULT_FILTER_SELECTIVITY, DEFAULT_NEIGHBORHOOD_FANOUT};
pub fn estimate_node_output_rows_logical(
node: &LogicalNodeEnum,
stats: &StatsView,
selectivity: &SelectivityEstimator,
) -> u64 {
use LogicalNodeEnum::*;
match node {
ScanVertices(n) => {
let tag_rows = n
.tag
.as_deref()
.map(|tag| stats.vertex_count(tag))
.unwrap_or(UNKNOWN_SCAN_ROWS);
n.limit
.map(|limit| tag_rows.min(limit as u64))
.unwrap_or(tag_rows)
}
ScanEdges(n) => {
let edge_rows = n
.edge_type
.as_deref()
.map(|edge_type| stats.edge_count(edge_type))
.unwrap_or(UNKNOWN_SCAN_ROWS);
n.limit
.map(|limit| edge_rows.min(limit as u64))
.unwrap_or(edge_rows)
}
GetVertices(n) => n.limit.unwrap_or(GET_VERTICES_DEFAULT_ROWS as i64).max(1) as u64,
GetEdges(n) => n.limit.unwrap_or(GET_EDGES_DEFAULT_ROWS as i64).max(1) as u64,
GetNeighbors(n) => {
let fanout = n
.edge_types
.iter()
.find_map(|edge_type| {
stats
.edge_stats(edge_type)
.map(|s| (s.avg_out_degree.max(0.0)) as u64)
})
.unwrap_or(DEFAULT_NEIGHBORHOOD_FANOUT);
let estimated = fanout.max(1);
match n.limit {
Some(limit) => (limit.max(1) as u64).min(estimated),
None => estimated,
}
}
Start(_) => 1,
Filter(n) => {
let input_rows = estimate_node_output_rows_logical(n.input(), stats, selectivity);
let tag_name = first_tag_of_logical_input(n.input());
let expr_selectivity = n
.condition
.expression()
.map(|meta| {
selectivity.estimate_from_expression(
stats.space(),
meta.inner(),
tag_name.as_deref(),
)
})
.unwrap_or(DEFAULT_FILTER_SELECTIVITY);
(input_rows as f64 * expr_selectivity).max(1.0) as u64
}
Project(_) | Sort(_) | Sample(_) | Window(_) => {
child_rows_of_logical(node, stats, selectivity)
}
TopN(n) => {
let input_rows = estimate_node_output_rows_logical(n.input(), stats, selectivity);
input_rows.min(n.limit as u64)
}
Limit(n) => {
let input_rows = estimate_node_output_rows_logical(n.input(), stats, selectivity);
input_rows.min((n.offset + n.count) as u64)
}
Skip(n) => {
let input_rows = estimate_node_output_rows_logical(n.input(), stats, selectivity);
input_rows.saturating_sub(n.offset.max(0) as u64)
}
Dedup(n) => {
let input_rows = estimate_node_output_rows_logical(n.input(), stats, selectivity);
(input_rows as f64 * DEDUP_SELECTIVITY).max(1.0) as u64
}
Aggregate(n) => {
let input_rows = estimate_node_output_rows_logical(n.input(), stats, selectivity);
if n.group_key_exprs.is_empty() {
1
} else {
let tag_for_ndv = first_tag_of_logical_input(n.input());
let keys: Vec<String> = n
.group_key_exprs
.iter()
.map(|e| e.to_expression_string())
.collect();
let ndv = factor_cost::ndv_for_group_keys(stats, tag_for_ndv.as_deref(), &keys);
if let Some(distinct) = ndv {
distinct.min(input_rows).max(1)
} else {
factor_cost::estimate_group_count(input_rows, keys.len())
}
}
}
InnerJoin(n) => {
let Some((left_node, right_node)) = logical_binary_inputs(node) else {
return child_rows_of_logical(node, stats, selectivity);
};
let left = estimate_node_output_rows_logical(left_node, stats, selectivity);
let right = estimate_node_output_rows_logical(right_node, stats, selectivity);
let sel = factor_cost::join_selectivity(stats, n.hash_keys(), n.probe_keys())
.unwrap_or(DEFAULT_JOIN_SELECTIVITY);
factor_cost::join_output_rows(left, right, sel)
}
LeftJoin(_) => {
let Some((left, _)) = logical_binary_inputs(node) else {
return child_rows_of_logical(node, stats, selectivity);
};
estimate_node_output_rows_logical(left, stats, selectivity)
}
RightJoin(_) => {
let Some((_, right)) = logical_binary_inputs(node) else {
return child_rows_of_logical(node, stats, selectivity);
};
estimate_node_output_rows_logical(right, stats, selectivity)
}
CrossJoin(_) => {
let Some((left, right)) = logical_binary_inputs(node) else {
return child_rows_of_logical(node, stats, selectivity);
};
let left = estimate_node_output_rows_logical(left, stats, selectivity);
let right = estimate_node_output_rows_logical(right, stats, selectivity);
left.saturating_mul(right)
}
FullOuterJoin(_) => {
let Some((left, right)) = logical_binary_inputs(node) else {
return child_rows_of_logical(node, stats, selectivity);
};
let left = estimate_node_output_rows_logical(left, stats, selectivity);
let right = estimate_node_output_rows_logical(right, stats, selectivity);
left.saturating_add(right)
}
SemiJoin(_) => child_rows_of_logical(node, stats, selectivity),
LogicalNodeEnum::WcoIntersect(n) => {
let probe = estimate_node_output_rows_logical(n.probe_side(), stats, selectivity);
let mut build_cards = Vec::with_capacity(n.num_builds());
let mut smallest_build = u64::MAX;
for build in n.build_sides() {
let rows = estimate_node_output_rows_logical(build, stats, selectivity);
smallest_build = smallest_build.min(rows);
build_cards.push(rows);
}
if smallest_build == u64::MAX {
probe
} else {
let conservative = (probe as f64
* crate::planning::join_order::cardinality_estimator::NON_EQUALITY_PREDICATE_SELECTIVITY)
.max(1.0) as u64;
let mut domain = probe.max(smallest_build).max(1);
for tag in stats.manager().get_all_tags() {
domain = domain.max(stats.vertex_count(&tag)).max(1);
}
let mut numerator = probe as u128;
for card in &build_cards {
numerator = numerator.saturating_mul(*card as u128);
}
let mut denominator: u128 = 1;
for _ in 0..build_cards.len() {
denominator = denominator.saturating_mul(domain as u128);
}
let independent = (numerator / denominator.max(1)).min(u64::MAX as u128) as u64;
conservative.min(independent).min(smallest_build).max(1)
}
}
Expand(n) => {
let fanout = stats_fanout_skewed(stats, &n.edge_types);
child_rows_of_logical(node, stats, selectivity).saturating_mul(fanout)
}
ExpandAll(n) => {
let fanout = stats_fanout_skewed(stats, &n.edge_types);
child_rows_of_logical(node, stats, selectivity).saturating_mul(fanout)
}
Traverse(n) => {
let fanout = stats_fanout_skewed(stats, &n.edge_types);
child_rows_of_logical(node, stats, selectivity).saturating_mul(fanout)
}
BiExpand(n) => {
let fanout = stats_fanout_skewed(stats, &n.edge_types);
child_rows_of_logical(node, stats, selectivity).saturating_mul(fanout)
}
BiTraverse(n) => {
let fanout = stats_fanout_skewed(stats, &n.edge_types);
child_rows_of_logical(node, stats, selectivity).saturating_mul(fanout)
}
AppendVertices(_) => child_rows_of_logical(node, stats, selectivity)
.saturating_mul(DEFAULT_NEIGHBORHOOD_FANOUT),
PatternApply(_) | Apply(_) | CorrelatedApply(_) => {
if let Some((left, right)) = logical_binary_inputs(node) {
let left_rows = estimate_node_output_rows_logical(left, stats, selectivity);
let right_rows = estimate_node_output_rows_logical(right, stats, selectivity);
left_rows.saturating_mul(right_rows).max(1)
} else {
child_rows_of_logical(node, stats, selectivity)
}
}
node => child_rows_of_logical(node, stats, selectivity),
}
}
fn child_rows_of_logical(
node: &LogicalNodeEnum,
stats: &StatsView,
selectivity: &SelectivityEstimator,
) -> u64 {
logical_first_child(node)
.map(|child| estimate_node_output_rows_logical(child, stats, selectivity))
.unwrap_or(1)
}
fn logical_first_child(node: &LogicalNodeEnum) -> Option<&LogicalNodeEnum> {
match node {
LogicalNodeEnum::Project(n) => Some(n.input()),
LogicalNodeEnum::Filter(n) => Some(n.input()),
LogicalNodeEnum::Sort(n) => Some(n.input()),
LogicalNodeEnum::Limit(n) => Some(n.input()),
LogicalNodeEnum::Skip(n) => Some(n.input()),
LogicalNodeEnum::TopN(n) => Some(n.input()),
LogicalNodeEnum::Sample(n) => Some(n.input()),
LogicalNodeEnum::Dedup(n) => Some(n.input()),
LogicalNodeEnum::Aggregate(n) => Some(n.input()),
LogicalNodeEnum::Window(n) => Some(n.input()),
LogicalNodeEnum::InnerJoin(n) => Some(n.left_input()),
LogicalNodeEnum::LeftJoin(n) => Some(n.left_input()),
LogicalNodeEnum::RightJoin(n) => Some(n.left_input()),
LogicalNodeEnum::CrossJoin(n) => Some(n.left_input()),
LogicalNodeEnum::FullOuterJoin(n) => Some(n.left_input()),
LogicalNodeEnum::SemiJoin(n) => Some(n.left_input()),
LogicalNodeEnum::WcoIntersect(n) => Some(n.probe_side()),
LogicalNodeEnum::GetVertices(n) => n.dependencies().first(),
LogicalNodeEnum::GetNeighbors(n) => n.dependencies().first(),
LogicalNodeEnum::Expand(n) => n.dependencies().first(),
LogicalNodeEnum::ExpandAll(n) => n.dependencies().first(),
LogicalNodeEnum::Traverse(n) => n.dependencies().first(),
LogicalNodeEnum::AppendVertices(n) => n.dependencies().first(),
LogicalNodeEnum::BiExpand(n) => Some(n.left_input()),
LogicalNodeEnum::BiTraverse(n) => Some(n.left_input()),
LogicalNodeEnum::PatternApply(n) => Some(n.left_input()),
LogicalNodeEnum::CorrelatedApply(n) => Some(n.left_input()),
LogicalNodeEnum::Apply(n) => Some(n.left_input()),
LogicalNodeEnum::RollUpApply(n) => n.dependencies().first(),
_ => None,
}
}
fn logical_binary_inputs(node: &LogicalNodeEnum) -> Option<(&LogicalNodeEnum, &LogicalNodeEnum)> {
match node {
LogicalNodeEnum::InnerJoin(n) => Some((n.left_input(), n.right_input())),
LogicalNodeEnum::LeftJoin(n) => Some((n.left_input(), n.right_input())),
LogicalNodeEnum::RightJoin(n) => Some((n.left_input(), n.right_input())),
LogicalNodeEnum::CrossJoin(n) => Some((n.left_input(), n.right_input())),
LogicalNodeEnum::FullOuterJoin(n) => Some((n.left_input(), n.right_input())),
LogicalNodeEnum::SemiJoin(n) => Some((n.left_input(), n.right_input())),
LogicalNodeEnum::PatternApply(n) => Some((n.left_input(), n.right_input())),
LogicalNodeEnum::CorrelatedApply(n) => Some((n.left_input(), n.right_input())),
LogicalNodeEnum::Apply(n) => Some((n.left_input(), n.right_input())),
LogicalNodeEnum::WcoIntersect(_) => None,
_ => None,
}
}
fn first_tag_of_logical_input(node: &LogicalNodeEnum) -> Option<String> {
match node {
LogicalNodeEnum::ScanVertices(n) => n.tag.clone(),
_ => None,
}
}