From c02d3f8bcdb1bd90593a0a214604fc91bd179d07 Mon Sep 17 00:00:00 2001 From: Jussi Saurio Date: Sat, 3 May 2025 13:01:25 +0300 Subject: [PATCH] Do groupby/orderby sort elimination based on optimizer decision --- core/translate/optimizer.rs | 410 +++++++++++++++++------------------- core/translate/plan.rs | 3 +- testing/groupby.test | 6 + testing/orderby.test | 76 +++---- 4 files changed, 243 insertions(+), 252 deletions(-) diff --git a/core/translate/optimizer.rs b/core/translate/optimizer.rs index 9e0a019f3..701b865a8 100644 --- a/core/translate/optimizer.rs +++ b/core/translate/optimizer.rs @@ -4,7 +4,7 @@ use limbo_sqlite3_parser::ast::{self, Expr, SortOrder}; use crate::{ parameters::PARAM_PREFIX, - schema::{BTreeTable, Column, Index, IndexColumn, Schema, Type}, + schema::{Index, IndexColumn, Schema, Table}, translate::plan::TerminationKey, types::SeekOp, util::exprs_are_equivalent, @@ -14,8 +14,8 @@ use crate::{ use super::{ emitter::Resolver, plan::{ - DeletePlan, EvalAt, GroupBy, IterationDirection, JoinInfo, JoinOrderMember, Operation, - Plan, Search, SeekDef, SeekKey, SelectPlan, TableReference, UpdatePlan, WhereTerm, + DeletePlan, EvalAt, GroupBy, IterationDirection, JoinOrderMember, Operation, Plan, Search, + SeekDef, SeekKey, SelectPlan, TableReference, UpdatePlan, WhereTerm, }, planner::determine_where_to_eval_expr, }; @@ -55,8 +55,6 @@ fn optimize_select_plan(plan: &mut SelectPlan, schema: &Schema) -> Result<()> { plan.join_order = best_join_order; } - eliminate_orderby_like_groupby(plan)?; - Ok(()) } @@ -108,164 +106,6 @@ fn optimize_subqueries(plan: &mut SelectPlan, schema: &Schema) -> Result<()> { Ok(()) } -fn eliminate_orderby_like_groupby(plan: &mut SelectPlan) -> Result<()> { - if plan.order_by.is_none() | plan.group_by.is_none() { - return Ok(()); - } - if plan.table_references.len() == 0 { - return Ok(()); - } - - let order_by_clauses = plan.order_by.as_mut().unwrap(); - // TODO: let's make the group by sorter aware of the order by orders so we dont need to skip - // descending terms. - if order_by_clauses - .iter() - .any(|(_, dir)| matches!(dir, SortOrder::Desc)) - { - return Ok(()); - } - let group_by_clauses = plan.group_by.as_mut().unwrap(); - // all order by terms must be in the group by clause for order by to be eliminated - if !order_by_clauses.iter().all(|(o_expr, _)| { - group_by_clauses - .exprs - .iter() - .any(|g_expr| exprs_are_equivalent(g_expr, o_expr)) - }) { - return Ok(()); - } - - // reorder group by terms so that they match the order by terms - // this way the group by sorter will effectively do the order by sorter's job and - // we can remove the order by clause - group_by_clauses.exprs.sort_by_key(|g_expr| { - order_by_clauses - .iter() - .position(|(o_expr, _)| exprs_are_equivalent(o_expr, g_expr)) - .unwrap_or(usize::MAX) - }); - - plan.order_by = None; - Ok(()) -} - -/// Eliminate unnecessary ORDER BY clauses. -/// Returns true if the ORDER BY clause was eliminated. -fn eliminate_unnecessary_orderby( - table_references: &mut [TableReference], - available_indexes: &HashMap>>, - order_by: &mut Option>, - group_by: &Option, -) -> Result { - let Some(order) = order_by else { - return Ok(false); - }; - let Some(first_table_reference) = table_references.first_mut() else { - return Ok(false); - }; - let Some(btree_table) = first_table_reference.btree() else { - return Ok(false); - }; - // If GROUP BY clause is present, we can't rely on already ordered columns because GROUP BY reorders the data - // This early return prevents the elimination of ORDER BY when GROUP BY exists, as sorting must be applied after grouping - // And if ORDER BY clause duplicates GROUP BY we handle it later in fn eliminate_orderby_like_groupby - if group_by.is_some() { - return Ok(false); - } - let Operation::Scan { - index, iter_dir, .. - } = &mut first_table_reference.op - else { - return Ok(false); - }; - - assert!( - index.is_none(), - "Nothing shouldve transformed the scan to use an index yet" - ); - - // Special case: if ordering by just the rowid, we can remove the ORDER BY clause - if order.len() == 1 && order[0].0.is_rowid_alias_of(0) { - *iter_dir = match order[0].1 { - SortOrder::Asc => IterationDirection::Forwards, - SortOrder::Desc => IterationDirection::Backwards, - }; - *order_by = None; - return Ok(true); - } - - // Find the best matching index for the ORDER BY columns - let table_name = &btree_table.name; - let mut best_index = (None, 0); - - for (_, indexes) in available_indexes.iter() { - for index_candidate in indexes.iter().filter(|i| &i.table_name == table_name) { - let matching_columns = index_candidate.columns.iter().enumerate().take_while(|(i, c)| { - if let Some((Expr::Column { table, column, .. }, _)) = order.get(*i) { - let col_idx_in_table = btree_table - .columns - .iter() - .position(|tc| tc.name.as_ref() == Some(&c.name)); - matches!(col_idx_in_table, Some(col_idx) if *table == 0 && *column == col_idx) - } else { - false - } - }).count(); - - if matching_columns > best_index.1 { - best_index = (Some(index_candidate), matching_columns); - } - } - } - - let Some(matching_index) = best_index.0 else { - return Ok(false); - }; - let match_count = best_index.1; - - // If we found a matching index, use it for scanning - *index = Some(matching_index.clone()); - // If the order by order matches the index order, we can iterate the index in forwards order. - // If they don't, we must iterate the index in backwards order. - let index_order = &matching_index.columns.first().as_ref().unwrap().order; - *iter_dir = match (index_order, order[0].1) { - (SortOrder::Asc, SortOrder::Asc) | (SortOrder::Desc, SortOrder::Desc) => { - IterationDirection::Forwards - } - (SortOrder::Asc, SortOrder::Desc) | (SortOrder::Desc, SortOrder::Asc) => { - IterationDirection::Backwards - } - }; - - // If the index covers all ORDER BY columns, and one of the following applies: - // - the ORDER BY orders exactly match the index orderings, - // - the ORDER by orders are the exact opposite of the index orderings, - // we can remove the ORDER BY clause. - if match_count == order.len() { - let full_match = { - let mut all_match_forward = true; - let mut all_match_reverse = true; - for (i, (_, order)) in order.iter().enumerate() { - match (&matching_index.columns[i].order, order) { - (SortOrder::Asc, SortOrder::Asc) | (SortOrder::Desc, SortOrder::Desc) => { - all_match_reverse = false; - } - (SortOrder::Asc, SortOrder::Desc) | (SortOrder::Desc, SortOrder::Asc) => { - all_match_forward = false; - } - } - } - all_match_forward || all_match_reverse - }; - if full_match { - *order_by = None; - } - } - - Ok(order_by.is_none()) -} - /// Represents an n-ary join, anywhere from 1 table to N tables. #[derive(Debug, Clone)] struct JoinN { @@ -363,7 +203,30 @@ fn join_lhs_tables_to_rhs_table( build_cost: Cost(0.0), }, kind: AccessMethodKind::TableScan { - iter_dir: IterationDirection::Forwards, + iter_dir: if let Some(order_target) = maybe_order_target { + // if the order target 1. has a single column 2. it is the rowid alias of this table 3. the order target column is in descending order, then we should use IterationDirection::Backwards + let rowid_alias_column_no = rhs_table_reference + .columns() + .iter() + .position(|c| c.is_rowid_alias); + + let should_use_backwards = + if let Some(rowid_alias_column_no) = rowid_alias_column_no { + order_target.0.len() == 1 + && order_target.0[0].table_no == rhs_table_number + && order_target.0[0].column_no == rowid_alias_column_no + && order_target.0[0].order == SortOrder::Desc + } else { + false + }; + if should_use_backwards { + IterationDirection::Backwards + } else { + IterationDirection::Forwards + } + } else { + IterationDirection::Forwards + }, }, }; @@ -414,7 +277,7 @@ fn join_lhs_tables_to_rhs_table( } else { AccessMethodKind::IndexScan { index: index_search.index.expect("index must exist"), - iter_dir: IterationDirection::Forwards, + iter_dir: index_search.iter_dir, } }, }; @@ -451,7 +314,7 @@ fn join_lhs_tables_to_rhs_table( let lhs_cost = lhs.map_or(Cost(0.0), |l| l.cost); let cost = lhs_cost + best_access_method.cost.total(); - let mut new_numbers = lhs.map_or(vec![rhs_table_number], |l| { + let new_numbers = lhs.map_or(vec![rhs_table_number], |l| { let mut numbers = l.table_numbers.clone(); numbers.push(rhs_table_number); numbers @@ -564,7 +427,6 @@ fn generate_join_bitmasks(table_number_max_exclusive: usize, how_many: usize) -> } /// Check if the plan's row iteration order matches the [OrderTarget]'s column order -/// TODO this needs to take iteration order into account, foo vitun bar saatana fn plan_satisfies_order_target( plan: &JoinN, table_references: &[TableReference], @@ -624,11 +486,17 @@ fn plan_satisfies_order_target( } } else { // same as table scan + let iter_dir = seek_def.iter_dir; for i in 0..table_ref.table.columns().len() { let target_col = &order_target.0[target_col_idx]; + let order_matches = if iter_dir == IterationDirection::Forwards { + target_col.order == SortOrder::Asc + } else { + target_col.order == SortOrder::Desc + }; if target_col.table_no != *table_no || target_col.column_no != i - || target_col.order != SortOrder::Asc + || !order_matches { return false; } @@ -664,23 +532,29 @@ fn plan_satisfies_order_target( } } AccessMethodKind::TableScan { iter_dir } => { - for i in 0..table_ref.table.columns().len() { - let target_col = &order_target.0[target_col_idx]; - let order_matches = if *iter_dir == IterationDirection::Forwards { - target_col.order == SortOrder::Asc - } else { - target_col.order != SortOrder::Asc - }; - if target_col.table_no != *table_no - || target_col.column_no != i - || !order_matches - { - return false; - } - target_col_idx += 1; - if target_col_idx == order_target.0.len() { - return true; - } + let rowid_alias_col = table_ref + .table + .columns() + .iter() + .position(|c| c.is_rowid_alias); + let Some(rowid_alias_col) = rowid_alias_col else { + return false; + }; + let target_col = &order_target.0[target_col_idx]; + let order_matches = if *iter_dir == IterationDirection::Forwards { + target_col.order == SortOrder::Asc + } else { + target_col.order == SortOrder::Desc + }; + if target_col.table_no != *table_no + || target_col.column_no != rowid_alias_col + || !order_matches + { + return false; + } + target_col_idx += 1; + if target_col_idx == order_target.0.len() { + return true; } } } @@ -688,15 +562,24 @@ fn plan_satisfies_order_target( false } +/// The result of [compute_best_join_order]. +#[derive(Debug)] +struct BestJoinOrderResult { + /// The best plan overall. + best_plan: JoinN, + /// The best plan for the given order target, if it isn't the overall best. + best_ordered_plan: Option, +} + /// Compute the best way to join a given set of tables. /// Returns the best [JoinN] if one exists, otherwise returns None. fn compute_best_join_order( table_references: &[TableReference], available_indexes: &HashMap>>, where_clause: &Vec, - maybe_order_target: Option, + maybe_order_target: Option<&OrderTarget>, access_methods_cache: &mut HashMap, -) -> Result> { +) -> Result> { if table_references.is_empty() { return Ok(None); } @@ -709,12 +592,9 @@ fn compute_best_join_order( table_references, available_indexes, where_clause, - maybe_order_target.as_ref(), + maybe_order_target, access_methods_cache, )?; - if table_references.len() == 1 { - return Ok(Some(naive_plan)); - } let mut best_ordered_plan: Option = None; let mut best_plan_is_also_ordered = if let Some(ref order_target) = maybe_order_target { plan_satisfies_order_target( @@ -726,6 +606,12 @@ fn compute_best_join_order( } else { false }; + if table_references.len() == 1 { + return Ok(Some(BestJoinOrderResult { + best_plan: naive_plan, + best_ordered_plan: None, + })); + } let mut best_plan = naive_plan; let mut join_order = Vec::with_capacity(n); join_order.push(JoinOrderMember { @@ -763,7 +649,7 @@ fn compute_best_join_order( where_clause, indexes_ref, &join_order, - maybe_order_target.as_ref(), + maybe_order_target, access_methods_cache, )?; best_plan_memo.insert(mask, rel); @@ -867,7 +753,7 @@ fn compute_best_join_order( where_clause, indexes_ref, &join_order, - maybe_order_target.as_ref(), + maybe_order_target, access_methods_cache, )?; join_order.clear(); @@ -927,7 +813,14 @@ fn compute_best_join_order( } } - Ok(Some(best_plan)) + Ok(Some(BestJoinOrderResult { + best_plan, + best_ordered_plan: if best_plan_is_also_ordered { + None + } else { + best_ordered_plan + }, + })) } /// Specialized version of [compute_best_join_order] that just joins tables in the order they are given @@ -990,17 +883,27 @@ fn compute_naive_left_deep_plan( Ok(best_plan) } +#[derive(Debug, PartialEq, Clone)] struct ColumnOrder { table_no: usize, column_no: usize, order: SortOrder, } -struct OrderTarget(Vec); +#[derive(Debug, PartialEq, Clone)] +enum EliminatesSort { + GroupBy, + OrderBy, + GroupByAndOrderBy, +} + +#[derive(Debug, PartialEq, Clone)] +pub struct OrderTarget(Vec, EliminatesSort); impl OrderTarget { fn maybe_from_iterator<'a>( list: impl Iterator + Clone, + eliminates_sort: EliminatesSort, ) -> Option { if list.clone().count() == 0 { return None; @@ -1023,6 +926,7 @@ impl OrderTarget { } }) .collect(), + eliminates_sort, )) } } @@ -1031,6 +935,9 @@ impl OrderTarget { /// Ideally, a join order is both efficient in joining the tables /// but also returns the results in an order that minimizes the amount of /// sorting that needs to be done later (either in GROUP BY, ORDER BY, or both). +/// +/// TODO: this does not currently handle the case where we definitely cannot eliminate +/// the ORDER BY sorter, but we could still eliminate the GROUP BY sorter. fn compute_order_target( order_by: &Option>, group_by: Option<&mut GroupBy>, @@ -1039,12 +946,14 @@ fn compute_order_target( // No ordering demands - we don't care what order the joined result rows are in (None, None) => None, // Only ORDER BY - we would like the joined result rows to be in the order specified by the ORDER BY - (Some(order_by), None) => { - OrderTarget::maybe_from_iterator(order_by.iter().map(|(expr, order)| (expr, *order))) - } + (Some(order_by), None) => OrderTarget::maybe_from_iterator( + order_by.iter().map(|(expr, order)| (expr, *order)), + EliminatesSort::OrderBy, + ), // Only GROUP BY - we would like the joined result rows to be in the order specified by the GROUP BY (None, Some(group_by)) => OrderTarget::maybe_from_iterator( group_by.exprs.iter().map(|expr| (expr, SortOrder::Asc)), + EliminatesSort::GroupBy, ), // Both ORDER BY and GROUP BY: // If the GROUP BY does not contain all the expressions in the ORDER BY, @@ -1066,6 +975,7 @@ fn compute_order_target( if !group_by_contains_all { return OrderTarget::maybe_from_iterator( group_by.exprs.iter().map(|expr| (expr, SortOrder::Asc)), + EliminatesSort::GroupBy, ); } // If yes, let's try to target an ordering that matches the GROUP BY columns, @@ -1088,6 +998,7 @@ fn compute_order_target( .chain(std::iter::repeat(&SortOrder::Asc)), ) .map(|(expr, dir)| (expr, *dir)), + EliminatesSort::GroupByAndOrderBy, ) } } @@ -1102,25 +1013,68 @@ fn use_indexes( ) -> Result>> { let mut access_methods_cache = HashMap::new(); let maybe_order_target = compute_order_target(order_by, group_by.as_mut()); - let Some(best) = compute_best_join_order( + let Some(best_join_order_result) = compute_best_join_order( table_references, available_indexes, where_clause, - maybe_order_target, + maybe_order_target.as_ref(), &mut access_methods_cache, )? else { return Ok(None); }; + let BestJoinOrderResult { + best_plan, + best_ordered_plan, + } = best_join_order_result; + + let best_plan = if let Some(best_ordered_plan) = best_ordered_plan { + let best_unordered_plan_cost = best_plan.cost; + let best_ordered_plan_cost = best_ordered_plan.cost; + const SORT_COST_PER_ROW_MULTIPLIER: f64 = 0.001; + let sorting_penalty = + Cost(best_plan.output_cardinality as f64 * SORT_COST_PER_ROW_MULTIPLIER); + if best_unordered_plan_cost + sorting_penalty > best_ordered_plan_cost { + best_ordered_plan + } else { + best_plan + } + } else { + best_plan + }; + + if let Some(order_target) = maybe_order_target { + let satisfies_order_target = plan_satisfies_order_target( + &best_plan, + table_references, + &mut access_methods_cache, + &order_target, + ); + if satisfies_order_target { + match order_target.1 { + EliminatesSort::GroupBy => { + let _ = group_by.as_mut().and_then(|g| g.sort_order.take()); + } + EliminatesSort::OrderBy => { + let _ = order_by.take(); + } + EliminatesSort::GroupByAndOrderBy => { + let _ = group_by.as_mut().and_then(|g| g.sort_order.take()); + let _ = order_by.take(); + } + } + } + } + let (mut best_access_methods, best_table_numbers) = { - let mut kinds = Vec::with_capacity(best.best_access_methods.len()); - for am_idx in best.best_access_methods.iter() { + let mut kinds = Vec::with_capacity(best_plan.best_access_methods.len()); + for am_idx in best_plan.best_access_methods.iter() { // take value from cache let am = access_methods_cache.remove(am_idx).unwrap(); kinds.push(am.kind); } - (kinds, best.table_numbers) + (kinds, best_plan.table_numbers) }; let mut to_remove_from_where_clause = vec![]; for table_number in best_table_numbers.iter().rev() { @@ -1132,7 +1086,9 @@ fn use_indexes( // FIXME: Operation::Subquery shouldn't exist. It's not an operation, it's a kind of temporary table. assert!( matches!(access_method, AccessMethodKind::TableScan { .. }), - "nothing in the current optimizer should be able to optimize subqueries" + "nothing in the current optimizer should be able to optimize subqueries, but got {:?} for table {}", + access_method, + table_references[*table_number].table.get_name() ); continue; } @@ -1803,7 +1759,32 @@ pub fn try_extract_index_search_from_where_clause( false, input_cardinality, ); - if cost.total() < best_index.cost.total() { + let order_satisfiability_bonus = if let Some(order_target) = maybe_order_target { + let mut all_same_direction = true; + let mut all_opposite_direction = true; + for i in 0..order_target.0.len().min(index.columns.len()) { + if order_target.0[i].table_no != table_index + || order_target.0[i].column_no != index.columns[i].pos_in_table + { + all_same_direction = false; + all_opposite_direction = false; + break; + } + if order_target.0[i].order == index.columns[i].order { + all_opposite_direction = false; + } else { + all_same_direction = false; + } + } + if all_same_direction || all_opposite_direction { + Cost(1.0) + } else { + Cost(0.0) + } + } else { + Cost(0.0) + }; + if cost.total() < best_index.cost.total() + order_satisfiability_bonus { best_index.index = Some(Arc::clone(index)); best_index.cost = cost; best_index.constraints.clear(); @@ -1813,7 +1794,7 @@ pub fn try_extract_index_search_from_where_clause( // 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() { + if best_index.index.is_none() && matches!(table_reference.table, Table::BTree(_)) { let (ephemeral_cost, constraints_with_col_idx, mut constraints_without_col_idx) = ephemeral_index_estimate_cost( where_clause, @@ -1841,11 +1822,7 @@ pub fn try_extract_index_search_from_where_clause( return Ok(None); } - if best_index.constraints.is_empty() { - return Ok(Some(best_index)); - } - - let iter_dir = if let Some(order_target) = maybe_order_target { + best_index.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 = best_index.index.as_ref().unwrap(); let mut should_use_backwards = true; @@ -1870,9 +1847,16 @@ pub fn try_extract_index_search_from_where_clause( IterationDirection::Forwards }; + if best_index.constraints.is_empty() { + return Ok(Some(best_index)); + } + // Build the seek definition - let seek_def = - build_seek_def_from_index_constraints(&best_index.constraints, iter_dir, where_clause)?; + let seek_def = build_seek_def_from_index_constraints( + &best_index.constraints, + best_index.iter_dir, + where_clause, + )?; // Remove the used terms from the where_clause since they are now part of the seek definition // Sort terms by position in descending order to avoid shifting indices during removal diff --git a/core/translate/plan.rs b/core/translate/plan.rs index a422b6fed..50e325832 100644 --- a/core/translate/plan.rs +++ b/core/translate/plan.rs @@ -64,9 +64,10 @@ impl ResultSetColumn { #[derive(Debug, Clone)] pub struct GroupBy { pub exprs: Vec, + /// sort order, if a sorter is required (= the columns aren't already in the correct order) + pub sort_order: Option>, /// having clause split into a vec at 'AND' boundaries. pub having: Option>, - pub sort_order: Option>, } /// In a query plan, WHERE clause conditions and JOIN conditions are all folded into a vector of WhereTerm. diff --git a/testing/groupby.test b/testing/groupby.test index 9fce2e83e..70141be0a 100644 --- a/testing/groupby.test +++ b/testing/groupby.test @@ -192,3 +192,9 @@ do_execsql_test groupby_orderby_removal_regression_test { } {1|Foster|1 2|Salazar|1 3|Perry|1} + +do_execsql_test group_by_no_sorting_required { + select age, count(1) from users group by age limit 3; +} {1|112 +2|113 +3|97} \ No newline at end of file diff --git a/testing/orderby.test b/testing/orderby.test index a33b32eb0..b5b56cdd4 100755 --- a/testing/orderby.test +++ b/testing/orderby.test @@ -142,11 +142,11 @@ do_execsql_test case-insensitive-alias { select u.first_name as fF, count(1) > 0 as cC from users u where fF = 'Jamie' group by fF order by cC; } {Jamie|1} -#do_execsql_test age_idx_order_desc { -# select first_name from users order by age desc limit 3; -#} {Robert -#Sydney -#Matthew} +do_execsql_test age_idx_order_desc { + select first_name from users order by age desc limit 3; +} {Robert +Sydney +Matthew} do_execsql_test rowid_or_integer_pk_desc { select first_name from users order by id desc limit 3; @@ -163,40 +163,40 @@ do_execsql_test orderby_desc_verify_rows { select count(1) from (select * from users order by age desc) } {10000} -#do_execsql_test orderby_desc_with_offset { -# select first_name, age from users order by age desc limit 3 offset 666; -#} {Francis|94 -#Matthew|94 -#Theresa|94} +do_execsql_test orderby_desc_with_offset { + select first_name, age from users order by age desc limit 3 offset 666; +} {Francis|94 +Matthew|94 +Theresa|94} -#do_execsql_test orderby_desc_with_filter { -# select first_name, age from users where age <= 50 order by age desc limit 5; -#} {Gerald|50 -#Nicole|50 -#Tammy|50 -#Marissa|50 -#Daniel|50} +do_execsql_test orderby_desc_with_filter { + select first_name, age from users where age <= 50 order by age desc limit 5; +} {Gerald|50 +Nicole|50 +Tammy|50 +Marissa|50 +Daniel|50} -#do_execsql_test orderby_asc_with_filter_range { -# select first_name, age from users where age <= 50 and age >= 49 order by age asc limit 5; -#} {William|49 -#Jennifer|49 -#Robert|49 -#David|49 -#Stephanie|49} +do_execsql_test orderby_asc_with_filter_range { + select first_name, age from users where age <= 50 and age >= 49 order by age asc limit 5; +} {William|49 +Jennifer|49 +Robert|49 +David|49 +Stephanie|49} -#do_execsql_test orderby_desc_with_filter_id_lt { -# select id from users where id < 6666 order by id desc limit 5; -#} {6665 -#6664 -#6663 -#6662 -#6661} +do_execsql_test orderby_desc_with_filter_id_lt { + select id from users where id < 6666 order by id desc limit 5; +} {6665 +6664 +6663 +6662 +6661} -#do_execsql_test orderby_desc_with_filter_id_le { -# select id from users where id <= 6666 order by id desc limit 5; -#} {6666 -#6665 -#6664 -#6663 -#6662} \ No newline at end of file +do_execsql_test orderby_desc_with_filter_id_le { + select id from users where id <= 6666 order by id desc limit 5; +} {6666 +6665 +6664 +6663 +6662} \ No newline at end of file