use rudb_common::rules::Rule;
use rudb_common::{LogicalType, PhysicalType, Result};
use rudb_plan::{ConjunctionOp, Expr, ExprRef, Node, NodeRef, Plan, Slice};
use crate::estimate::{self, Facts};
use crate::pass::{Context, Pass, top_down};
use crate::walk;
const WORTH_REWRITING: f64 = 0.2;
const CHEAPEST: f64 = 0.25;
#[derive(Debug)]
pub struct FilterOrder;
impl Pass for FilterOrder {
fn name(&self) -> &'static str {
"reorder_filter"
}
fn run(&self, plan: &mut Plan, context: &Context) -> Result<()> {
if !context.allows(Rule::FilterOrder) {
return Ok(());
}
for node in top_down(plan) {
let Node::Filter { input, predicate } = *plan.node(node) else { continue };
if let Some(ordered) = ordered(plan, input, predicate, context.facts()) {
let rewritten = plan.add_expr(
Expr::Conjunction { op: ConjunctionOp::And, children: ordered },
LogicalType::Boolean,
);
if let Node::Filter { predicate, .. } = plan.node_mut(node) {
*predicate = rewritten;
}
}
}
Ok(())
}
}
fn ordered(plan: &mut Plan, input: NodeRef, predicate: ExprRef, stats: &Facts) -> Option<Slice> {
let Expr::Conjunction { op: ConjunctionOp::And, children } = *plan.expr(predicate) else {
return None;
};
let parts = plan.expr_list(children).to_vec();
if parts.len() < 2 || parts.iter().any(|&part| walk::volatile(plan, part)) {
return None;
}
let mut measured = false;
let mut scored = Vec::with_capacity(parts.len());
for &part in &parts {
let (kept, from) = estimate::kept_by(plan, input, part, stats);
measured |= from != estimate::FROM_A_CONSTANT;
scored.push(Conjunct { part, kept, cost: cost(plan, part).max(CHEAPEST) });
}
if !measured {
return None;
}
let mut run = scored.clone();
run.sort_by(|left, right| right.rank().total_cmp(&left.rank()));
if modelled(&run) > modelled(&scored) * (1.0 - WORTH_REWRITING) {
return None;
}
let reordered: Vec<ExprRef> = run.iter().map(|conjunct| conjunct.part).collect();
Some(plan.add_expr_list(&reordered))
}
#[derive(Clone, Copy)]
struct Conjunct {
part: ExprRef,
kept: f64,
cost: f64,
}
impl Conjunct {
fn rank(&self) -> f64 {
(1.0 - self.kept) / self.cost
}
}
fn modelled(conjuncts: &[Conjunct]) -> f64 {
let mut total = 0.0;
let mut reaching = 1.0;
for conjunct in conjuncts {
total += reaching * conjunct.cost;
reaching *= conjunct.kept;
}
total
}
fn cost(plan: &Plan, expr: ExprRef) -> f64 {
match *plan.expr(expr) {
Expr::Column(_) => 0.0,
Expr::Constant(_) => 0.25,
Expr::Conjunction { children, .. } => summed(plan, children),
Expr::Cast { input, .. } => 2.0 * touching(plan.expr_type(input)) + cost(plan, input),
Expr::Compare { left, right, .. } => {
touching(plan.expr_type(left)) + cost(plan, left) + cost(plan, right)
}
Expr::Function { args, .. } => {
let widest = plan
.expr_list(args)
.iter()
.map(|&argument| touching(plan.expr_type(argument)))
.fold(1.0, f64::max);
4.0 * widest + summed(plan, args)
}
Expr::Case { arms, .. } => 4.0 * plan.arm_list(arms).len() as f64,
Expr::Aggregate { .. } | Expr::Window { .. } => 0.0,
Expr::LambdaParam(_) => 0.0,
Expr::Lambda { body, .. } => 4.0 + cost(plan, body),
}
}
fn summed(plan: &Plan, slice: Slice) -> f64 {
plan.expr_list(slice).iter().map(|&expr| cost(plan, expr)).sum()
}
fn touching(ty: &LogicalType) -> f64 {
match ty.physical() {
PhysicalType::Varlen => 4.0,
PhysicalType::List | PhysicalType::Array | PhysicalType::Struct => 8.0,
_ => 1.0,
}
}
#[cfg(test)]
mod tests {
use rudb_common::Provenance;
use rudb_common::rules::{Rule, Rules};
use rudb_plan::Plan;
use super::FilterOrder;
use crate::estimate::Facts;
use crate::pass::{Context, Pass};
fn filtered(predicate: &str) -> Plan {
let text = format!(
"Filter {predicate}\n \
Get memory.main.nation AS nation #0 [n_nationkey::BIGINT, n_comment::VARCHAR]\n"
);
Plan::parse(&text).unwrap_or_else(|error| panic!("{text} did not parse: {error}"))
}
fn counted() -> Context {
let mut facts = Facts::new();
facts.record("memory", "main", "nation", 25);
facts.record_distinct("memory", "main", "nation", "n_nationkey", 25, Provenance::Sketch);
let mut context = Context::new();
context.measure(std::sync::Arc::new(facts));
context
}
fn rewritten(plan: &mut Plan, context: &Context) -> String {
FilterOrder.run(plan, context).expect("the pass does not fail");
plan.to_string()
}
fn leading(text: &str) -> String {
let line = text.lines().find(|line| line.contains("Filter")).expect("a filter is printed");
line.split(" AND ").next().expect("a conjunction prints its parts").to_owned()
}
#[test]
fn the_conjunct_that_throws_most_away_per_unit_of_work_goes_in_front() {
let mut plan = filtered(
"((upper(#0.1::VARCHAR)::VARCHAR = 'X'::VARCHAR)::BOOLEAN \
AND (#0.0::BIGINT = 3::BIGINT)::BOOLEAN)::BOOLEAN",
);
let text = rewritten(&mut plan, &counted());
assert!(
leading(&text).contains("#0.0"),
"the counted equality is both cheaper and more selective: {text}"
);
}
#[test]
fn a_predicate_already_in_the_best_order_is_left_alone() {
let predicate = "((#0.0::BIGINT = 3::BIGINT)::BOOLEAN \
AND (upper(#0.1::VARCHAR)::VARCHAR = 'X'::VARCHAR)::BOOLEAN)::BOOLEAN";
let mut plan = filtered(predicate);
let before = plan.to_string();
let text = rewritten(&mut plan, &counted());
assert_eq!(text, before, "there was nothing to move");
}
#[test]
fn two_conjuncts_nobody_counted_are_left_in_the_order_they_were_written() {
let mut plan = filtered(
"((upper(#0.1::VARCHAR)::VARCHAR = 'X'::VARCHAR)::BOOLEAN \
AND (#0.1::VARCHAR = 'y'::VARCHAR)::BOOLEAN)::BOOLEAN",
);
let before = plan.to_string();
let text = rewritten(&mut plan, &Context::new());
assert_eq!(text, before, "no conjunct here was measured");
}
#[test]
fn a_saving_below_the_threshold_is_not_worth_rewriting_the_predicate_for() {
let mut plan = filtered(
"((#0.0::BIGINT = 3::BIGINT)::BOOLEAN \
AND (#0.0::BIGINT = 4::BIGINT)::BOOLEAN)::BOOLEAN",
);
let before = plan.to_string();
let text = rewritten(&mut plan, &counted());
assert_eq!(text, before, "the two orders cost the same");
}
#[test]
fn the_rule_turns_the_whole_pass_off() {
let mut plan = filtered(
"((upper(#0.1::VARCHAR)::VARCHAR = 'X'::VARCHAR)::BOOLEAN \
AND (#0.0::BIGINT = 3::BIGINT)::BOOLEAN)::BOOLEAN",
);
let before = plan.to_string();
let mut rules = Rules::new();
rules.set(Rule::FilterOrder, false);
let mut context = counted();
context.govern(rules);
let text = rewritten(&mut plan, &context);
assert_eq!(text, before, "the setting is the whole of the switch");
}
#[test]
fn running_the_pass_twice_gives_the_same_plan() {
let mut plan = filtered(
"((upper(#0.1::VARCHAR)::VARCHAR = 'X'::VARCHAR)::BOOLEAN \
AND (#0.0::BIGINT = 3::BIGINT)::BOOLEAN)::BOOLEAN",
);
let context = counted();
let once = rewritten(&mut plan, &context);
let twice = rewritten(&mut plan, &context);
assert_eq!(once, twice, "a predicate this pass ordered is already in order");
}
}