Skip to main content

dbx_core/engine/
workload_analyzer.rs

1//! 적응형 워크로드 분석기
2//!
3//! 최근 쿼리 N개 중 OLTP(포인트 쿼리) vs OLAP(풀스캔/집계) 비율을 추적하고
4//! 최적 설정을 추천합니다.
5//!
6//! # 동작 원리
7//!
8//! 1. `record(pattern)` : 각 쿼리 실행 후 패턴 기록
9//! 2. `oltp_ratio()` : 최근 window 내 OLTP 비율 반환 (0.0~1.0)
10//! 3. `recommended_config()` : OLTP/OLAP 비율에 따른 추천 설정 반환
11
12use std::collections::VecDeque;
13
14/// 쿼리 패턴 분류
15#[derive(Debug, Clone, PartialEq)]
16pub enum QueryPattern {
17    /// OLTP: 단일 키 조회, INSERT, UPDATE
18    PointQuery,
19    /// OLAP: 범위 스캔
20    RangeScan,
21    /// OLAP: 집계 (COUNT, SUM, AVG 등)
22    Aggregation,
23    /// OLAP: 조인
24    Join,
25}
26
27impl QueryPattern {
28    /// OLTP 패턴 여부
29    pub fn is_oltp(&self) -> bool {
30        matches!(self, QueryPattern::PointQuery)
31    }
32}
33
34/// 적응형 워크로드 설정
35#[derive(Debug, Clone, PartialEq)]
36pub struct AdaptiveConfig {
37    /// Delta 최대 크기 (행 수). OLTP에서 작게, OLAP에서 크게.
38    pub delta_max_rows: usize,
39    /// Columnar Cache 최대 크기 (MB). OLAP에서 크게.
40    pub cache_max_mb: usize,
41    /// Compaction 주기 (초). OLAP에서 자주.
42    pub compaction_interval_secs: u64,
43}
44
45impl AdaptiveConfig {
46    /// OLTP 중심 (포인트 쿼리 70%+)
47    pub fn oltp_heavy() -> Self {
48        Self {
49            delta_max_rows: 1_000,          // 작은 delta → 빠른 쓰기
50            cache_max_mb: 64,               // 캐시 최소화
51            compaction_interval_secs: 3600, // 드물게 compaction
52        }
53    }
54
55    /// OLAP 중심 (포인트 쿼리 30%-)
56    pub fn olap_heavy() -> Self {
57        Self {
58            delta_max_rows: 100_000,       // 큰 delta → 배치 쓰기 효율
59            cache_max_mb: 1_024,           // 큰 캐시 → 쿼리 가속
60            compaction_interval_secs: 300, // 자주 compaction
61        }
62    }
63
64    /// 균형 (30~70%)
65    pub fn balanced() -> Self {
66        Self {
67            delta_max_rows: 10_000,
68            cache_max_mb: 256,
69            compaction_interval_secs: 900,
70        }
71    }
72}
73
74/// 적응형 워크로드 분석기
75///
76/// 슬라이딩 윈도우로 최근 쿼리 패턴을 추적합니다.
77pub struct WorkloadAnalyzer {
78    window: VecDeque<QueryPattern>,
79    window_size: usize,
80}
81
82impl WorkloadAnalyzer {
83    /// 새 분석기 생성. window_size는 추적할 최근 쿼리 수.
84    pub fn new(window_size: usize) -> Self {
85        Self {
86            window: VecDeque::with_capacity(window_size),
87            window_size,
88        }
89    }
90
91    /// 기본 윈도우(1000)로 분석기 생성
92    pub fn default_window() -> Self {
93        Self::new(1_000)
94    }
95
96    /// 쿼리 패턴 기록
97    pub fn record(&mut self, pattern: QueryPattern) {
98        if self.window.len() >= self.window_size {
99            self.window.pop_front();
100        }
101        self.window.push_back(pattern);
102    }
103
104    /// 0.0 = 순수 OLAP, 1.0 = 순수 OLTP
105    /// 윈도우가 비어있으면 0.5 (중립) 반환
106    pub fn oltp_ratio(&self) -> f64 {
107        if self.window.is_empty() {
108            return 0.5;
109        }
110        let oltp_count = self.window.iter().filter(|p| p.is_oltp()).count();
111        oltp_count as f64 / self.window.len() as f64
112    }
113
114    /// 현재 워크로드에 맞는 추천 설정 반환
115    pub fn recommended_config(&self) -> AdaptiveConfig {
116        let ratio = self.oltp_ratio();
117        if ratio > 0.7 {
118            AdaptiveConfig::oltp_heavy()
119        } else if ratio < 0.3 {
120            AdaptiveConfig::olap_heavy()
121        } else {
122            AdaptiveConfig::balanced()
123        }
124    }
125
126    /// 현재 윈도우 내 쿼리 수
127    pub fn window_len(&self) -> usize {
128        self.window.len()
129    }
130
131    /// 분석기 초기화 (윈도우 비우기)
132    pub fn reset(&mut self) {
133        self.window.clear();
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_oltp_ratio_empty() {
143        let analyzer = WorkloadAnalyzer::new(100);
144        assert!((analyzer.oltp_ratio() - 0.5).abs() < f64::EPSILON);
145    }
146
147    #[test]
148    fn test_oltp_ratio_pure_oltp() {
149        let mut analyzer = WorkloadAnalyzer::new(10);
150        for _ in 0..10 {
151            analyzer.record(QueryPattern::PointQuery);
152        }
153        assert!((analyzer.oltp_ratio() - 1.0).abs() < f64::EPSILON);
154    }
155
156    #[test]
157    fn test_oltp_ratio_pure_olap() {
158        let mut analyzer = WorkloadAnalyzer::new(10);
159        for _ in 0..5 {
160            analyzer.record(QueryPattern::Aggregation);
161            analyzer.record(QueryPattern::RangeScan);
162        }
163        assert!((analyzer.oltp_ratio() - 0.0).abs() < f64::EPSILON);
164    }
165
166    #[test]
167    fn test_recommended_config_oltp_heavy() {
168        let mut analyzer = WorkloadAnalyzer::new(100);
169        for _ in 0..80 {
170            analyzer.record(QueryPattern::PointQuery);
171        }
172        for _ in 0..20 {
173            analyzer.record(QueryPattern::RangeScan);
174        }
175        let config = analyzer.recommended_config();
176        assert_eq!(config, AdaptiveConfig::oltp_heavy());
177    }
178
179    #[test]
180    fn test_recommended_config_olap_heavy() {
181        let mut analyzer = WorkloadAnalyzer::new(100);
182        for _ in 0..10 {
183            analyzer.record(QueryPattern::PointQuery);
184        }
185        for _ in 0..90 {
186            analyzer.record(QueryPattern::Aggregation);
187        }
188        let config = analyzer.recommended_config();
189        assert_eq!(config, AdaptiveConfig::olap_heavy());
190    }
191
192    #[test]
193    fn test_recommended_config_balanced() {
194        let mut analyzer = WorkloadAnalyzer::new(100);
195        for _ in 0..50 {
196            analyzer.record(QueryPattern::PointQuery);
197        }
198        for _ in 0..50 {
199            analyzer.record(QueryPattern::Join);
200        }
201        let config = analyzer.recommended_config();
202        assert_eq!(config, AdaptiveConfig::balanced());
203    }
204
205    #[test]
206    fn test_sliding_window_eviction() {
207        let mut analyzer = WorkloadAnalyzer::new(5);
208        // 5개 OLAP 채우기
209        for _ in 0..5 {
210            analyzer.record(QueryPattern::Aggregation);
211        }
212        assert!((analyzer.oltp_ratio() - 0.0).abs() < f64::EPSILON);
213
214        // OLTP 3개 추가 → 오래된 OLAP 3개 퇴출
215        for _ in 0..3 {
216            analyzer.record(QueryPattern::PointQuery);
217        }
218        // 윈도우: [OLAP, OLAP, OLTP, OLTP, OLTP]
219        assert!((analyzer.oltp_ratio() - 0.6).abs() < 1e-9);
220    }
221
222    #[test]
223    fn test_reset() {
224        let mut analyzer = WorkloadAnalyzer::new(10);
225        for _ in 0..5 {
226            analyzer.record(QueryPattern::PointQuery);
227        }
228        assert_eq!(analyzer.window_len(), 5);
229        analyzer.reset();
230        assert_eq!(analyzer.window_len(), 0);
231        assert!((analyzer.oltp_ratio() - 0.5).abs() < f64::EPSILON);
232    }
233}