#![forbid(unsafe_code)]
mod auto_partition;
mod broadcast;
mod broadcast_runtime;
mod coalesce;
mod constant_folding;
mod dynamic_partition_pruning;
mod join_reorder;
mod predicate_pushdown;
mod skew_join;
mod small_file;
mod stats;
#[cfg(test)]
mod optimizer_tests;
pub use auto_partition::AutoPartitionRule;
pub use broadcast::{BroadcastAutoRule, DEFAULT_BROADCAST_THRESHOLD_ROWS};
pub use broadcast_runtime::{BroadcastRuntimeRule, DEFAULT_MAX_BROADCAST_BYTES};
pub use coalesce::{CoalesceAdvice, CoalesceRule};
pub use constant_folding::ConstantFoldingRule;
pub use dynamic_partition_pruning::{
DPP_MAX_BUILD_ROWS, DPP_MAX_KEYS, DppAdvice, DynamicPartitionPruningRule,
};
pub use join_reorder::JoinReorderRule;
pub use predicate_pushdown::PredicatePushdownRule;
pub use skew_join::{DEFAULT_SALT_FACTOR, DEFAULT_SKEW_THRESHOLD, SkewAdvice, SkewJoinRule};
pub use small_file::{FileStats, SmallFilePlanner, SplitPlanAdvice};
pub use stats::{
CboCostModel, ColumnCboStats, TableCboStats, TableStatsRegistry, global_table_stats,
};
use std::panic::{AssertUnwindSafe, catch_unwind};
use crate::{ExecutionKind, LogicalPlan, NodeOp, PhysicalPlan, PlanError};
pub type OptimizerResult<T> = Result<T, OptimizerError>;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum OptimizerError {
#[error("invalid {optimizer} optimizer input: {source}")]
InvalidInput {
optimizer: &'static str,
#[source]
source: PlanError,
},
#[error("{optimizer} optimizer rule '{rule}' produced an invalid plan: {source}")]
InvalidRuleOutput {
optimizer: &'static str,
rule: String,
#[source]
source: PlanError,
},
#[error("{optimizer} optimizer rule '{rule}' panicked: {message}")]
RulePanicked {
optimizer: &'static str,
rule: String,
message: String,
},
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Cost {
pub cpu_nanos: u64,
pub memory_bytes: u64,
pub network_bytes: u64,
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RuntimeStats {
pub input_rows: u64,
pub output_rows: u64,
pub cpu_nanos: u64,
pub memory_bytes: u64,
pub spill_bytes: u64,
pub serialized_bytes: u64,
}
pub trait CostModel: Send + Sync {
fn estimate(&self, plan: &LogicalPlan) -> Cost;
}
pub struct StaticCostModel;
impl CostModel for StaticCostModel {
fn estimate(&self, plan: &LogicalPlan) -> Cost {
const DEFAULT_ROWS: u64 = 10_000;
let mut cpu_nanos: u64 = 0;
let mut memory_bytes: u64 = 0;
let mut network_bytes: u64 = 0;
for node in plan.nodes() {
let rows = node.estimated_rows().unwrap_or(DEFAULT_ROWS);
match node.op() {
Some(NodeOp::Scan { .. }) => {
cpu_nanos = cpu_nanos.saturating_add(rows.saturating_mul(10));
memory_bytes = memory_bytes.saturating_add(rows.saturating_mul(64));
}
Some(NodeOp::Filter { .. }) => {
cpu_nanos = cpu_nanos.saturating_add(rows.saturating_mul(5));
}
Some(NodeOp::Project { .. }) => {
cpu_nanos = cpu_nanos.saturating_add(rows.saturating_mul(2));
}
Some(NodeOp::Aggregate { .. }) => {
cpu_nanos = cpu_nanos.saturating_add(rows.saturating_mul(50));
memory_bytes = memory_bytes.saturating_add(rows.saturating_mul(200));
}
Some(NodeOp::Join { .. }) => {
cpu_nanos = cpu_nanos.saturating_add(rows.saturating_mul(100));
memory_bytes = memory_bytes.saturating_add(rows.saturating_mul(100));
}
Some(NodeOp::Exchange { .. }) => {
cpu_nanos = cpu_nanos.saturating_add(rows.saturating_mul(20));
network_bytes = network_bytes.saturating_add(rows.saturating_mul(200));
}
_ => {
cpu_nanos = cpu_nanos.saturating_add(rows.saturating_mul(15));
memory_bytes = memory_bytes.saturating_add(rows.saturating_mul(64));
}
}
}
Cost {
cpu_nanos,
memory_bytes,
network_bytes,
}
}
}
pub trait OptimizerRule: Send + Sync {
fn name(&self) -> &str;
fn apply(&self, plan: &LogicalPlan) -> Option<LogicalPlan>;
}
pub trait AqeRule: Send + Sync {
fn name(&self) -> &str;
fn apply(&self, plan: &PhysicalPlan, stats: &[RuntimeStats]) -> Option<PhysicalPlan>;
}
pub trait SkewRule: Send + Sync {
fn name(&self) -> &str;
fn detect_hot_partitions(&self, stats: &[RuntimeStats]) -> Vec<usize>;
}
#[derive(Debug, Clone)]
pub struct OptimizeResult {
pub plan: LogicalPlan,
pub applied_rules: Vec<String>,
}
impl OptimizeResult {
pub fn describe(&self) -> String {
if self.applied_rules.is_empty() {
return "optimizer: no rules applied".to_string();
}
let rules = self.applied_rules.join(", ");
format!("optimizer applied: {rules}")
}
}
pub struct Optimizer {
rules: Vec<Box<dyn OptimizerRule>>,
}
impl Optimizer {
pub fn new() -> Self {
Self { rules: Vec::new() }
}
pub fn add_rule(&mut self, rule: Box<dyn OptimizerRule>) {
self.rules.push(rule);
}
pub fn optimize(&self, plan: LogicalPlan) -> OptimizerResult<OptimizeResult> {
plan.validate()
.map_err(|source| OptimizerError::InvalidInput {
optimizer: "logical",
source,
})?;
let mut current = plan;
let mut applied_rules = Vec::new();
for rule in &self.rules {
let rule_name = rule.name().to_string();
let outcome =
catch_unwind(AssertUnwindSafe(|| rule.apply(¤t))).map_err(|payload| {
OptimizerError::RulePanicked {
optimizer: "logical",
rule: rule_name.clone(),
message: krishiv_common::panic_payload_to_string(&*payload),
}
})?;
if let Some(new_plan) = outcome {
if new_plan.name() != current.name() || new_plan.kind() != current.kind() {
return Err(OptimizerError::InvalidRuleOutput {
optimizer: "logical",
rule: rule_name,
source: PlanError::Validation(String::from(
"logical optimizer rules must preserve plan name and execution kind",
)),
});
}
new_plan
.validate()
.map_err(|source| OptimizerError::InvalidRuleOutput {
optimizer: "logical",
rule: rule_name.clone(),
source,
})?;
if new_plan != current {
applied_rules.push(rule_name);
current = new_plan;
}
}
}
Ok(OptimizeResult {
plan: current,
applied_rules,
})
}
}
impl Default for Optimizer {
fn default() -> Self {
Self::new()
}
}
pub struct ThresholdSkewRule {
threshold: f64,
}
impl ThresholdSkewRule {
pub fn new(threshold: f64) -> Self {
Self { threshold }
}
fn median_rows(stats: &[RuntimeStats]) -> f64 {
if stats.is_empty() {
return 0.0;
}
let mut rows: Vec<u64> = stats.iter().map(|s| s.input_rows).collect();
rows.sort_unstable();
let n = rows.len();
let mid = n / 2;
if n.is_multiple_of(2) {
let a = rows.get(mid.saturating_sub(1)).copied().unwrap_or(0);
let b = rows.get(mid).copied().unwrap_or(0);
(a as f64 + b as f64) / 2.0
} else {
rows.get(mid).copied().unwrap_or(0) as f64
}
}
}
impl SkewRule for ThresholdSkewRule {
fn name(&self) -> &str {
"threshold-skew"
}
fn detect_hot_partitions(&self, stats: &[RuntimeStats]) -> Vec<usize> {
if stats.is_empty() {
return Vec::new();
}
let median = Self::median_rows(stats);
stats
.iter()
.enumerate()
.filter(|(_, s)| s.input_rows as f64 > self.threshold * median)
.map(|(i, _)| i)
.collect()
}
}
pub struct StreamingAqeGuard;
impl StreamingAqeGuard {
pub fn plan_is_streaming(plan: &PhysicalPlan) -> bool {
plan.kind() == ExecutionKind::Streaming
|| plan
.nodes()
.iter()
.any(|node| node.kind() == ExecutionKind::Streaming)
}
}
pub struct AqeOptimizer {
always_rules: Vec<Box<dyn AqeRule>>,
guarded_rules: Vec<Box<dyn AqeRule>>,
cost_model: std::sync::Arc<dyn CostModel>,
}
impl AqeOptimizer {
pub fn new() -> Self {
Self {
always_rules: Vec::new(),
guarded_rules: Vec::new(),
cost_model: std::sync::Arc::new(StaticCostModel),
}
}
pub fn with_cost_model(mut self, model: std::sync::Arc<dyn CostModel>) -> Self {
self.cost_model = model;
self
}
pub fn add_rule(&mut self, rule: Box<dyn AqeRule>) {
self.always_rules.push(rule);
}
pub fn add_guarded_rule(&mut self, rule: Box<dyn AqeRule>) {
self.guarded_rules.push(rule);
}
pub fn apply(
&self,
plan: PhysicalPlan,
stats: &[RuntimeStats],
) -> OptimizerResult<(PhysicalPlan, Vec<String>)> {
plan.validate()
.map_err(|source| OptimizerError::InvalidInput {
optimizer: "AQE",
source,
})?;
let input_is_streaming = StreamingAqeGuard::plan_is_streaming(&plan);
let mut current = plan;
let mut applied = Vec::new();
let cost_synthesised_stats: Vec<RuntimeStats>;
let effective_stats = if stats.is_empty() && !input_is_streaming {
let mut lplan = crate::LogicalPlan::new(current.name(), current.kind());
for node in current.nodes() {
lplan.add_node(node.clone());
}
let cost = self.cost_model.estimate(&lplan);
cost_synthesised_stats = vec![RuntimeStats {
memory_bytes: cost.memory_bytes,
cpu_nanos: cost.cpu_nanos,
..Default::default()
}];
&cost_synthesised_stats[..]
} else {
stats
};
for rule in &self.always_rules {
let rule_name = rule.name().to_string();
let outcome = catch_unwind(AssertUnwindSafe(|| rule.apply(¤t, effective_stats)))
.map_err(|payload| OptimizerError::RulePanicked {
optimizer: "AQE",
rule: rule_name.clone(),
message: krishiv_common::panic_payload_to_string(&*payload),
})?;
if let Some(new_plan) = outcome {
if new_plan.name() != current.name() || new_plan.kind() != current.kind() {
return Err(OptimizerError::InvalidRuleOutput {
optimizer: "AQE",
rule: rule_name,
source: PlanError::Validation(String::from(
"AQE rules must preserve plan name and execution kind",
)),
});
}
new_plan
.validate()
.map_err(|source| OptimizerError::InvalidRuleOutput {
optimizer: "AQE",
rule: rule_name.clone(),
source,
})?;
if new_plan != current {
applied.push(rule_name);
current = new_plan;
}
}
}
if !input_is_streaming && !StreamingAqeGuard::plan_is_streaming(¤t) {
for rule in &self.guarded_rules {
let rule_name = rule.name().to_string();
let outcome =
catch_unwind(AssertUnwindSafe(|| rule.apply(¤t, effective_stats)))
.map_err(|payload| OptimizerError::RulePanicked {
optimizer: "AQE",
rule: rule_name.clone(),
message: krishiv_common::panic_payload_to_string(&*payload),
})?;
if let Some(new_plan) = outcome {
if new_plan.name() != current.name() || new_plan.kind() != current.kind() {
return Err(OptimizerError::InvalidRuleOutput {
optimizer: "AQE",
rule: rule_name,
source: PlanError::Validation(String::from(
"AQE rules must preserve plan name and execution kind",
)),
});
}
new_plan
.validate()
.map_err(|source| OptimizerError::InvalidRuleOutput {
optimizer: "AQE",
rule: rule_name.clone(),
source,
})?;
if new_plan != current {
applied.push(rule_name);
current = new_plan;
}
}
}
}
Ok((current, applied))
}
}
impl Default for AqeOptimizer {
fn default() -> Self {
Self::new()
}
}
pub fn default_logical_optimizer() -> Optimizer {
let mut optimizer = Optimizer::new();
optimizer.add_rule(Box::new(ConstantFoldingRule));
optimizer.add_rule(Box::new(PredicatePushdownRule));
optimizer.add_rule(Box::new(BroadcastAutoRule::new(
DEFAULT_BROADCAST_THRESHOLD_ROWS,
)));
optimizer.add_rule(Box::new(JoinReorderRule));
optimizer
}
pub fn default_aqe_optimizer() -> AqeOptimizer {
default_aqe_optimizer_with_parallelism(1)
}
pub fn default_aqe_optimizer_with_parallelism(min_partitions: usize) -> AqeOptimizer {
let mut optimizer = AqeOptimizer::new();
optimizer.add_guarded_rule(Box::new(BroadcastRuntimeRule::new(
DEFAULT_MAX_BROADCAST_BYTES,
)));
optimizer.add_guarded_rule(Box::new(AutoPartitionRule::new(64)));
optimizer.add_guarded_rule(Box::new(
CoalesceRule::new(64 * 1024 * 1024).with_min_partitions(min_partitions),
));
optimizer.add_guarded_rule(Box::new(SkewJoinRule::with_default_factor(
DEFAULT_SKEW_THRESHOLD,
)));
optimizer
}
pub fn default_aqe_optimizer_with_stats() -> AqeOptimizer {
default_aqe_optimizer_with_stats_and_parallelism(1)
}
pub fn default_aqe_optimizer_with_stats_and_parallelism(min_partitions: usize) -> AqeOptimizer {
default_aqe_optimizer_with_parallelism(min_partitions).with_cost_model(std::sync::Arc::new(
CboCostModel {
registry: stats::global_table_stats().clone(),
},
))
}