1use std::collections::HashMap;
21
22#[derive(Debug, Default)]
24pub struct QueryOptimizer {
25 cost_model: CostModel,
26 cardinality_hints: HashMap<String, CardinalityHint>,
27 total_edges: usize,
28}
29
30#[derive(Debug, Clone)]
32pub struct CostModel {
33 pub row_read_cost: f64,
35 pub index_lookup_cost: f64,
37 pub seq_scan_cost: f64,
39 pub vector_search_cost: f64,
41}
42
43impl Default for CostModel {
44 fn default() -> Self {
45 Self {
46 row_read_cost: 1.0,
47 index_lookup_cost: 0.5,
48 seq_scan_cost: 0.1,
49 vector_search_cost: 10.0,
50 }
51 }
52}
53
54#[derive(Debug, Clone)]
56pub struct CardinalityHint {
57 pub cardinality: usize,
59 pub confidence: f64,
61 pub source: CardinalitySource,
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum CardinalitySource {
68 Exact,
70 HyperLogLog,
72 Histogram,
74 Default,
76}
77
78#[derive(Debug, Clone)]
80pub enum QueryPredicate {
81 Eq(String, String),
83 Range {
85 column: String,
86 min: Option<String>,
87 max: Option<String>,
88 },
89 In(String, Vec<String>),
91 Prefix(String, String),
93 TimeRange(u64, u64),
95 Project(u16),
97 Tenant(u32),
99 SpanType(String),
101}
102
103#[derive(Debug, Clone, PartialEq)]
105pub enum QueryOperation {
106 PointLookup,
108 RangeScan,
110 FullScan,
112 VectorSearch { k: usize },
114 LsmRangeScan { start_us: u64, end_us: u64 },
116 GraphTraversal {
118 direction: TraversalDirection,
119 max_depth: usize,
120 },
121}
122
123#[derive(Debug, Clone)]
125pub enum IndexSelection {
126 PrimaryKey,
128 Secondary(String),
130 TimeIndex,
132 VectorIndex,
134 FullScan,
136 LsmScan,
138 CausalIndex,
140 ProjectIndex,
142 MultiIndex(Vec<IndexSelection>),
144}
145
146#[derive(Debug, Clone)]
148pub struct QueryCost {
149 pub estimated_cost: f64,
151 pub total_cost: f64,
153 pub estimated_rows: usize,
155 pub records_returned: usize,
157 pub index: IndexSelection,
159 pub operation: QueryOperation,
161 pub breakdown: Vec<(QueryOperation, f64)>,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum TraversalDirection {
168 Forward,
169 Backward,
170}
171
172#[derive(Debug, Clone)]
174pub struct QueryPlan {
175 pub cost: QueryCost,
177 pub predicates: Vec<QueryPredicate>,
179 pub direction: TraversalDirection,
181 pub limit: Option<usize>,
183 pub index_selection: IndexSelection,
185 pub operations: Vec<QueryOperation>,
187}
188
189impl QueryOptimizer {
190 pub fn new() -> Self {
192 Self::default()
193 }
194
195 pub fn with_cost_model(cost_model: CostModel) -> Self {
197 Self {
198 cost_model,
199 ..Default::default()
200 }
201 }
202
203 pub fn update_total_edges(&mut self, count: usize, _source: CardinalitySource) {
205 self.total_edges = count;
206 }
207
208 pub fn set_cardinality_hint(&mut self, column: &str, hint: CardinalityHint) {
210 self.cardinality_hints.insert(column.to_string(), hint);
211 }
212
213 pub fn plan_query(&self, predicates: &[QueryPredicate], limit: Option<usize>) -> QueryPlan {
215 let (index, operation) = if predicates.is_empty() {
217 (IndexSelection::FullScan, QueryOperation::FullScan)
218 } else {
219 let has_eq = predicates
221 .iter()
222 .any(|p| matches!(p, QueryPredicate::Eq(_, _)));
223 if has_eq {
224 (IndexSelection::PrimaryKey, QueryOperation::PointLookup)
225 } else {
226 (IndexSelection::FullScan, QueryOperation::RangeScan)
227 }
228 };
229
230 let estimated_rows = match &index {
231 IndexSelection::PrimaryKey => 1,
232 IndexSelection::FullScan => self.total_edges.max(1000),
233 _ => self.total_edges / 10,
234 };
235
236 let estimated_cost = match &operation {
237 QueryOperation::PointLookup => self.cost_model.index_lookup_cost,
238 QueryOperation::RangeScan => estimated_rows as f64 * self.cost_model.row_read_cost,
239 QueryOperation::FullScan => self.total_edges as f64 * self.cost_model.seq_scan_cost,
240 QueryOperation::VectorSearch { .. } => self.cost_model.vector_search_cost,
241 _ => estimated_rows as f64 * self.cost_model.row_read_cost,
242 };
243
244 QueryPlan {
245 cost: QueryCost {
246 estimated_cost,
247 total_cost: estimated_cost,
248 estimated_rows,
249 records_returned: estimated_rows,
250 index: index.clone(),
251 operation: operation.clone(),
252 breakdown: vec![(operation.clone(), estimated_cost)],
253 },
254 predicates: predicates.to_vec(),
255 direction: TraversalDirection::Forward,
256 limit,
257 index_selection: index,
258 operations: vec![operation],
259 }
260 }
261
262 pub fn estimate_selectivity(&self, predicate: &QueryPredicate) -> f64 {
264 match predicate {
265 QueryPredicate::Eq(col, _) => {
266 if let Some(hint) = self.cardinality_hints.get(col) {
267 1.0 / hint.cardinality.max(1) as f64
268 } else {
269 0.1 }
271 }
272 QueryPredicate::Range { .. } => 0.25, QueryPredicate::In(_, values) => (values.len() as f64 * 0.1).min(0.5),
274 QueryPredicate::Prefix(_, _) => 0.15, QueryPredicate::TimeRange(_, _) => 0.2,
276 QueryPredicate::Project(_) => 0.1,
277 QueryPredicate::Tenant(_) => 0.05,
278 QueryPredicate::SpanType(_) => 0.15,
279 }
280 }
281}