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}