linkrs_query/optimizer/cost/config.rs
1//! Cost Model Configuration Module
2//!
3//! Provide configurable cost parameters, referring to the design of the cost model in PostgreSQL.
4//!
5//! ## Usage Examples
6//!
7//! ```rust
8//! use linkrs_query::optimizer::cost::CostModelConfig;
9//!
10//! // Use the default configuration
11//! let config = CostModelConfig::default();
12//!
13//! // Optimizations for SSDs
14//! let ssd_config = CostModelConfig::for_ssd();
15//!
16//! // Custom configuration
17//! let custom_config = CostModelConfig {
18//! seq_page_cost: 0.5,
19//! random_page_cost: 1.0,
20//! ..Default::default()
21//! };
22//! ```
23
24/// Cost Model Configuration
25///
26/// Define the cost parameters for various operations, which are used to calculate the execution cost of the query plan.
27/// Refer to the design of the PostgreSQL cost model and extend it to take into account the characteristics of graph databases.
28/// These parameters can be adjusted according to the hardware environment.
29///
30/// Unit notes: `effective_cache_pages` counts pages (`ROWS_PER_PAGE` rows
31/// per page); `memory_sort_threshold` counts rows. The two must not be
32/// compared directly without converting via `ROWS_PER_PAGE`.
33#[derive(Debug, Clone, Copy, PartialEq)]
34pub struct CostModelConfig {
35 // ==================== Basic I/O Cost Parameters (Consistent with PostgreSQL) ====================
36 /// Alternative to reading pages in order
37 ///
38 /// The cost of sequentially reading a disk page. Default value 1.0
39 /// In an SSD environment, the requirements can be appropriately reduced.
40 pub seq_page_cost: f64,
41
42 /// Alternative to reading a random page:
43 ///
44 /// The cost of reading a random disk page. Default value 4.0
45 /// On traditional mechanical hard drives, random access is much slower than sequential access.
46 /// In an SSD environment, the value of `seq_page_cost` can be approached.
47 pub random_page_cost: f64,
48
49 /// Cost of CPU processing per row
50 ///
51 /// CPU cost to process each row of data. Default value 0.01
52 pub cpu_tuple_cost: f64,
53
54 /// Cost of processing index rows
55 ///
56 /// CPU cost to process each index entry. Default value 0.005
57 /// It is usually lower than `cpu_tuple_cost` because the index entries are smaller.
58 pub cpu_index_tuple_cost: f64,
59
60 /// The cost of operator calculations
61 ///
62 /// The CPU cost of executing each operator or function. Default value 0.0025
63 pub cpu_operator_cost: f64,
64
65 // ==================== Algorithm-related parameters ====================
66 /// Hash construction overhead coefficient
67 ///
68 /// Extra overhead factor for building the hash table. Default value 0.1
69 pub hash_build_overhead: f64,
70
71 /// Sorting and comparing cost coefficients
72 ///
73 /// The cost factor for each comparison operation. Default value 1.0
74 pub sort_comparison_cost: f64,
75
76 /// Memory sorting threshold (number of rows)
77 ///
78 /// If this threshold is exceeded, external sorting will be used. The default value is 10000.
79 pub memory_sort_threshold: u64,
80
81 /// Cost of external sorting pages
82 ///
83 /// The cost of reading and writing temporary files while externally sorting. Default value 2.0
84 pub external_sort_page_cost: f64,
85
86 // ==================== Parameters specific to graph databases ====================
87 /// Edge traversal cost
88 ///
89 /// The cost of traversing an edge, which is more complex than vertex processing. Default value 0.02
90 pub edge_traversal_cost: f64,
91
92 /// Multi-hop traversal with an incrementing coefficient at each step
93 ///
94 /// The factor by which the cost is incremented for each additional hop. Default value 1.2
95 pub multi_hop_penalty: f64,
96
97 /// Cost of neighbor node lookup
98 ///
99 /// The cost of finding a neighbor node. Default value 0.015
100 pub neighbor_lookup_cost: f64,
101
102 /// Effective cache size (number of pages)
103 ///
104 /// Used for cost calculation in cache-aware scenarios. Default value: 10000
105 pub effective_cache_pages: u64,
106
107 /// Cache Hit Cost Coefficient
108 ///
109 /// The cost factor for when the data is in the cache. Default value 0.1
110 pub cache_hit_cost_factor: f64,
111
112 /// Fundamentals of the cost in shortest path algorithms
113 ///
114 /// Fixed overhead for the shortest path algorithm. Default value 10.0
115 pub shortest_path_base_cost: f64,
116
117 /// Path enumeration exponent coefficient
118 ///
119 /// The complexity factor when enumerating all paths. Default value 2.0
120 pub path_enumeration_factor: f64,
121
122 /// Super Node Threshold (Degree)
123 ///
124 /// Nodes that exceed this degree are considered super nodes. The default value is 10000.
125 pub super_node_threshold: u64,
126
127 /// Super Node Additional Cost Coefficient
128 ///
129 /// Additional cost when super nodes are involved. Default value 2.0
130 pub super_node_penalty: f64,
131
132 // ==================== Default parameters for control flow ====================
133 /// The default list size for “Unwind”.
134 ///
135 /// Default value used when the list size cannot be inferred from an expression. Default value 3.0
136 pub default_unwind_list_size: f64,
137
138 /// The default number of iterations for the Loop function
139 ///
140 /// The default value used when the number of iterations cannot be determined from the conditions. The default value is 3.
141 pub default_loop_iterations: u32,
142
143 /// Select the default number of branches.
144 ///
145 /// The default value used when the number of branches cannot be determined. The default value is 2.
146 pub default_select_branches: usize,
147
148 // ==================== Memory and expression cost parameters ====================
149 /// Cost per byte of memory usage
150 ///
151 /// Cost per byte of memory usage (used to estimate memory pressure). Default value 0.0001
152 pub memory_byte_cost: f64,
153
154 /// Memory pressure threshold (bytes)
155 ///
156 /// Memory pressure threshold (exceeding this value increases the cost penalty). Default value 100MB
157 pub memory_pressure_threshold: usize,
158
159 /// Memory pressure penalty factor
160 ///
161 /// Memory pressure penalty factor. Default value 2.0
162 pub memory_pressure_penalty: f64,
163
164 /// Simple expression (leaf node) cost
165 ///
166 /// Simple expression (leaf node) cost. Default value 0.001
167 pub simple_expression_cost: f64,
168
169 /// Function call base cost
170 ///
171 /// The function call base cost. Default value 0.01
172 pub function_call_base_cost: f64,
173
174 /// Expression nesting cost per level
175 ///
176 /// Nesting cost per level for complex expressions. Default value 0.005
177 pub expression_nesting_cost: f64,
178
179 /// Fixed-size type cost factor
180 ///
181 /// Fixed size type processing cost factor. Default value 1.0
182 pub fixed_type_cost_factor: f64,
183
184 /// Variable-length type cost factor
185 ///
186 /// Variable length type processing cost factor. Default value 1.5
187 pub variable_type_cost_factor: f64,
188
189 /// Complex type cost factor
190 ///
191 /// Complex type processing cost factor. Default value 2.0
192 pub complex_type_cost_factor: f64,
193
194 /// Graph type cost factor
195 ///
196 /// Chart type processing cost factor. Default value 3.0
197 pub graph_type_cost_factor: f64,
198
199 /// Average row width in bytes for memory estimates.
200 ///
201 /// Stack size of one `Value` (`ESTIMATED_ROW_WIDTH_BYTES`); heap
202 /// payloads of variable-length values are not included, so wide rows
203 /// are systematically underestimated. All memory estimates (sort,
204 /// hash join, aggregation, budget allocation) share this single
205 /// source instead of private copies; per-space overrides flow through
206 /// the existing cost-config mechanism.
207 pub row_width_bytes: usize,
208
209 // ==================== Strategy Threshold Parameters ====================
210 /// Strategy threshold configuration
211 ///
212 /// Policy threshold configuration for controlling the behavior of various optimization policies
213 pub strategy_thresholds: StrategyThresholds,
214}
215
216/// Single source for estimation fallbacks shared by cost and planning layers.
217///
218/// `UNKNOWN_SCAN_ROWS` (1000) is the only no-statistics scan default;
219/// `DEFAULT_FANOUT` (10) is the only neighborhood fanout default;
220/// `DEFAULT_DEGREE_FALLBACK` (2.0) is the only per-edge average-degree
221/// fallback; `ROWS_PER_PAGE` (100) is the only page/row conversion;
222/// `SUPER_NODE_THRESHOLD` (10000) is the only super-node degree threshold;
223/// `TOPN_RATIO` (10) with `TOPN_MIN_ROWS` is the only sort-to-TopN rule.
224/// Point lookups use `GET_VERTICES_DEFAULT_ROWS` (1) and
225/// `GET_EDGES_DEFAULT_ROWS` (10); dedup uses `DEDUP_SELECTIVITY` (0.8).
226/// Skew judgments use the `SKEW_*` thresholds so edge statistics and
227/// traversal estimation share one definition.
228/// Row width is derived from the real `Value` layout so memory estimates
229/// track column types instead of a hardcoded 64.
230pub const UNKNOWN_SCAN_ROWS: u64 = 1_000;
231pub const DEFAULT_FANOUT: u64 = 10;
232pub const DEFAULT_DEGREE_FALLBACK: f64 = 2.0;
233pub const ROWS_PER_PAGE: u64 = 100;
234pub const SUPER_NODE_THRESHOLD: u64 = 10_000;
235pub const TOPN_RATIO: u64 = 10;
236pub const TOPN_MIN_ROWS: u64 = 10_000;
237pub const GET_VERTICES_DEFAULT_ROWS: u64 = 1;
238pub const GET_EDGES_DEFAULT_ROWS: u64 = 10;
239pub const DEDUP_SELECTIVITY: f64 = 0.8;
240pub const SKEW_SEVERE_GINI: f64 = 0.7;
241pub const SKEW_MODERATE_GINI: f64 = 0.5;
242pub const SKEW_MILD_GINI: f64 = 0.3;
243pub const SKEW_MAX_DEGREE_RATIO: f64 = 10.0;
244pub const SKEW_DIRECTION_RATIO: f64 = 5.0;
245pub const SKEW_SEVERE_PENALTY: f64 = 2.0;
246pub const SKEW_MODERATE_PENALTY: f64 = 1.5;
247pub const SKEW_MILD_PENALTY: f64 = 1.2;
248pub const SKEW_SEVERE_ROW_FACTOR: f64 = 1.5;
249pub const SKEW_MODERATE_ROW_FACTOR: f64 = 1.3;
250pub const SKEW_MILD_ROW_FACTOR: f64 = 1.1;
251/// Row width derived from the real value layout.
252///
253/// Default for `CostModelConfig::row_width_bytes`; stack size only, heap
254/// payloads excluded.
255pub const ESTIMATED_ROW_WIDTH_BYTES: usize = std::mem::size_of::<linkrs_core::Value>();
256
257/// Strategy Threshold Configuration
258///
259/// Define threshold parameters for various optimization strategies that are used to control the strategy selection behavior.
260/// These parameters can be adjusted according to the workload characteristics.
261#[derive(Debug, Clone, Copy, PartialEq)]
262pub struct StrategyThresholds {
263 // ==================== Aggregate Strategy ====================
264 /// Small dataset threshold for aggregate strategy
265 ///
266 /// Small dataset threshold (number of rows) below which the simple aggregation strategy is used. Default value 1000
267 pub small_dataset_threshold: u64,
268
269 /// Low cardinality threshold for aggregate strategy
270 ///
271 /// Low base threshold below which sorted aggregation is used. Default value 100
272 pub low_cardinality_threshold: u64,
273
274 /// High cardinality ratio threshold
275 ///
276 /// High base scale threshold (relative to the number of input lines) above which streaming aggregation is used. Default value 0.1
277 pub high_cardinality_ratio: f64,
278
279 // ==================== Traversal Strategy ====================
280 // Super-node threshold is the single `CostModelConfig::super_node_threshold`
281 // (`SUPER_NODE_THRESHOLD`); no separate traversal copy is kept.
282 /// Bidirectional traversal savings threshold
283 ///
284 /// Bidirectional traversal savings threshold above which bidirectional traversal is used. Default value 0.3
285 pub bidirectional_savings_threshold: f64,
286
287 /// Default branching factor for traversal
288 ///
289 /// Traverses the default branching factor for cost estimation. Default value 2.0
290 pub default_branching_factor: f64,
291
292 // ==================== TopN Strategy ====================
293 /// TopN selectivity threshold
294 ///
295 /// TopN selectivity threshold below which TopN optimization is used. Default value 0.1
296 pub topn_threshold: f64,
297 /// TopN default limit
298 ///
299 /// TopN defaults to limiting the number of rows. Default value 100
300 pub topn_default_limit: u64,
301
302 // ==================== Join reorder review ====================
303 /// Minimum relative improvement for the join-order reviewer.
304 ///
305 /// The reviewer rewrites a join chain only when the proposed order
306 /// costs less than `current * (1 - this)`. Zero keeps model authority
307 /// (any strict improvement wins, exact ties keep the current order so
308 /// plans never flap between equal-cost shapes); raise it to calm
309 /// replanning churn when statistics wobble. Clamped to `[0, 1)`.
310 pub join_reorder_min_improvement: f64,
311
312 // ==================== Materialization Strategy ====================
313 /// Maximum result rows for CTE materialization
314 ///
315 /// Maximum number of result rows for CTE materialization. Default value 10000
316 pub max_result_rows: u64,
317
318 /// Minimum reference count for CTE materialization
319 ///
320 /// Minimum number of references for CTE materialization. Default value 2
321 pub min_reference_count: usize,
322
323 /// Minimum complexity score for CTE materialization
324 ///
325 /// Minimum complexity score for CTE materialization. Default value 5.0
326 pub min_complexity_score: f64,
327}
328
329impl Default for StrategyThresholds {
330 fn default() -> Self {
331 Self {
332 // Aggregate strategy
333 small_dataset_threshold: 1000,
334 low_cardinality_threshold: 100,
335 high_cardinality_ratio: 0.1,
336 // Traversal strategy: super-node uses CostModelConfig::super_node_threshold.
337 bidirectional_savings_threshold: 0.3,
338 default_branching_factor: DEFAULT_DEGREE_FALLBACK,
339 // TopN strategy: threshold derives from the sort-to-TopN ratio so
340 // the two spellings cannot drift apart.
341 topn_threshold: 1.0 / TOPN_RATIO as f64,
342 topn_default_limit: 100,
343 // Join reorder review: any strict improvement wins by default.
344 join_reorder_min_improvement: 0.0,
345 // Materialization strategy
346 max_result_rows: 10000,
347 min_reference_count: 2,
348 min_complexity_score: 5.0,
349 }
350 }
351}
352
353impl Default for CostModelConfig {
354 fn default() -> Self {
355 Self {
356 // Basic I/O cost parameters (consistent with PostgreSQL)
357 seq_page_cost: 1.0,
358 random_page_cost: 4.0,
359 cpu_tuple_cost: 0.01,
360 cpu_index_tuple_cost: 0.005,
361 cpu_operator_cost: 0.0025,
362 // Algorithm-related parameters: row-count threshold.
363 hash_build_overhead: 0.1,
364 sort_comparison_cost: 1.0,
365 memory_sort_threshold: TOPN_MIN_ROWS,
366 external_sort_page_cost: 2.0,
367 // Parameter specific to graph databases
368 edge_traversal_cost: 0.02,
369 multi_hop_penalty: 1.2,
370 neighbor_lookup_cost: 0.015,
371 effective_cache_pages: 10000,
372 cache_hit_cost_factor: 0.1,
373 shortest_path_base_cost: 10.0,
374 path_enumeration_factor: 2.0,
375 super_node_threshold: SUPER_NODE_THRESHOLD,
376 super_node_penalty: 2.0,
377 // Default parameters for control flow
378 default_unwind_list_size: 3.0,
379 default_loop_iterations: 3,
380 default_select_branches: 2,
381 // Memory and expression cost parameters
382 memory_byte_cost: 0.0001,
383 memory_pressure_threshold: 100 * 1024 * 1024, // 100MB
384 memory_pressure_penalty: 2.0,
385 simple_expression_cost: 0.001,
386 function_call_base_cost: 0.01,
387 expression_nesting_cost: 0.005,
388 fixed_type_cost_factor: 1.0,
389 variable_type_cost_factor: 1.5,
390 complex_type_cost_factor: 2.0,
391 graph_type_cost_factor: 3.0,
392 row_width_bytes: ESTIMATED_ROW_WIDTH_BYTES,
393 // Strategy thresholds
394 strategy_thresholds: StrategyThresholds::default(),
395 }
396 }
397}
398
399impl CostModelConfig {
400 /// Create the default configuration.
401 pub fn new() -> Self {
402 Self::default()
403 }
404
405 /// Optimizations for SSD storage
406 ///
407 /// The random access performance of SSDs is close to that of sequential access; therefore, reducing the value of `random_page_cost` has a positive impact on overall system performance.
408 pub fn for_ssd() -> Self {
409 Self {
410 random_page_cost: 1.1, // Random access to SSDs is similar to sequential access.
411 ..Default::default()
412 }
413 }
414
415 /// Optimizations for in-memory databases
416 ///
417 /// There are no disk I/O overheads in memory; the main consideration is the cost associated with the CPU.
418 pub fn for_in_memory() -> Self {
419 Self {
420 seq_page_cost: 0.1,
421 random_page_cost: 0.1,
422 cache_hit_cost_factor: 0.01, // A cache hit incurs almost no cost.
423 ..Default::default()
424 }
425 }
426
427 /// Optimizations for mechanical hard drives (conservative configuration)
428 ///
429 /// Traditional mechanical hard drives have poor random access performance.
430 pub fn for_hdd() -> Self {
431 Self {
432 seq_page_cost: 1.0,
433 random_page_cost: 4.0,
434 ..Default::default()
435 }
436 }
437
438 /// Set the order for reading pages as an alternative option.
439 pub fn with_seq_page_cost(mut self, cost: f64) -> Self {
440 self.seq_page_cost = cost;
441 self
442 }
443
444 /// Set a substitute price for random page reading.
445 pub fn with_random_page_cost(mut self, cost: f64) -> Self {
446 self.random_page_cost = cost;
447 self
448 }
449
450 /// Setting the CPU cost for line processing
451 pub fn with_cpu_tuple_cost(mut self, cost: f64) -> Self {
452 self.cpu_tuple_cost = cost;
453 self
454 }
455
456 /// Setting the cost of processing index rows
457 pub fn with_cpu_index_tuple_cost(mut self, cost: f64) -> Self {
458 self.cpu_index_tuple_cost = cost;
459 self
460 }
461
462 /// Setting the cost of operator calculations
463 pub fn with_cpu_operator_cost(mut self, cost: f64) -> Self {
464 self.cpu_operator_cost = cost;
465 self
466 }
467}
468
469#[cfg(test)]
470mod tests {
471 use super::*;
472
473 #[test]
474 fn test_default_config() {
475 let config = CostModelConfig::default();
476 // Basic I/O Cost Parameters
477 assert_eq!(config.seq_page_cost, 1.0);
478 assert_eq!(config.random_page_cost, 4.0);
479 assert_eq!(config.cpu_tuple_cost, 0.01);
480 assert_eq!(config.cpu_index_tuple_cost, 0.005);
481 assert_eq!(config.cpu_operator_cost, 0.0025);
482 // Algorithm-related parameters
483 assert_eq!(config.hash_build_overhead, 0.1);
484 assert_eq!(config.sort_comparison_cost, 1.0);
485 assert_eq!(config.memory_sort_threshold, 10000);
486 assert_eq!(config.external_sort_page_cost, 2.0);
487 // Unique parameters specific to graph databases
488 assert_eq!(config.edge_traversal_cost, 0.02);
489 assert_eq!(config.multi_hop_penalty, 1.2);
490 assert_eq!(config.neighbor_lookup_cost, 0.015);
491 assert_eq!(config.effective_cache_pages, 10000);
492 assert_eq!(config.cache_hit_cost_factor, 0.1);
493 assert_eq!(config.shortest_path_base_cost, 10.0);
494 assert_eq!(config.path_enumeration_factor, 2.0);
495 assert_eq!(config.super_node_threshold, 10000);
496 assert_eq!(config.super_node_penalty, 2.0);
497 // Default parameters for control flow
498 assert_eq!(config.default_unwind_list_size, 3.0);
499 assert_eq!(config.default_loop_iterations, 3);
500 assert_eq!(config.default_select_branches, 2);
501 }
502
503 #[test]
504 fn test_ssd_config() {
505 let config = CostModelConfig::for_ssd();
506 assert_eq!(config.random_page_cost, 1.1);
507 assert_eq!(config.seq_page_cost, 1.0); // The rest should remain as default.
508 }
509
510 #[test]
511 fn test_in_memory_config() {
512 let config = CostModelConfig::for_in_memory();
513 assert_eq!(config.seq_page_cost, 0.1);
514 assert_eq!(config.random_page_cost, 0.1);
515 }
516
517 #[test]
518 fn test_builder_pattern() {
519 let config = CostModelConfig::new()
520 .with_seq_page_cost(0.5)
521 .with_random_page_cost(2.0)
522 .with_cpu_tuple_cost(0.02);
523
524 assert_eq!(config.seq_page_cost, 0.5);
525 assert_eq!(config.random_page_cost, 2.0);
526 assert_eq!(config.cpu_tuple_cost, 0.02);
527 assert_eq!(config.cpu_index_tuple_cost, 0.005); // “Default”
528 }
529
530 #[test]
531 fn test_join_reorder_min_improvement_defaults_to_zero() {
532 let config = CostModelConfig::default();
533 assert_eq!(config.strategy_thresholds.join_reorder_min_improvement, 0.0);
534 }
535}