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
27//!    WHERE clause has equality predicates for all PK columns.
28//!
29//! 2. **Secondary Index Lookup** (`try_secondary_index_lookup_fast`): O(log n)
30//!    lookup when WHERE clause has equality predicates for all columns of a
31//!    secondary index. Handles queries like:
32//!    `SELECT * FROM customer WHERE c_w_id = 1 AND c_d_id = 2 AND c_last = 'SMITH'`
33//!
34//! 3. **Standard Scan**: Falls back to execute_from_clause for other queries.
35//!
36//! # ORDER BY Support
37//!
38//! The fast path supports ORDER BY with simple column references:
39//! - If an index exists that matches the ORDER BY column(s), results are
40//!   returned pre-sorted from the index scan (zero-cost sorting)
41//! - If no matching index exists, explicit sorting is applied after filtering
42//!
43//! # Performance Impact
44//!
45//! For a query like `SELECT w_tax FROM warehouse WHERE w_id = 1`:
46//! - Standard path: ~1200us (optimizer passes, strategy selection, pipeline creation)
47//! - Fast path: ~50-100us (direct index scan, minimal overhead)
48//!
49//! For secondary index lookups (TPC-C customer-by-last-name):
50//! - Standard path: ~4000-5000us (full scan machinery)
51//! - Fast path: ~100-200us (direct index lookup)
52//!
53//! # Example Queries
54//!
55//! ```sql
56//! -- These queries use the fast path:
57//! SELECT col FROM table WHERE pk = 1
58//! SELECT col1, col2 FROM table WHERE pk1 = 1 AND pk2 = 2
59//! SELECT * FROM table WHERE id = 123
60//! SELECT no_o_id FROM new_order WHERE no_w_id = 1 ORDER BY no_o_id  -- with ORDER BY
61//! SELECT * FROM customer WHERE c_w_id = 1 AND c_d_id = 2 AND c_last = 'SMITH'  -- secondary index
62//!
63//! -- These queries use the standard path:
64//! SELECT COUNT(*) FROM table WHERE id = 1  -- aggregate
65//! SELECT a FROM t1, t2 WHERE t1.id = t2.id  -- join
66//! SELECT a FROM t WHERE id IN (SELECT id FROM t2)  -- subquery
67//! SELECT a FROM t ORDER BY UPPER(a)  -- complex ORDER BY expression
68//! ```
69//!
70//! # Module Structure
71//!
72//! - `analysis`: Query analysis functions to detect fast-path eligibility
73//! - `helpers`: Shared utilities for projection, filtering, sorting
74//! - `pk_lookup`: Primary key lookup strategies
75//! - `index_lookup`: Secondary index lookup strategies
76//! - `range_scan`: PK range scan with early projection
77//! - `streaming_agg`: Streaming aggregation execution
78
79use std::collections::HashMap;
80
81use vibesql_ast::SelectStmt;
82use vibesql_storage::Row;
83
84use super::builder::SelectExecutor;
85use crate::errors::ExecutorError;
86
87// Submodules
88pub(crate) mod analysis;
89mod helpers;
90mod index_lookup;
91mod pk_lookup;
92mod range_scan;
93mod streaming_agg;
94
95// Re-export public API
96pub use analysis::{is_simple_point_query, is_streaming_aggregate_query};
97
98impl SelectExecutor<'_> {
99    /// Execute a query using the fast path
100    ///
101    /// This bypasses the optimizer infrastructure and goes directly to table scan
102    /// with optional index optimization.
103    ///
104    /// # Performance Note (#3780)
105    ///
106    /// This method is called by `Session::execute_prepared()` for queries using
107    /// `SimpleFastPath` cached plans. It executes the query and returns just the
108    /// rows, leaving column name resolution to the cached plan.
109    pub fn execute_fast_path(&self, stmt: &SelectStmt) -> Result<Vec<Row>, ExecutorError> {
110        // Extract table name from FROM clause
111        let (table_name, alias) = match &stmt.from {
112            Some(vibesql_ast::FromClause::Table { name, alias, .. }) => {
113                (name.as_str(), alias.as_ref())
114            }
115            _ => unreachable!("Fast path requires simple table FROM clause"),
116        };
117
118        // Try ultra-fast PK lookup path first
119        if let Some(result) = self.try_pk_lookup_fast(table_name, alias, stmt)? {
120            return Ok(result);
121        }
122
123        // Try PK prefix lookup with early LIMIT termination (TPC-C Delivery optimization)
124        if let Some(result) = self.try_pk_prefix_with_limit_fast(table_name, alias, stmt)? {
125            return Ok(result);
126        }
127
128        // Try secondary index prefix lookup with ORDER BY + LIMIT (TPC-C Order-Status optimization)
129        if let Some(result) =
130            self.try_secondary_index_prefix_with_limit_fast(table_name, alias, stmt)?
131        {
132            return Ok(result);
133        }
134
135        // Try secondary index lookup path next
136        if let Some(result) = self.try_secondary_index_lookup_fast(table_name, alias, stmt)? {
137            return Ok(result);
138        }
139
140        // Try covering index scan (index-only scan) for queries where all SELECT columns
141        // are in the index key - eliminates table fetches entirely
142        if let Some(result) = self.try_covering_index_scan_fast(table_name, alias, stmt)? {
143            return Ok(result);
144        }
145
146        // Try PK range scan with early projection (#3799)
147        // For simple range queries like `SELECT c FROM t WHERE pk BETWEEN ? AND ?`,
148        // use streaming scan with early projection to avoid cloning unneeded columns.
149        if let Some(result) = self.try_pk_range_scan_with_early_projection(table_name, stmt)? {
150            return Ok(result);
151        }
152
153        // Fall back to standard fast path with execute_from_clause
154        // Pass LIMIT for early termination optimization (#3253)
155        let from_result = crate::select::scan::execute_from_clause(
156            stmt.from.as_ref().unwrap(),
157            &HashMap::new(), // No CTEs
158            self.database,
159            stmt.where_clause.as_ref(),
160            stmt.order_by.as_deref(),
161            stmt.limit, // LIMIT pushdown for ORDER BY optimization
162            None,       // No outer row
163            None,       // No outer schema
164            |_| unreachable!("Fast path doesn't support subqueries"),
165        )?;
166
167        let schema = from_result.schema.clone();
168        let where_filtered = from_result.where_filtered;
169        let sorted_by = from_result.sorted_by.clone();
170        let rows = from_result.into_rows();
171
172        // Apply remaining WHERE clause if not already filtered
173        let filtered_rows = if where_filtered || stmt.where_clause.is_none() {
174            rows
175        } else {
176            self.apply_where_filter_fast(stmt.where_clause.as_ref().unwrap(), rows, &schema)?
177        };
178
179        // Apply ORDER BY sorting if needed (index didn't provide the order)
180        let sorted_rows = if let Some(order_by) = &stmt.order_by {
181            if analysis::needs_sorting(order_by, &sorted_by) {
182                self.apply_order_by_fast(order_by, filtered_rows, &schema)?
183            } else {
184                filtered_rows
185            }
186        } else {
187            filtered_rows
188        };
189
190        // Apply projection
191        let projected_rows = self.apply_projection_fast(&stmt.select_list, sorted_rows, &schema)?;
192
193        // Apply LIMIT/OFFSET
194        let final_rows =
195            crate::select::helpers::apply_limit_offset(projected_rows, stmt.limit, stmt.offset);
196
197        Ok(final_rows)
198    }
199}