Skip to main content

oxirs_arq/advanced_optimizer/
streaming_analyzer.rs

1//! Streaming Query Analyzer
2//!
3//! This module provides comprehensive analysis and optimization for streaming query execution,
4//! including memory management, spilling policies, streaming strategies, and pipeline analysis.
5
6use crate::algebra::Algebra;
7use anyhow::Result;
8use std::sync::atomic::{AtomicU64, Ordering};
9use std::sync::Arc;
10
11/// Streaming query analyzer with advanced memory-aware optimization
12#[derive(Clone)]
13pub struct StreamingAnalyzer {
14    config: StreamingConfig,
15    current_memory_bytes: Arc<AtomicU64>,
16    spill_policies: Vec<SpillPolicy>,
17}
18
19/// Configuration for streaming execution
20#[derive(Debug, Clone)]
21pub struct StreamingConfig {
22    pub enable_streaming: bool,
23    pub memory_threshold_mb: usize,   // 2048 MB default
24    pub spill_threshold_percent: f64, // 0.8 (80%)
25    pub streaming_batch_size: usize,  // 1000 rows
26}
27
28impl Default for StreamingConfig {
29    fn default() -> Self {
30        Self {
31            enable_streaming: true,
32            memory_threshold_mb: 2048,
33            spill_threshold_percent: 0.8,
34            streaming_batch_size: 1000,
35        }
36    }
37}
38
39/// Streaming execution strategy
40#[derive(Debug, Clone)]
41pub struct StreamingStrategy {
42    pub strategy_type: StreamingType,
43    pub memory_limit: usize,
44    pub batch_size: usize,
45    pub spill_threshold: f64,
46    pub parallelism_degree: usize,
47}
48
49/// Types of streaming strategies
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum StreamingType {
52    PipelineBreaker,
53    HashJoinStreaming,
54    SortMergeStreaming,
55    NestedLoopStreaming,
56    IndexNestedLoop,
57    HybridStreaming,
58}
59
60/// Spill policy for memory management
61#[derive(Debug, Clone)]
62pub struct SpillPolicy {
63    pub policy_type: SpillType,
64    pub threshold: f64,
65    pub target_operators: Vec<String>,
66    pub cost_factor: f64,
67}
68
69/// Types of spill policies
70#[derive(Debug, Clone, PartialEq, Eq)]
71pub enum SpillType {
72    LeastRecentlyUsed,
73    LargestFirst,
74    CostBased,
75    PredictiveBased,
76}
77
78/// Streaming opportunities identified in query plan
79#[derive(Debug, Clone, Default)]
80pub struct StreamingOpportunities {
81    pub streamable_scans: Vec<OperatorId>,
82    pub streamable_filters: Vec<OperatorId>,
83    pub streamable_projects: Vec<OperatorId>,
84    pub requires_materialization: Vec<(OperatorId, &'static str)>,
85    pub pipeline_breakers: Vec<OperatorId>,
86    pub estimated_memory_savings_mb: usize,
87}
88
89/// Operator identifier
90#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
91pub struct OperatorId(u64);
92
93impl OperatorId {
94    fn new(id: u64) -> Self {
95        Self(id)
96    }
97}
98
99/// Query plan representation for streaming analysis
100#[derive(Debug, Clone)]
101pub struct QueryPlan {
102    operators: Vec<Operator>,
103    operator_id_counter: u64,
104}
105
106impl QueryPlan {
107    pub fn from_algebra(algebra: &Algebra) -> Self {
108        let mut plan = Self {
109            operators: Vec::new(),
110            operator_id_counter: 0,
111        };
112        plan.build_from_algebra(algebra);
113        plan
114    }
115
116    fn build_from_algebra(&mut self, algebra: &Algebra) {
117        match algebra {
118            Algebra::Bgp(patterns) => {
119                let id = self.next_id();
120                self.operators.push(Operator::Scan(ScanOperator {
121                    id,
122                    patterns: patterns.len(),
123                }));
124            }
125            Algebra::Filter { pattern, .. } => {
126                self.build_from_algebra(pattern);
127                let id = self.next_id();
128                self.operators.push(Operator::Filter(FilterOperator { id }));
129            }
130            Algebra::Project { pattern, variables } => {
131                self.build_from_algebra(pattern);
132                let id = self.next_id();
133                self.operators.push(Operator::Project(ProjectOperator {
134                    id,
135                    num_vars: variables.len(),
136                }));
137            }
138            Algebra::Join { left, right } => {
139                self.build_from_algebra(left);
140                self.build_from_algebra(right);
141                let id = self.next_id();
142                self.operators
143                    .push(Operator::HashJoin(HashJoinOperator { id }));
144            }
145            Algebra::LeftJoin { left, right, .. } => {
146                self.build_from_algebra(left);
147                self.build_from_algebra(right);
148                let id = self.next_id();
149                self.operators
150                    .push(Operator::HashJoin(HashJoinOperator { id }));
151            }
152            Algebra::Union { left, right } => {
153                self.build_from_algebra(left);
154                self.build_from_algebra(right);
155                let id = self.next_id();
156                self.operators.push(Operator::Union(UnionOperator { id }));
157            }
158            Algebra::OrderBy { pattern, .. } => {
159                self.build_from_algebra(pattern);
160                let id = self.next_id();
161                self.operators.push(Operator::Sort(SortOperator { id }));
162            }
163            Algebra::Group { pattern, .. } => {
164                self.build_from_algebra(pattern);
165                let id = self.next_id();
166                self.operators
167                    .push(Operator::Aggregation(AggregationOperator { id }));
168            }
169            Algebra::Distinct { pattern } => {
170                self.build_from_algebra(pattern);
171                let id = self.next_id();
172                self.operators
173                    .push(Operator::Distinct(DistinctOperator { id }));
174            }
175            Algebra::Slice { pattern, .. } => {
176                self.build_from_algebra(pattern);
177                let id = self.next_id();
178                self.operators.push(Operator::Limit(LimitOperator { id }));
179            }
180            _ => {
181                // Handle other operators generically
182                let id = self.next_id();
183                self.operators
184                    .push(Operator::Generic(GenericOperator { id }));
185            }
186        }
187    }
188
189    fn next_id(&mut self) -> OperatorId {
190        let id = OperatorId::new(self.operator_id_counter);
191        self.operator_id_counter += 1;
192        id
193    }
194
195    pub fn operators(&self) -> &[Operator] {
196        &self.operators
197    }
198}
199
200/// Query operators
201#[derive(Debug, Clone)]
202pub enum Operator {
203    Scan(ScanOperator),
204    Filter(FilterOperator),
205    Project(ProjectOperator),
206    HashJoin(HashJoinOperator),
207    SortMergeJoin(SortMergeJoinOperator),
208    Sort(SortOperator),
209    Aggregation(AggregationOperator),
210    Distinct(DistinctOperator),
211    Union(UnionOperator),
212    Limit(LimitOperator),
213    Generic(GenericOperator),
214}
215
216impl Operator {
217    pub fn id(&self) -> OperatorId {
218        match self {
219            Operator::Scan(op) => op.id,
220            Operator::Filter(op) => op.id,
221            Operator::Project(op) => op.id,
222            Operator::HashJoin(op) => op.id,
223            Operator::SortMergeJoin(op) => op.id,
224            Operator::Sort(op) => op.id,
225            Operator::Aggregation(op) => op.id,
226            Operator::Distinct(op) => op.id,
227            Operator::Union(op) => op.id,
228            Operator::Limit(op) => op.id,
229            Operator::Generic(op) => op.id,
230        }
231    }
232}
233
234#[derive(Debug, Clone)]
235pub struct ScanOperator {
236    pub id: OperatorId,
237    pub patterns: usize,
238}
239
240#[derive(Debug, Clone)]
241pub struct FilterOperator {
242    pub id: OperatorId,
243}
244
245#[derive(Debug, Clone)]
246pub struct ProjectOperator {
247    pub id: OperatorId,
248    pub num_vars: usize,
249}
250
251#[derive(Debug, Clone)]
252pub struct HashJoinOperator {
253    pub id: OperatorId,
254}
255
256#[derive(Debug, Clone)]
257pub struct SortMergeJoinOperator {
258    pub id: OperatorId,
259}
260
261#[derive(Debug, Clone)]
262pub struct SortOperator {
263    pub id: OperatorId,
264}
265
266#[derive(Debug, Clone)]
267pub struct AggregationOperator {
268    pub id: OperatorId,
269}
270
271#[derive(Debug, Clone)]
272pub struct DistinctOperator {
273    pub id: OperatorId,
274}
275
276#[derive(Debug, Clone)]
277pub struct UnionOperator {
278    pub id: OperatorId,
279}
280
281#[derive(Debug, Clone)]
282pub struct LimitOperator {
283    pub id: OperatorId,
284}
285
286#[derive(Debug, Clone)]
287pub struct GenericOperator {
288    pub id: OperatorId,
289}
290
291impl StreamingAnalyzer {
292    /// Create a new streaming analyzer
293    pub fn new(config: StreamingConfig) -> Self {
294        Self {
295            config,
296            current_memory_bytes: Arc::new(AtomicU64::new(0)),
297            spill_policies: Vec::new(),
298        }
299    }
300
301    /// Analyze query plan for streaming opportunities
302    pub fn analyze(&self, plan: &QueryPlan) -> StreamingOpportunities {
303        let mut opportunities = StreamingOpportunities::default();
304
305        // Identify streaming operators
306        for op in plan.operators() {
307            match op {
308                Operator::Scan(_) => {
309                    // Always streamable
310                    opportunities.streamable_scans.push(op.id());
311                }
312                Operator::Filter(_)
313                    // Streamable if input is streamable
314                    if self.is_input_streamable(op, &opportunities) => {
315                        opportunities.streamable_filters.push(op.id());
316                    }
317                Operator::Project(_) => {
318                    // Streamable projection
319                    opportunities.streamable_projects.push(op.id());
320                }
321                Operator::HashJoin(_) => {
322                    // Hash join: Build side must materialize, probe side can stream
323                    opportunities
324                        .requires_materialization
325                        .push((op.id(), "build_side"));
326                }
327                Operator::SortMergeJoin(_) => {
328                    // Both sides must be sorted (materialization required)
329                    opportunities
330                        .requires_materialization
331                        .push((op.id(), "both_sides"));
332                }
333                Operator::Sort(_) => {
334                    // Sorting requires full materialization
335                    opportunities.pipeline_breakers.push(op.id());
336                }
337                Operator::Aggregation(_) => {
338                    // Aggregation requires materialization (unless streaming aggregates)
339                    opportunities.pipeline_breakers.push(op.id());
340                }
341                Operator::Distinct(_) => {
342                    // Distinct requires materialization
343                    opportunities.pipeline_breakers.push(op.id());
344                }
345                Operator::Union(_) => {
346                    // Non-streaming union requires materialization
347                    opportunities.pipeline_breakers.push(op.id());
348                }
349                _ => {}
350            }
351        }
352
353        // Estimate memory savings
354        opportunities.estimated_memory_savings_mb = self.estimate_memory_savings(&opportunities);
355
356        opportunities
357    }
358
359    /// Check if input to operator is streamable
360    fn is_input_streamable(
361        &self,
362        _operator: &Operator,
363        opportunities: &StreamingOpportunities,
364    ) -> bool {
365        // Simplified: Check if most upstream operators are streamable
366        !opportunities.streamable_scans.is_empty() || !opportunities.streamable_filters.is_empty()
367    }
368
369    /// Estimate memory savings from streaming
370    fn estimate_memory_savings(&self, opportunities: &StreamingOpportunities) -> usize {
371        // Simple heuristic: Each streamable operator saves ~100MB
372        let num_streamable = opportunities.streamable_scans.len()
373            + opportunities.streamable_filters.len()
374            + opportunities.streamable_projects.len();
375        num_streamable * 100 // MB per operator
376    }
377
378    /// Determine if operator should stream
379    pub fn should_stream(&self, _operator: &Operator, estimated_size: usize) -> bool {
380        if !self.config.enable_streaming {
381            return false;
382        }
383
384        // Stream if estimated result size > memory threshold
385        let threshold_bytes = self.config.memory_threshold_mb * 1024 * 1024;
386        estimated_size > threshold_bytes
387    }
388
389    /// Identify pipeline breakers (operators that block streaming)
390    pub fn find_pipeline_breakers(&self, plan: &QueryPlan) -> Vec<OperatorId> {
391        let mut breakers = vec![];
392
393        for op in plan.operators() {
394            if self.is_pipeline_breaker(op) {
395                breakers.push(op.id());
396            }
397        }
398
399        breakers
400    }
401
402    fn is_pipeline_breaker(&self, op: &Operator) -> bool {
403        matches!(
404            op,
405            Operator::Sort(_)
406                | Operator::Aggregation(_)
407                | Operator::Distinct(_)
408                | Operator::Union(_)
409        )
410    }
411
412    /// Convert materialized operator to streaming
413    pub fn convert_to_streaming(&self, operator: &mut Operator) -> Result<()> {
414        match operator {
415            Operator::HashJoin(_) => {
416                // Use streaming probe side
417                // In a real implementation, we'd modify the operator's internal state
418                Ok(())
419            }
420            Operator::Aggregation(_) => {
421                // Convert to streaming aggregation (if grouping keys sortable)
422                // Check if can stream
423                Ok(())
424            }
425            _ => Ok(()),
426        }
427    }
428
429    /// Analyze streaming potential for query
430    pub fn analyze_streaming_potential(
431        &self,
432        algebra: &Algebra,
433    ) -> Result<Option<StreamingStrategy>> {
434        let plan = QueryPlan::from_algebra(algebra);
435        let opportunities = self.analyze(&plan);
436
437        // If we have streaming opportunities, return a strategy
438        if !opportunities.streamable_scans.is_empty() {
439            Ok(Some(StreamingStrategy {
440                strategy_type: StreamingType::HashJoinStreaming,
441                memory_limit: self.config.memory_threshold_mb * 1024 * 1024,
442                batch_size: self.config.streaming_batch_size,
443                spill_threshold: self.config.spill_threshold_percent,
444                parallelism_degree: std::thread::available_parallelism()
445                    .map(|n| n.get())
446                    .unwrap_or(1),
447            }))
448        } else {
449            Ok(None)
450        }
451    }
452
453    /// Get memory threshold
454    pub fn memory_threshold(&self) -> usize {
455        self.config.memory_threshold_mb * 1024 * 1024
456    }
457
458    /// Update memory threshold
459    pub fn set_memory_threshold(&mut self, threshold_mb: usize) {
460        self.config.memory_threshold_mb = threshold_mb;
461    }
462
463    /// Add spill policy
464    pub fn add_spill_policy(&mut self, policy: SpillPolicy) {
465        self.spill_policies.push(policy);
466    }
467
468    /// Get active spill policies
469    pub fn spill_policies(&self) -> &[SpillPolicy] {
470        &self.spill_policies
471    }
472
473    /// Get the count of optimizations applied
474    pub fn optimizations_count(&self) -> usize {
475        // Return count based on config
476        if self.config.enable_streaming {
477            1
478        } else {
479            0
480        }
481    }
482
483    /// Get current memory usage
484    pub fn current_memory_usage(&self) -> usize {
485        self.current_memory_bytes.load(Ordering::Relaxed) as usize
486    }
487
488    /// Check if should spill to disk
489    pub fn should_spill(&self) -> bool {
490        let current_usage = self.current_memory_usage();
491        let threshold =
492            (self.memory_threshold() as f64 * self.config.spill_threshold_percent) as usize;
493        current_usage > threshold
494    }
495
496    /// Analyze query complexity for streaming decision
497    pub fn analyze_query_complexity(&self, algebra: &Algebra) -> QueryComplexity {
498        let mut complexity = QueryComplexity::default();
499        self.compute_complexity(algebra, &mut complexity);
500        complexity
501    }
502
503    fn compute_complexity(&self, algebra: &Algebra, complexity: &mut QueryComplexity) {
504        match algebra {
505            Algebra::Bgp(patterns) => {
506                complexity.num_patterns += patterns.len();
507            }
508            Algebra::Join { left, right }
509            | Algebra::Union { left, right }
510            | Algebra::LeftJoin { left, right, .. } => {
511                complexity.num_joins += 1;
512                self.compute_complexity(left, complexity);
513                self.compute_complexity(right, complexity);
514            }
515            Algebra::Filter { pattern, .. } => {
516                complexity.num_filters += 1;
517                self.compute_complexity(pattern, complexity);
518            }
519            Algebra::OrderBy { pattern, .. } => {
520                complexity.num_sorts += 1;
521                self.compute_complexity(pattern, complexity);
522            }
523            Algebra::Group { pattern, .. } => {
524                complexity.num_aggregations += 1;
525                self.compute_complexity(pattern, complexity);
526            }
527            _ => {}
528        }
529    }
530}
531
532/// Query complexity metrics
533#[derive(Debug, Clone, Default)]
534pub struct QueryComplexity {
535    pub num_patterns: usize,
536    pub num_joins: usize,
537    pub num_filters: usize,
538    pub num_sorts: usize,
539    pub num_aggregations: usize,
540}
541
542impl QueryComplexity {
543    pub fn total_complexity(&self) -> usize {
544        self.num_patterns
545            + self.num_joins * 2
546            + self.num_filters
547            + self.num_sorts * 3
548            + self.num_aggregations * 2
549    }
550}
551
552#[cfg(test)]
553mod tests {
554    use super::*;
555
556    #[test]
557    fn test_streaming_analyzer_creation() {
558        let config = StreamingConfig::default();
559        let analyzer = StreamingAnalyzer::new(config);
560        assert_eq!(analyzer.memory_threshold(), 2048 * 1024 * 1024);
561    }
562
563    #[test]
564    fn test_add_spill_policy_is_retained() {
565        let config = StreamingConfig::default();
566        let mut analyzer = StreamingAnalyzer::new(config);
567        assert!(analyzer.spill_policies().is_empty());
568
569        analyzer.add_spill_policy(SpillPolicy {
570            policy_type: SpillType::LeastRecentlyUsed,
571            threshold: 0.75,
572            target_operators: vec!["scan-1".to_string()],
573            cost_factor: 1.5,
574        });
575        analyzer.add_spill_policy(SpillPolicy {
576            policy_type: SpillType::CostBased,
577            threshold: 0.9,
578            target_operators: vec!["sort-2".to_string()],
579            cost_factor: 2.0,
580        });
581
582        let policies = analyzer.spill_policies();
583        assert_eq!(policies.len(), 2);
584        assert_eq!(policies[0].policy_type, SpillType::LeastRecentlyUsed);
585        assert_eq!(policies[1].policy_type, SpillType::CostBased);
586        assert_eq!(policies[1].target_operators, vec!["sort-2".to_string()]);
587    }
588
589    #[test]
590    fn test_should_stream_decision() {
591        let config = StreamingConfig {
592            enable_streaming: true,
593            memory_threshold_mb: 1024,
594            spill_threshold_percent: 0.8,
595            streaming_batch_size: 1000,
596        };
597        let analyzer = StreamingAnalyzer::new(config);
598
599        // Small data should not stream
600        assert!(!analyzer.should_stream(
601            &Operator::Scan(ScanOperator {
602                id: OperatorId::new(1),
603                patterns: 1
604            }),
605            100 * 1024 * 1024
606        ));
607
608        // Large data should stream
609        assert!(analyzer.should_stream(
610            &Operator::Scan(ScanOperator {
611                id: OperatorId::new(1),
612                patterns: 1
613            }),
614            2048 * 1024 * 1024
615        ));
616    }
617
618    #[test]
619    fn test_pipeline_breaker_detection() {
620        let config = StreamingConfig::default();
621        let analyzer = StreamingAnalyzer::new(config);
622
623        assert!(analyzer.is_pipeline_breaker(&Operator::Sort(SortOperator {
624            id: OperatorId::new(1)
625        })));
626        assert!(
627            analyzer.is_pipeline_breaker(&Operator::Aggregation(AggregationOperator {
628                id: OperatorId::new(2)
629            }))
630        );
631        assert!(
632            !analyzer.is_pipeline_breaker(&Operator::Filter(FilterOperator {
633                id: OperatorId::new(3)
634            }))
635        );
636    }
637}