fix ephemeral

This commit is contained in:
Jussi Saurio
2025-05-09 23:42:11 +03:00
parent 3b1aef4a9e
commit de9e8442e8

View File

@@ -4,7 +4,7 @@ use limbo_sqlite3_parser::ast::{self, Expr, SortOrder};
use crate::{
parameters::PARAM_PREFIX,
schema::{Index, IndexColumn, Schema, Table},
schema::{Index, IndexColumn, Schema},
translate::plan::TerminationKey,
types::SeekOp,
util::exprs_are_equivalent,
@@ -237,7 +237,12 @@ impl<'a> AccessMethod<'a> {
}
}
pub fn set_constraints(&mut self, index: Option<Arc<Index>>, constraints: &'a [Constraint]) {
pub fn set_constraints(&mut self, lookup: &ConstraintLookup, constraints: &'a [Constraint]) {
let index = match lookup {
ConstraintLookup::Index(index) => Some(index),
ConstraintLookup::Rowid => None,
ConstraintLookup::EphemeralIndex => panic!("set_constraints called with Lookup::None"),
};
match (&mut self.kind, constraints.is_empty()) {
(
AccessMethodKind::Search {
@@ -248,23 +253,23 @@ impl<'a> AccessMethod<'a> {
false,
) => {
*constraints = constraints;
*i = index;
*i = index.cloned();
}
(AccessMethodKind::Search { iter_dir, .. }, true) => {
self.kind = AccessMethodKind::Scan {
index,
index: index.cloned(),
iter_dir: *iter_dir,
};
}
(AccessMethodKind::Scan { iter_dir, .. }, false) => {
self.kind = AccessMethodKind::Search {
index,
index: index.cloned(),
iter_dir: *iter_dir,
constraints,
};
}
(AccessMethodKind::Scan { index: i, .. }, true) => {
*i = index;
*i = index.cloned();
}
}
}
@@ -983,13 +988,25 @@ fn use_indexes(
let (best_access_methods, best_table_numbers) =
(best_plan.best_access_methods, best_plan.table_numbers);
let best_join_order: Vec<JoinOrderMember> = best_table_numbers
.into_iter()
.map(|table_number| JoinOrderMember {
table_no: table_number,
is_outer: table_references[table_number]
.join_info
.as_ref()
.map_or(false, |join_info| join_info.outer),
})
.collect();
let mut to_remove_from_where_clause = vec![];
for (i, table_number) in best_table_numbers.iter().enumerate() {
for (i, join_order_member) in best_join_order.iter().enumerate() {
let table_number = join_order_member.table_no;
let access_method_kind = access_methods_arena.borrow()[best_access_methods[i]]
.kind
.clone();
if matches!(
table_references[*table_number].op,
table_references[table_number].op,
Operation::Subquery { .. }
) {
// FIXME: Operation::Subquery shouldn't exist. It's not an operation, it's a kind of temporary table.
@@ -997,12 +1014,49 @@ fn use_indexes(
matches!(access_method_kind, AccessMethodKind::Scan { index: None, .. }),
"nothing in the current optimizer should be able to optimize subqueries, but got {:?} for table {}",
access_method_kind,
table_references[*table_number].table.get_name()
table_references[table_number].table.get_name()
);
continue;
}
table_references[*table_number].op = match access_method_kind {
AccessMethodKind::Scan { iter_dir, index } => Operation::Scan { iter_dir, index },
table_references[table_number].op = match access_method_kind {
AccessMethodKind::Scan { iter_dir, index } => {
if index.is_some() || i == 0 {
Operation::Scan { iter_dir, index }
} else {
// Try to construct ephemeral index since it's going to be better than a scan for non-outermost tables.
let unindexable_constraints = constraints.iter().find(|c| {
c.table_no == table_number
&& matches!(c.lookup, ConstraintLookup::EphemeralIndex)
});
if let Some(unindexable) = unindexable_constraints {
let usable_constraints = usable_constraints_for_join_order(
&unindexable.constraints,
table_number,
&best_join_order[..=i],
);
if usable_constraints.is_empty() {
Operation::Scan { iter_dir, index }
} else {
let ephemeral_index = ephemeral_index_build(
&table_references[table_number],
table_number,
&usable_constraints,
);
let ephemeral_index = Arc::new(ephemeral_index);
Operation::Search(Search::Seek {
index: Some(ephemeral_index),
seek_def: build_seek_def_from_constraints(
usable_constraints,
iter_dir,
where_clause,
)?,
})
}
} else {
Operation::Scan { iter_dir, index }
}
}
}
AccessMethodKind::Search {
index,
constraints,
@@ -1010,7 +1064,7 @@ fn use_indexes(
} => {
assert!(!constraints.is_empty());
for constraint in constraints.iter() {
to_remove_from_where_clause.push(constraint.where_clause_position.0);
to_remove_from_where_clause.push(constraint.where_clause_pos.0);
}
if let Some(index) = index {
Operation::Search(Search::Seek {
@@ -1030,7 +1084,7 @@ fn use_indexes(
match constraints[0].operator {
ast::Operator::Equals => Operation::Search(Search::RowidEq {
cmp_expr: {
let (idx, side) = constraints[0].where_clause_position;
let (idx, side) = constraints[0].where_clause_pos;
let ast::Expr::Binary(lhs, _, rhs) =
unwrap_parens(&where_clause[idx].expr)?
else {
@@ -1063,16 +1117,7 @@ fn use_indexes(
for position in to_remove_from_where_clause.iter().rev() {
where_clause.remove(*position);
}
let best_join_order = best_table_numbers
.into_iter()
.map(|table_number| JoinOrderMember {
table_no: table_number,
is_outer: table_references[table_number]
.join_info
.as_ref()
.map_or(false, |join_info| join_info.outer),
})
.collect();
Ok(Some(best_join_order))
}
@@ -1679,17 +1724,18 @@ pub fn find_best_access_method_for_join_order<'a>(
.iter()
.filter(|csmap| csmap.table_no == table_index)
{
let index_info = match csmap.index.as_ref() {
Some(index) => IndexInfo {
let index_info = match &csmap.lookup {
ConstraintLookup::Index(index) => IndexInfo {
unique: index.unique,
covering: table_reference.index_is_covering(index),
column_count: index.columns.len(),
},
None => IndexInfo {
ConstraintLookup::Rowid => IndexInfo {
unique: true, // rowids are always unique
covering: false,
column_count: 1,
},
ConstraintLookup::EphemeralIndex => continue,
};
let usable_constraints =
usable_constraints_for_join_order(&csmap.constraints, table_index, join_order);
@@ -1706,11 +1752,14 @@ pub fn find_best_access_method_for_join_order<'a>(
for i in 0..order_target.0.len().min(index_info.column_count) {
let correct_table = order_target.0[i].table_no == table_index;
let correct_column = {
match csmap.index.as_ref() {
Some(index) => index.columns[i].pos_in_table == order_target.0[i].column_no,
None => {
match &csmap.lookup {
ConstraintLookup::Index(index) => {
index.columns[i].pos_in_table == order_target.0[i].column_no
}
ConstraintLookup::Rowid => {
rowid_column_idx.map_or(false, |idx| idx == order_target.0[i].column_no)
}
ConstraintLookup::EphemeralIndex => unreachable!(),
}
};
if !correct_table || !correct_column {
@@ -1719,9 +1768,12 @@ pub fn find_best_access_method_for_join_order<'a>(
break;
}
let correct_order = {
match csmap.index.as_ref() {
Some(index) => order_target.0[i].order == index.columns[i].order,
None => order_target.0[i].order == SortOrder::Asc,
match &csmap.lookup {
ConstraintLookup::Index(index) => {
order_target.0[i].order == index.columns[i].order
}
ConstraintLookup::Rowid => order_target.0[i].order == SortOrder::Asc,
ConstraintLookup::EphemeralIndex => unreachable!(),
}
};
if correct_order {
@@ -1740,33 +1792,10 @@ pub fn find_best_access_method_for_join_order<'a>(
};
if cost.total() < best_access_method.cost.total() + order_satisfiability_bonus {
best_access_method.cost = cost;
best_access_method.set_constraints(csmap.index.clone(), &usable_constraints);
best_access_method.set_constraints(&csmap.lookup, &usable_constraints);
}
}
// FIXME: ephemeral indexes are disabled for now.
// These constraints need to be computed ad hoc.
// // We haven't found a persistent btree index that is any better than a full table scan;
// // let's see if building an ephemeral index would be better.
// if best_index.index.is_none() && matches!(table_reference.table, Table::BTree(_)) {
// let (ephemeral_cost, constraints_with_col_idx) = ephemeral_index_estimate_cost(
// where_clause,
// table_reference,
// loop_index,
// table_index,
// join_order,
// input_cardinality,
// )?;
// if ephemeral_cost.total() < best_index.cost.total() {
// // ephemeral index makes sense, so let's build it now.
// // ephemeral columns are: columns from the table_reference, constraints first, then the rest
// let ephemeral_index =
// ephemeral_index_build(table_reference, table_index, &constraints_with_col_idx);
// best_index.index = Some(Arc::new(ephemeral_index));
// best_index.cost = ephemeral_cost;
// }
// }
let iter_dir = if let Some(order_target) = maybe_order_target {
// if index columns match the order target columns in the exact reverse directions, then we should use IterationDirection::Backwards
let index = match &best_access_method.kind {
@@ -1813,114 +1842,10 @@ pub fn find_best_access_method_for_join_order<'a>(
Ok(best_access_method)
}
// TODO get rid of doing this every time
fn ephemeral_index_estimate_cost(
where_clause: &[WhereTerm],
table_reference: &TableReference,
loop_index: usize,
table_index: usize,
join_order: &[JoinOrderMember],
input_cardinality: f64,
) -> Result<(ScanCost, Vec<(usize, Constraint)>)> {
let mut constraints_with_col_idx: Vec<(usize, Constraint)> = where_clause
.iter()
.enumerate()
.filter(|(_, term)| is_potential_index_constraint(term, loop_index, join_order))
.filter_map(|(i, term)| {
let Ok(ast::Expr::Binary(lhs, operator, rhs)) = unwrap_parens(&term.expr) else {
panic!("expected binary expression");
};
if let ast::Expr::Column { table, column, .. } = lhs.as_ref() {
if *table == table_index {
let Ok(constraining_table_mask) = table_mask_from_expr(rhs) else {
return None;
};
return Some((
*column,
Constraint {
where_clause_position: (i, BinaryExprSide::Rhs),
operator: *operator,
key_position: *column,
sort_order: SortOrder::Asc,
lhs_mask: constraining_table_mask,
},
));
}
}
if let ast::Expr::Column { table, column, .. } = rhs.as_ref() {
if *table == table_index {
let Ok(constraining_table_mask) = table_mask_from_expr(lhs) else {
return None;
};
return Some((
*column,
Constraint {
where_clause_position: (i, BinaryExprSide::Lhs),
operator: opposite_cmp_op(*operator),
key_position: *column,
sort_order: SortOrder::Asc,
lhs_mask: constraining_table_mask,
},
));
}
}
None
})
.collect();
// sort equalities first
constraints_with_col_idx.sort_by(|a, _| {
if a.1.operator == ast::Operator::Equals {
Ordering::Less
} else {
Ordering::Equal
}
});
// drop everything after the first inequality
constraints_with_col_idx.truncate(
constraints_with_col_idx
.iter()
.position(|c| c.1.operator != ast::Operator::Equals)
.unwrap_or(constraints_with_col_idx.len()),
);
if constraints_with_col_idx.is_empty() {
return Ok((
ScanCost {
run_cost: Cost(0.0),
build_cost: Cost(f64::MAX),
},
vec![],
));
}
let ephemeral_column_count = table_reference
.columns()
.iter()
.enumerate()
.filter(|(i, _)| table_reference.column_is_used(*i))
.count();
let constraints_without_col_idx = constraints_with_col_idx
.iter()
.cloned()
.map(|(_, c)| c)
.collect::<Vec<Constraint>>();
let ephemeral_cost = estimate_cost_for_scan_or_seek(
Some(IndexInfo {
unique: false,
column_count: ephemeral_column_count,
covering: false,
}),
&constraints_without_col_idx,
true,
input_cardinality,
);
Ok((ephemeral_cost, constraints_with_col_idx))
}
fn ephemeral_index_build(
table_reference: &TableReference,
table_index: usize,
constraints: &[(usize, Constraint)],
constraints: &[Constraint],
) -> Index {
let mut ephemeral_columns: Vec<IndexColumn> = table_reference
.columns()
@@ -1939,11 +1864,11 @@ fn ephemeral_index_build(
let a_constraint = constraints
.iter()
.enumerate()
.find(|(_, c)| c.0 == a.pos_in_table);
.find(|(_, c)| c.table_col_pos == a.pos_in_table);
let b_constraint = constraints
.iter()
.enumerate()
.find(|(_, c)| c.0 == b.pos_in_table);
.find(|(_, c)| c.table_col_pos == b.pos_in_table);
match (a_constraint, b_constraint) {
(Some(_), None) => Ordering::Less,
(None, Some(_)) => Ordering::Greater,
@@ -1971,12 +1896,14 @@ fn ephemeral_index_build(
pub struct Constraint {
/// The position of the constraint in the WHERE clause, e.g. in SELECT * FROM t WHERE true AND t.x = 10, the position is (1, BinaryExprSide::Rhs),
/// since the RHS '10' is the constraining expression and it's part of the second term in the WHERE clause.
where_clause_position: (usize, BinaryExprSide),
where_clause_pos: (usize, BinaryExprSide),
/// The operator of the constraint, e.g. =, >, <
operator: ast::Operator,
/// The position of the index column in the index, e.g. if the index is (a,b,c) and the constraint is on b, then index_column_pos is 1.
/// For Rowid constraints this is always 0.
key_position: usize,
index_col_pos: usize,
/// The position of the constrained column in the table.
table_col_pos: usize,
/// The sort order of the index column, ASC or DESC. For Rowid constraints this is always ASC.
sort_order: SortOrder,
/// Bitmask of tables that are required to be on the left side of the constrained table,
@@ -1984,15 +1911,28 @@ pub struct Constraint {
lhs_mask: TableMask,
}
#[derive(Debug, Clone)]
/// Lookup denotes how a given set of [Constraint]s can be used to access a table.
///
/// Lookup::Index(index) means that the constraints can be used to access the table using the given index.
/// Lookup::Rowid means that the constraints can be used to access the table using the table's rowid column.
/// Lookup::EphemeralIndex means that the constraints are not useful for accessing the table,
/// but an ephemeral index can be built ad-hoc to use them.
pub enum ConstraintLookup {
Index(Arc<Index>),
Rowid,
EphemeralIndex,
}
#[derive(Debug)]
/// A collection of [Constraint]s for a given (table, index) pair.
pub struct Constraints {
index: Option<Arc<Index>>,
lookup: ConstraintLookup,
table_no: usize,
constraints: Vec<Constraint>,
}
/// Precompute all potentially usable constraints from a WHERE clause.
/// Precompute all potentially usable [Constraints] from a WHERE clause.
/// The resulting list of [Constraints] is then used to evaluate the best access methods for various join orders.
pub fn constraints_from_where_clause(
where_clause: &[WhereTerm],
@@ -2006,131 +1946,173 @@ pub fn constraints_from_where_clause(
.iter()
.position(|c| c.is_rowid_alias);
if let Some(rowid_alias_column) = rowid_alias_column {
let mut cs = Constraints {
index: None,
table_no,
constraints: Vec::new(),
let mut cs = Constraints {
lookup: ConstraintLookup::Rowid,
table_no,
constraints: Vec::new(),
};
let mut cs_ephemeral = Constraints {
lookup: ConstraintLookup::EphemeralIndex,
table_no,
constraints: Vec::new(),
};
for (i, term) in where_clause.iter().enumerate() {
let ast::Expr::Binary(lhs, operator, rhs) = unwrap_parens(&term.expr)? else {
continue;
};
for (i, term) in where_clause.iter().enumerate() {
let ast::Expr::Binary(lhs, operator, rhs) = unwrap_parens(&term.expr)? else {
continue;
};
if !matches!(
operator,
ast::Operator::Equals
| ast::Operator::Greater
| ast::Operator::Less
| ast::Operator::GreaterEquals
| ast::Operator::LessEquals
) {
continue;
}
if let Some(outer_join_tbl) = term.from_outer_join {
if outer_join_tbl != table_no {
continue;
}
}
match lhs.as_ref() {
ast::Expr::Column { table, column, .. } => {
if *table == table_no && *column == rowid_alias_column {
cs.constraints.push(Constraint {
where_clause_position: (i, BinaryExprSide::Rhs),
operator: *operator,
key_position: 0,
sort_order: SortOrder::Asc,
lhs_mask: table_mask_from_expr(rhs)?,
});
}
}
ast::Expr::RowId { table, .. } => {
if *table == table_no {
cs.constraints.push(Constraint {
where_clause_position: (i, BinaryExprSide::Rhs),
operator: *operator,
key_position: 0,
sort_order: SortOrder::Asc,
lhs_mask: table_mask_from_expr(rhs)?,
});
}
}
_ => {}
};
match rhs.as_ref() {
ast::Expr::Column { table, column, .. } => {
if *table == table_no && *column == rowid_alias_column {
cs.constraints.push(Constraint {
where_clause_position: (i, BinaryExprSide::Lhs),
operator: opposite_cmp_op(*operator),
key_position: 0,
sort_order: SortOrder::Asc,
lhs_mask: table_mask_from_expr(lhs)?,
});
}
}
ast::Expr::RowId { table, .. } => {
if *table == table_no {
cs.constraints.push(Constraint {
where_clause_position: (i, BinaryExprSide::Lhs),
operator: opposite_cmp_op(*operator),
key_position: 0,
sort_order: SortOrder::Asc,
lhs_mask: table_mask_from_expr(lhs)?,
});
}
}
_ => {}
};
if !matches!(
operator,
ast::Operator::Equals
| ast::Operator::Greater
| ast::Operator::Less
| ast::Operator::GreaterEquals
| ast::Operator::LessEquals
) {
continue;
}
// First sort by position, with equalities first within each position
cs.constraints.sort_by(|a, b| {
let pos_cmp = a.key_position.cmp(&b.key_position);
if pos_cmp == Ordering::Equal {
// If same position, sort equalities first
if a.operator == ast::Operator::Equals {
Ordering::Less
} else if b.operator == ast::Operator::Equals {
Ordering::Greater
} else {
Ordering::Equal
if let Some(outer_join_tbl) = term.from_outer_join {
if outer_join_tbl != table_no {
continue;
}
}
match lhs.as_ref() {
ast::Expr::Column { table, column, .. } => {
if *table == table_no {
if rowid_alias_column.map_or(false, |idx| *column == idx) {
cs.constraints.push(Constraint {
where_clause_pos: (i, BinaryExprSide::Rhs),
operator: *operator,
index_col_pos: 0,
table_col_pos: rowid_alias_column.unwrap(),
sort_order: SortOrder::Asc,
lhs_mask: table_mask_from_expr(rhs)?,
});
} else {
cs_ephemeral.constraints.push(Constraint {
where_clause_pos: (i, BinaryExprSide::Rhs),
operator: *operator,
index_col_pos: 0,
table_col_pos: *column,
sort_order: SortOrder::Asc,
lhs_mask: table_mask_from_expr(rhs)?,
});
}
}
}
ast::Expr::RowId { table, .. } => {
if *table == table_no && rowid_alias_column.is_some() {
cs.constraints.push(Constraint {
where_clause_pos: (i, BinaryExprSide::Rhs),
operator: *operator,
index_col_pos: 0,
table_col_pos: rowid_alias_column.unwrap(),
sort_order: SortOrder::Asc,
lhs_mask: table_mask_from_expr(rhs)?,
});
}
}
_ => {}
};
match rhs.as_ref() {
ast::Expr::Column { table, column, .. } => {
if *table == table_no {
if rowid_alias_column.map_or(false, |idx| *column == idx) {
cs.constraints.push(Constraint {
where_clause_pos: (i, BinaryExprSide::Lhs),
operator: opposite_cmp_op(*operator),
index_col_pos: 0,
table_col_pos: rowid_alias_column.unwrap(),
sort_order: SortOrder::Asc,
lhs_mask: table_mask_from_expr(lhs)?,
});
} else {
cs_ephemeral.constraints.push(Constraint {
where_clause_pos: (i, BinaryExprSide::Lhs),
operator: opposite_cmp_op(*operator),
index_col_pos: 0,
table_col_pos: *column,
sort_order: SortOrder::Asc,
lhs_mask: table_mask_from_expr(lhs)?,
});
}
}
}
ast::Expr::RowId { table, .. } => {
if *table == table_no && rowid_alias_column.is_some() {
cs.constraints.push(Constraint {
where_clause_pos: (i, BinaryExprSide::Lhs),
operator: opposite_cmp_op(*operator),
index_col_pos: 0,
table_col_pos: rowid_alias_column.unwrap(),
sort_order: SortOrder::Asc,
lhs_mask: table_mask_from_expr(lhs)?,
});
}
}
_ => {}
};
}
// First sort by position, with equalities first within each position
cs.constraints.sort_by(|a, b| {
let pos_cmp = a.index_col_pos.cmp(&b.index_col_pos);
if pos_cmp == Ordering::Equal {
// If same position, sort equalities first
if a.operator == ast::Operator::Equals {
Ordering::Less
} else if b.operator == ast::Operator::Equals {
Ordering::Greater
} else {
pos_cmp
Ordering::Equal
}
});
} else {
pos_cmp
}
});
cs_ephemeral.constraints.sort_by(|a, b| {
if a.operator == ast::Operator::Equals {
Ordering::Less
} else if b.operator == ast::Operator::Equals {
Ordering::Greater
} else {
Ordering::Equal
}
});
// Deduplicate by position, keeping first occurrence (which will be equality if one exists)
cs.constraints.dedup_by_key(|c| c.key_position);
// Deduplicate by position, keeping first occurrence (which will be equality if one exists)
cs.constraints.dedup_by_key(|c| c.index_col_pos);
// Truncate at first gap in positions
let mut last_pos = 0;
let mut i = 0;
for constraint in cs.constraints.iter() {
if constraint.key_position != last_pos {
if constraint.key_position != last_pos + 1 {
break;
}
last_pos = constraint.key_position;
// Truncate at first gap in positions
let mut last_pos = 0;
let mut i = 0;
for constraint in cs.constraints.iter() {
if constraint.index_col_pos != last_pos {
if constraint.index_col_pos != last_pos + 1 {
break;
}
i += 1;
last_pos = constraint.index_col_pos;
}
cs.constraints.truncate(i);
i += 1;
}
cs.constraints.truncate(i);
// Truncate after the first inequality
if let Some(first_inequality) = cs
.constraints
.iter()
.position(|c| c.operator != ast::Operator::Equals)
{
cs.constraints.truncate(first_inequality + 1);
}
// Truncate after the first inequality
if let Some(first_inequality) = cs
.constraints
.iter()
.position(|c| c.operator != ast::Operator::Equals)
{
cs.constraints.truncate(first_inequality + 1);
}
if rowid_alias_column.is_some() {
constraints.push(cs);
}
constraints.push(cs_ephemeral);
let indexes = available_indexes.get(table_reference.table.get_name());
if let Some(indexes) = indexes {
for index in indexes {
let mut cs = Constraints {
index: Some(index.clone()),
lookup: ConstraintLookup::Index(index.clone()),
table_no,
constraints: Vec::new(),
};
@@ -2157,9 +2139,15 @@ pub fn constraints_from_where_clause(
get_column_position_in_index(lhs, table_no, index)?
{
cs.constraints.push(Constraint {
where_clause_position: (i, BinaryExprSide::Rhs),
where_clause_pos: (i, BinaryExprSide::Rhs),
operator: *operator,
key_position: position_in_index,
index_col_pos: position_in_index,
table_col_pos: {
let ast::Expr::Column { column, .. } = lhs.as_ref() else {
crate::bail_parse_error!("expected column in index constraint");
};
*column
},
sort_order: index.columns[position_in_index].order,
lhs_mask: table_mask_from_expr(rhs)?,
});
@@ -2168,9 +2156,15 @@ pub fn constraints_from_where_clause(
get_column_position_in_index(rhs, table_no, index)?
{
cs.constraints.push(Constraint {
where_clause_position: (i, BinaryExprSide::Lhs),
where_clause_pos: (i, BinaryExprSide::Lhs),
operator: opposite_cmp_op(*operator),
key_position: position_in_index,
index_col_pos: position_in_index,
table_col_pos: {
let ast::Expr::Column { column, .. } = rhs.as_ref() else {
crate::bail_parse_error!("expected column in index constraint");
};
*column
},
sort_order: index.columns[position_in_index].order,
lhs_mask: table_mask_from_expr(lhs)?,
});
@@ -2178,7 +2172,7 @@ pub fn constraints_from_where_clause(
}
// First sort by position, with equalities first within each position
cs.constraints.sort_by(|a, b| {
let pos_cmp = a.key_position.cmp(&b.key_position);
let pos_cmp = a.index_col_pos.cmp(&b.index_col_pos);
if pos_cmp == Ordering::Equal {
// If same position, sort equalities first
if a.operator == ast::Operator::Equals {
@@ -2194,17 +2188,17 @@ pub fn constraints_from_where_clause(
});
// Deduplicate by position, keeping first occurrence (which will be equality if one exists)
cs.constraints.dedup_by_key(|c| c.key_position);
cs.constraints.dedup_by_key(|c| c.index_col_pos);
// Truncate at first gap in positions
let mut last_pos = 0;
let mut i = 0;
for constraint in cs.constraints.iter() {
if constraint.key_position != last_pos {
if constraint.key_position != last_pos + 1 {
if constraint.index_col_pos != last_pos {
if constraint.index_col_pos != last_pos + 1 {
break;
}
last_pos = constraint.key_position;
last_pos = constraint.index_col_pos;
}
i += 1;
}
@@ -2222,6 +2216,7 @@ pub fn constraints_from_where_clause(
}
}
}
Ok(constraints)
}
@@ -2350,7 +2345,7 @@ pub fn build_seek_def_from_constraints(
for constraint in constraints {
// Extract the other expression from the binary WhereTerm (i.e. the one being compared to the index column)
let (idx, side) = constraint.where_clause_position;
let (idx, side) = constraint.where_clause_pos;
let where_term = &where_clause[idx];
let ast::Expr::Binary(lhs, _, rhs) = unwrap_parens(where_term.expr.clone())? else {
crate::bail_parse_error!("expected binary expression");
@@ -3051,7 +3046,7 @@ mod tests {
iter_dir,
constraints,
}
if *iter_dir == IterationDirection::Forwards && constraints.len() == 1 && constraints[0].where_clause_position == (0, BinaryExprSide::Rhs),
if *iter_dir == IterationDirection::Forwards && constraints.len() == 1 && constraints[0].where_clause_pos == (0, BinaryExprSide::Rhs),
),
"expected rowid eq access method, got {:?}",
access_methods_arena.borrow()[best_plan.best_access_methods[0]].kind
@@ -3113,7 +3108,7 @@ mod tests {
iter_dir,
constraints,
}
if *iter_dir == IterationDirection::Forwards && constraints.len() == 1 && constraints[0].where_clause_position == (0, BinaryExprSide::Rhs),
if *iter_dir == IterationDirection::Forwards && constraints.len() == 1 && constraints[0].lhs_mask.is_empty() && index.name == "sqlite_autoindex_test_table_1"
),
"expected index search access method, got {:?}",
access_methods_arena.borrow()[best_plan.best_access_methods[0]].kind
@@ -3194,7 +3189,7 @@ mod tests {
iter_dir,
constraints,
}
if *iter_dir == IterationDirection::Forwards && constraints.len() == 1 && constraints[0].where_clause_position == (0, BinaryExprSide::Rhs) && index.name == "index1",
if *iter_dir == IterationDirection::Forwards && constraints.len() == 1 && constraints[0].where_clause_pos == (0, BinaryExprSide::Rhs) && index.name == "index1",
),
"expected Search access method, got {:?}",
access_methods_arena.borrow()[best_plan.best_access_methods[1]].kind
@@ -3410,84 +3405,6 @@ mod tests {
}
}
#[test]
/// Test that [compute_best_join_order] faces a query with no indexes,
/// it chooses the outer table based on the most restrictive filter,
/// and builds an ephemeral index on the inner table.
fn test_join_order_no_indexes_inner_ephemeral() {
let t1 = _create_btree_table("t1", _create_column_list(&["id", "foo"], Type::Integer));
let t2 = _create_btree_table("t2", _create_column_list(&["id", "foo"], Type::Integer));
let mut table_references = vec![
_create_table_reference(t1.clone(), None),
_create_table_reference(
t2.clone(),
Some(JoinInfo {
outer: false,
using: None,
}),
),
];
// SELECT * FROM t1 JOIN t2 ON t1.id = t2.id WHERE t2.foo > 10
let where_clause = vec![
// t2.foo > 10
// this should make the optimizer choose t2 as the outer table despite being inner in the query,
// because it restricts the output of t2 to a smaller set of rows, resulting in a cheaper plan.
_create_binary_expr(
_create_column_expr(1, 1, false), // table 1, column 1 (foo)
ast::Operator::Greater,
_create_numeric_literal("10"),
),
// t1.id = t2.id
// this should make the optimizer choose to create an ephemeral index on t1
// because it is cheaper than a table scan, despite the cost of building the index.
_create_binary_expr(
_create_column_expr(0, 0, false), // table 0, column 0 (id)
ast::Operator::Equals,
_create_column_expr(1, 0, false), // table 1, column 0 (id)
),
];
let available_indexes = HashMap::new();
let access_methods_arena = RefCell::new(Vec::new());
let constraints =
constraints_from_where_clause(&where_clause, &table_references, &available_indexes)
.unwrap();
let BestJoinOrderResult { best_plan, .. } = compute_best_join_order(
&mut table_references,
&where_clause,
None,
&constraints,
&access_methods_arena,
)
.unwrap()
.unwrap();
// Verify that t2 is chosen first due to its filter
assert_eq!(best_plan.table_numbers[0], 1, "best_plan: {:?}", best_plan);
// Verify table scan is used since there are no indexes
assert!(matches!(
access_methods_arena.borrow()[best_plan.best_access_methods[0]].kind,
AccessMethodKind::Scan { index: None, iter_dir }
if iter_dir == IterationDirection::Forwards
));
// Verify that an ephemeral index was built on t1
assert!(
matches!(
&access_methods_arena.borrow()[best_plan.best_access_methods[1]].kind,
AccessMethodKind::Search {
index: Some(index),
iter_dir,
constraints,
}
if *iter_dir == IterationDirection::Forwards && constraints.len() == 1 && constraints[0].where_clause_position == (0, BinaryExprSide::Rhs) && index.ephemeral
),
"expected ephemeral index, got {:?}",
access_methods_arena.borrow()[best_plan.best_access_methods[1]].kind
);
}
#[test]
fn test_join_order_three_tables_no_indexes() {
let t1 = _create_btree_table("t1", _create_column_list(&["id", "foo"], Type::Integer));