vibesql_executor/select/executor/fast_path/
mod.rs

1//! Fast path execution for simple point-lookup queries
2//!
3//! This module provides an optimized execution path for simple OLTP queries that:
4//! - Query a single table (no JOINs)
5//! - Have no subqueries
6//! - Have no aggregates, window functions, or GROUP BY
7//! - Have simple column references in SELECT
8//! - Have simple equality predicates in WHERE
9//! - Have simple ORDER BY clauses (column references only, no expressions)
10//!
11//! These queries skip expensive optimizer passes and go directly to index scan,
12//! providing 5-10x speedup for TPC-C style point lookups.
13//!
14//! # Streaming Aggregation (#3815)
15//!
16//! For queries like `SELECT SUM(k) FROM sbtest1 WHERE id BETWEEN ? AND ?`,
17//! we provide an ultra-fast streaming aggregation path that:
18//! - Accumulates aggregates inline during PK range scan
19//! - Never materializes intermediate row objects
20//! - Achieves SQLite-like performance (~4μs vs 30μs for standard path)
21//!
22//! # Lookup Strategies
23//!
24//! The fast path tries these lookup strategies in order:
25//!
26//! 1. **Primary Key Lookup** (`try_pk_lookup_fast`): Direct O(1) lookup when WHERE clause has
27//!    equality predicates for all PK columns.
28//!
29//! 2. **Secondary Index Lookup** (`try_secondary_index_lookup_fast`): O(log n) lookup when WHERE
30//!    clause has equality predicates for all columns of a secondary index. Handles queries like:
31//!    `SELECT * FROM customer WHERE c_w_id = 1 AND c_d_id = 2 AND c_last = 'SMITH'`
32//!
33//! 3. **Standard Scan**: Falls back to execute_from_clause for other queries.
34//!
35//! # ORDER BY Support
36//!
37//! The fast path supports ORDER BY with simple column references:
38//! - If an index exists that matches the ORDER BY column(s), results are returned pre-sorted from
39//!   the index scan (zero-cost sorting)
40//! - If no matching index exists, explicit sorting is applied after filtering
41//!
42//! # Performance Impact
43//!
44//! For a query like `SELECT w_tax FROM warehouse WHERE w_id = 1`:
45//! - Standard path: ~1200us (optimizer passes, strategy selection, pipeline creation)
46//! - Fast path: ~50-100us (direct index scan, minimal overhead)
47//!
48//! For secondary index lookups (TPC-C customer-by-last-name):
49//! - Standard path: ~4000-5000us (full scan machinery)
50//! - Fast path: ~100-200us (direct index lookup)
51//!
52//! # Example Queries
53//!
54//! ```sql
55//! -- These queries use the fast path:
56//! SELECT col FROM table WHERE pk = 1
57//! SELECT col1, col2 FROM table WHERE pk1 = 1 AND pk2 = 2
58//! SELECT * FROM table WHERE id = 123
59//! SELECT no_o_id FROM new_order WHERE no_w_id = 1 ORDER BY no_o_id  -- with ORDER BY
60//! SELECT * FROM customer WHERE c_w_id = 1 AND c_d_id = 2 AND c_last = 'SMITH'  -- secondary index
61//!
62//! -- These queries use the standard path:
63//! SELECT COUNT(*) FROM table WHERE id = 1  -- aggregate
64//! SELECT a FROM t1, t2 WHERE t1.id = t2.id  -- join
65//! SELECT a FROM t WHERE id IN (SELECT id FROM t2)  -- subquery
66//! SELECT a FROM t ORDER BY UPPER(a)  -- complex ORDER BY expression
67//! ```
68//!
69//! # Module Structure
70//!
71//! - `analysis`: Query analysis functions to detect fast-path eligibility
72//! - `helpers`: Shared utilities for projection, filtering, sorting
73//! - `pk_lookup`: Primary key lookup strategies
74//! - `index_lookup`: Secondary index lookup strategies
75//! - `range_scan`: PK range scan with early projection
76//! - `streaming_agg`: Streaming aggregation execution
77
78use std::collections::HashMap;
79
80use vibesql_ast::SelectStmt;
81use vibesql_storage::Row;
82
83use super::builder::SelectExecutor;
84use crate::errors::ExecutorError;
85
86// Submodules
87pub(crate) mod analysis;
88mod helpers;
89mod index_lookup;
90mod pk_lookup;
91mod range_scan;
92mod streaming_agg;
93
94// Re-export public API
95pub use analysis::{is_simple_point_query, is_streaming_aggregate_query};
96
97impl SelectExecutor<'_> {
98    /// Execute a query using the fast path
99    ///
100    /// This bypasses the optimizer infrastructure and goes directly to table scan
101    /// with optional index optimization.
102    ///
103    /// # Performance Note (#3780)
104    ///
105    /// This method is called by `Session::execute_prepared()` for queries using
106    /// `SimpleFastPath` cached plans. It executes the query and returns just the
107    /// rows, leaving column name resolution to the cached plan.
108    pub fn execute_fast_path(&self, stmt: &SelectStmt) -> Result<Vec<Row>, ExecutorError> {
109        // Extract table name from FROM clause
110        let (table_name, alias) = match &stmt.from {
111            Some(vibesql_ast::FromClause::Table { name, alias, .. }) => {
112                (name.as_str(), alias.as_ref())
113            }
114            _ => unreachable!("Fast path requires simple table FROM clause"),
115        };
116
117        // Resolve SELECT aliases in WHERE clause BEFORE any processing (SQLite extension)
118        // This allows queries like: SELECT f1-22 AS x FROM t1 WHERE x > 0
119        // NOTE: Table column names take precedence over aliases (SQLite behavior)
120        // PERFORMANCE: Skip alias resolution if no aliases exist (common case in OLTP)
121        let resolved_where = if stmt.where_clause.is_some()
122            && crate::select::order::select_list_has_aliases(&stmt.select_list)
123        {
124            stmt.where_clause.as_ref().map(|where_expr| {
125                // Build schema for the single table
126                // catalog.get_table returns &TableSchema, not &Table
127                if let Some(table_schema) = self.database.catalog.get_table(table_name) {
128                    let effective_name = alias.map(|a| a.as_str()).unwrap_or(table_name);
129                    let schema = crate::schema::CombinedSchema::from_table(
130                        effective_name.to_string(),
131                        table_schema.clone(),
132                    );
133                    crate::select::order::resolve_where_aliases_with_schema(
134                        where_expr,
135                        &stmt.select_list,
136                        &schema,
137                    )
138                } else {
139                    // Fallback if schema not found (shouldn't happen in fast path)
140                    crate::select::order::resolve_where_aliases(where_expr, &stmt.select_list)
141                }
142            })
143        } else {
144            stmt.where_clause.clone()
145        };
146
147        // Try ultra-fast PK lookup path first
148        if let Some(result) = self.try_pk_lookup_fast(table_name, alias, stmt)? {
149            return Ok(result);
150        }
151
152        // Try PK prefix lookup with early LIMIT termination (TPC-C Delivery optimization)
153        if let Some(result) = self.try_pk_prefix_with_limit_fast(table_name, alias, stmt)? {
154            return Ok(result);
155        }
156
157        // Try secondary index prefix lookup with ORDER BY + LIMIT (TPC-C Order-Status optimization)
158        if let Some(result) =
159            self.try_secondary_index_prefix_with_limit_fast(table_name, alias, stmt)?
160        {
161            return Ok(result);
162        }
163
164        // Try secondary index lookup path next
165        if let Some(result) = self.try_secondary_index_lookup_fast(table_name, alias, stmt)? {
166            return Ok(result);
167        }
168
169        // Try covering index scan (index-only scan) for queries where all SELECT columns
170        // are in the index key - eliminates table fetches entirely
171        if let Some(result) = self.try_covering_index_scan_fast(table_name, alias, stmt)? {
172            return Ok(result);
173        }
174
175        // Try PK range scan with early projection (#3799)
176        // For simple range queries like `SELECT c FROM t WHERE pk BETWEEN ? AND ?`,
177        // use streaming scan with early projection to avoid cloning unneeded columns.
178        if let Some(result) = self.try_pk_range_scan_with_early_projection(table_name, stmt)? {
179            return Ok(result);
180        }
181
182        // Fall back to standard fast path with execute_from_clause
183        // Pass LIMIT for early termination optimization (#3253)
184        let limit = crate::select::helpers::evaluate_limit(&stmt.limit, self.database)?;
185        let from_result = crate::select::scan::execute_from_clause(
186            stmt.from.as_ref().unwrap(),
187            &HashMap::new(), // No CTEs
188            self.database,
189            resolved_where.as_ref(),
190            stmt.order_by.as_deref(),
191            limit, // LIMIT pushdown for ORDER BY optimization
192            None,  // No outer row
193            None,  // No outer schema
194            |_| unreachable!("Fast path doesn't support subqueries"),
195        )?;
196
197        let schema = from_result.schema.clone();
198        let where_filtered = from_result.where_filtered;
199        let sorted_by = from_result.sorted_by.clone();
200        let rows = from_result.into_rows();
201
202        // Apply remaining WHERE clause if not already filtered
203        let filtered_rows = match (&resolved_where, where_filtered) {
204            (Some(where_expr), false) => {
205                self.apply_where_filter_fast(where_expr, rows, &schema)?
206            }
207            _ => rows,
208        };
209
210        // Apply ORDER BY sorting if needed (index didn't provide the order)
211        let sorted_rows = if let Some(order_by) = &stmt.order_by {
212            if analysis::needs_sorting(order_by, &sorted_by) {
213                self.apply_order_by_fast(order_by, filtered_rows, &schema)?
214            } else {
215                filtered_rows
216            }
217        } else {
218            filtered_rows
219        };
220
221        // Apply projection
222        let projected_rows = self.apply_projection_fast(&stmt.select_list, sorted_rows, &schema)?;
223
224        // Apply LIMIT/OFFSET
225        let offset = crate::select::helpers::evaluate_offset(&stmt.offset, self.database)?;
226        let final_rows = crate::select::helpers::apply_limit_offset(projected_rows, limit, offset);
227
228        Ok(final_rows)
229    }
230}