use vibesql_ast::{BinaryOperator, Expression};
use vibesql_storage::statistics::TableStatistics;
pub fn order_predicates_by_selectivity(
predicates: Vec<Expression>,
stats: &TableStatistics,
) -> Vec<Expression> {
if predicates.is_empty() {
return predicates;
}
let mut pred_with_selectivity: Vec<(f64, Expression)> = predicates
.into_iter()
.map(|pred| {
let selectivity = estimate_selectivity(&pred, stats);
(selectivity, pred)
})
.collect();
pred_with_selectivity
.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
pred_with_selectivity.into_iter().map(|(_, pred)| pred).collect()
}
pub fn estimate_selectivity(predicate: &Expression, stats: &TableStatistics) -> f64 {
match predicate {
Expression::Literal(vibesql_types::SqlValue::Boolean(true)) => 1.0,
Expression::Literal(vibesql_types::SqlValue::Boolean(false)) => 0.0,
Expression::BinaryOp { op: BinaryOperator::And, left, right } => {
let left_sel = estimate_selectivity(left.as_ref(), stats);
let right_sel = estimate_selectivity(right.as_ref(), stats);
left_sel * right_sel
}
Expression::BinaryOp { op: BinaryOperator::Or, left, right } => {
let left_sel = estimate_selectivity(left.as_ref(), stats);
let right_sel = estimate_selectivity(right.as_ref(), stats);
1.0 - ((1.0 - left_sel) * (1.0 - right_sel))
}
Expression::BinaryOp { op, left, right } => {
estimate_binary_op_selectivity(op, left, right, stats)
}
Expression::UnaryOp { op: vibesql_ast::UnaryOperator::Not, expr } => {
1.0 - estimate_selectivity(expr, stats)
}
Expression::IsNull { expr, negated: false } => estimate_is_null_selectivity(expr, stats),
Expression::IsNull { expr, negated: true } => {
1.0 - estimate_is_null_selectivity(expr, stats)
}
Expression::Between { .. } => 0.33,
Expression::InList { expr, values, .. } => {
estimate_in_list_selectivity(expr, values, stats)
}
Expression::Like { pattern, .. } => estimate_like_selectivity(pattern),
_ => 0.5,
}
}
fn estimate_binary_op_selectivity(
op: &BinaryOperator,
left: &Expression,
right: &Expression,
stats: &TableStatistics,
) -> f64 {
match op {
BinaryOperator::Equal => estimate_equality_selectivity(left, right, stats),
BinaryOperator::NotEqual => 1.0 - estimate_equality_selectivity(left, right, stats),
BinaryOperator::LessThan | BinaryOperator::GreaterThan => {
estimate_range_selectivity(op, left, right, stats)
}
BinaryOperator::LessThanOrEqual | BinaryOperator::GreaterThanOrEqual => {
estimate_range_selectivity(op, left, right, stats)
}
_ => 0.5, }
}
fn estimate_equality_selectivity(
left: &Expression,
right: &Expression,
stats: &TableStatistics,
) -> f64 {
let (column_name, literal_value) = match (left, right) {
(Expression::ColumnRef(col_id), Expression::Literal(val)) => {
(col_id.column_canonical(), val)
}
(Expression::Literal(val), Expression::ColumnRef(col_id)) => {
(col_id.column_canonical(), val)
}
_ => return 0.1, };
let col_stats = stats
.columns
.get(column_name)
.or_else(|| stats.columns.get(&column_name.to_uppercase()))
.or_else(|| stats.columns.get(&column_name.to_lowercase()));
if let Some(col_stats) = col_stats {
col_stats.estimate_eq_selectivity(literal_value)
} else {
0.1
}
}
fn estimate_range_selectivity(
op: &BinaryOperator,
left: &Expression,
right: &Expression,
stats: &TableStatistics,
) -> f64 {
let (column_name, literal_value, operator_str) = match (op, left, right) {
(BinaryOperator::LessThan, Expression::ColumnRef(col_id), Expression::Literal(val)) => {
(col_id.column_canonical(), val, "<")
}
(BinaryOperator::GreaterThan, Expression::ColumnRef(col_id), Expression::Literal(val)) => {
(col_id.column_canonical(), val, ">")
}
(
BinaryOperator::LessThanOrEqual,
Expression::ColumnRef(col_id),
Expression::Literal(val),
) => (col_id.column_canonical(), val, "<="),
(
BinaryOperator::GreaterThanOrEqual,
Expression::ColumnRef(col_id),
Expression::Literal(val),
) => (col_id.column_canonical(), val, ">="),
_ => return 0.33, };
let col_stats = stats
.columns
.get(column_name)
.or_else(|| stats.columns.get(&column_name.to_uppercase()))
.or_else(|| stats.columns.get(&column_name.to_lowercase()));
if let Some(col_stats) = col_stats {
col_stats.estimate_range_selectivity(literal_value, operator_str)
} else {
0.33
}
}
fn estimate_in_list_selectivity(
expr: &Expression,
values: &[Expression],
stats: &TableStatistics,
) -> f64 {
let column_name = match expr {
Expression::ColumnRef(col_id) => col_id.column_canonical(),
_ => return (values.len() as f64 / 100.0).min(1.0), };
let col_stats = stats
.columns
.get(column_name)
.or_else(|| stats.columns.get(&column_name.to_uppercase()))
.or_else(|| stats.columns.get(&column_name.to_lowercase()));
if let Some(col_stats) = col_stats {
let n_values = values.len() as f64;
let n_distinct = col_stats.n_distinct as f64;
if n_distinct > 0.0 {
(n_values / n_distinct).min(1.0)
} else {
(n_values / 100.0).min(1.0)
}
} else {
(values.len() as f64 / 100.0).min(1.0)
}
}
fn estimate_is_null_selectivity(expr: &Expression, stats: &TableStatistics) -> f64 {
let column_name = match expr {
Expression::ColumnRef(col_id) => col_id.column_canonical(),
_ => return 0.1, };
let col_stats = stats
.columns
.get(column_name)
.or_else(|| stats.columns.get(&column_name.to_uppercase()))
.or_else(|| stats.columns.get(&column_name.to_lowercase()));
if let Some(col_stats) = col_stats {
let total_rows = stats.row_count;
if total_rows > 0 {
col_stats.null_count as f64 / total_rows as f64
} else {
0.0
}
} else {
0.1
}
}
fn estimate_like_selectivity(pattern: &Expression) -> f64 {
let pattern_str = match pattern {
Expression::Literal(vibesql_types::SqlValue::Varchar(s)) => s,
_ => return 0.5, };
if pattern_str.starts_with('%') && pattern_str.ends_with('%') {
0.1
} else if pattern_str.starts_with('%') {
0.15
} else if pattern_str.ends_with('%') {
0.2
} else if pattern_str.contains('%') || pattern_str.contains('_') {
0.15
} else {
0.1
}
}
#[cfg(test)]
mod tests {
use vibesql_catalog::{ColumnSchema, TableSchema};
use vibesql_storage::Row;
use vibesql_types::{DataType, SqlValue};
use super::*;
fn create_test_stats() -> TableStatistics {
let schema = TableSchema::new(
"test_table".to_string(),
vec![
ColumnSchema::new("id".to_string(), DataType::Integer, false),
ColumnSchema::new(
"status".to_string(),
DataType::Varchar { max_length: Some(20) },
true,
),
],
);
let rows = vec![
Row::new(vec![SqlValue::Integer(1), SqlValue::Varchar(arcstr::ArcStr::from("active"))]),
Row::new(vec![
SqlValue::Integer(2),
SqlValue::Varchar(arcstr::ArcStr::from("inactive")),
]),
Row::new(vec![SqlValue::Integer(3), SqlValue::Varchar(arcstr::ArcStr::from("active"))]),
Row::new(vec![SqlValue::Integer(4), SqlValue::Varchar(arcstr::ArcStr::from("active"))]),
Row::new(vec![SqlValue::Integer(5), SqlValue::Null]),
];
TableStatistics::compute(&rows, &schema)
}
#[test]
fn test_equality_selectivity() {
let stats = create_test_stats();
let pred = Expression::BinaryOp {
op: BinaryOperator::Equal,
left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"status", false,
))),
right: Box::new(Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from("active")))),
};
let selectivity = estimate_selectivity(&pred, &stats);
assert!((selectivity - 0.75).abs() < 0.01);
}
#[test]
fn test_and_selectivity() {
let stats = create_test_stats();
let pred = Expression::BinaryOp {
op: BinaryOperator::And,
left: Box::new(Expression::BinaryOp {
op: BinaryOperator::Equal,
left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"status", false,
))),
right: Box::new(Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from(
"active",
)))),
}),
right: Box::new(Expression::BinaryOp {
op: BinaryOperator::LessThan,
left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"id", false,
))),
right: Box::new(Expression::Literal(SqlValue::Integer(10))),
}),
};
let selectivity = estimate_selectivity(&pred, &stats);
assert!(
(selectivity - 0.75).abs() < 0.01,
"Expected selectivity 0.75, got {}",
selectivity
);
}
#[test]
fn test_or_selectivity() {
let stats = create_test_stats();
let pred = Expression::BinaryOp {
op: BinaryOperator::Or,
left: Box::new(Expression::BinaryOp {
op: BinaryOperator::Equal,
left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"status", false,
))),
right: Box::new(Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from(
"active",
)))),
}),
right: Box::new(Expression::BinaryOp {
op: BinaryOperator::LessThan,
left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"id", false,
))),
right: Box::new(Expression::Literal(SqlValue::Integer(10))),
}),
};
let selectivity = estimate_selectivity(&pred, &stats);
assert!((selectivity - 1.0).abs() < 0.01, "Expected selectivity 1.0, got {}", selectivity);
}
#[test]
fn test_in_list_selectivity() {
let stats = create_test_stats();
let pred = Expression::InList {
expr: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"status", false,
))),
values: vec![
Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from("active"))),
Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from("inactive"))),
],
negated: false,
};
let selectivity = estimate_selectivity(&pred, &stats);
assert!(
(selectivity - 1.0).abs() < 0.01,
"Expected selectivity 1.0 for IN list matching all distinct values, got {}",
selectivity
);
let pred_single = Expression::InList {
expr: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"status", false,
))),
values: vec![Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from("active")))],
negated: false,
};
let selectivity_single = estimate_selectivity(&pred_single, &stats);
assert!(
(selectivity_single - 0.5).abs() < 0.01,
"Expected selectivity 0.5 for IN list with single value, got {}",
selectivity_single
);
}
#[test]
fn test_nested_and_or_selectivity() {
let stats = create_test_stats();
let pred = Expression::BinaryOp {
op: BinaryOperator::And,
left: Box::new(Expression::BinaryOp {
op: BinaryOperator::Or,
left: Box::new(Expression::BinaryOp {
op: BinaryOperator::Equal,
left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"status", false,
))),
right: Box::new(Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from(
"active",
)))),
}),
right: Box::new(Expression::BinaryOp {
op: BinaryOperator::Equal,
left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"status", false,
))),
right: Box::new(Expression::Literal(SqlValue::Varchar(arcstr::ArcStr::from(
"inactive",
)))),
}),
}),
right: Box::new(Expression::BinaryOp {
op: BinaryOperator::LessThan,
left: Box::new(Expression::ColumnRef(vibesql_ast::ColumnIdentifier::simple(
"id", false,
))),
right: Box::new(Expression::Literal(SqlValue::Integer(3))),
}),
};
let selectivity = estimate_selectivity(&pred, &stats);
assert!(
(0.25..=0.30).contains(&selectivity),
"Expected nested selectivity ~0.27, got {}",
selectivity
);
}
}