Skip to content

Commit 5eb98ba

Browse files
authored
Optimize sensor history queries and database performance (#944)
- Add SQLite performance PRAGMAs (WAL mode, memory temp store) to improve responsiveness on low-power hardware like Raspberry Pi - Refactor history downsampling logic to use a pre-calculated step based on total row count - Improve sampling efficiency for multi-sensor queries using per-sensor window partitions
1 parent 69e563f commit 5eb98ba

1 file changed

Lines changed: 43 additions & 18 deletions

File tree

‎backend/src/storage.rs‎

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -83,24 +83,42 @@ impl Storage {
8383
max_points: Option<u32>,
8484
) -> rusqlite::Result<Vec<SensorReading>> {
8585
let conn = self.conn.lock().await;
86+
87+
// 1. Performance PRAGMAs (Crucial for Raspberry Pi)
88+
conn.execute_batch(
89+
"
90+
PRAGMA journal_mode = WAL;
91+
PRAGMA temp_store = MEMORY;
92+
PRAGMA synchronous = NORMAL;
93+
",
94+
)?;
95+
8696
let cutoff = format!("-{hours} hours");
8797

8898
match (sensor_id, max_points) {
8999
(Some(sid), Some(max)) => {
100+
// Get total count first to determine sampling step
101+
let total: u32 = conn.query_row(
102+
"SELECT COUNT(*) FROM sensor_readings WHERE sensor_id = ?1 AND recorded_at > strftime('%Y-%m-%dT%H:%M:%SZ', 'now', ?2)",
103+
rusqlite::params![sid, cutoff],
104+
105+
|r| r.get(0),
106+
)?;
107+
let step = (total / max).max(1);
108+
90109
let mut stmt = conn.prepare_cached(
91110
"SELECT sensor_id, sensor_name, temperature, room_type, recorded_at
92111
FROM (
93-
SELECT sensor_id, sensor_name, temperature, room_type, recorded_at,
94-
ROW_NUMBER() OVER (ORDER BY recorded_at) AS rn,
95-
COUNT(*) OVER () AS total
112+
SELECT *, ROW_NUMBER() OVER (ORDER BY recorded_at) AS rn
96113
FROM sensor_readings
97114
WHERE sensor_id = ?1
98115
AND recorded_at > strftime('%Y-%m-%dT%H:%M:%SZ', 'now', ?2)
99116
)
100-
WHERE (rn - 1) % MAX(1, (total + ?3 - 1) / ?3) = 0
117+
WHERE rn % ?3 = 0
101118
ORDER BY recorded_at ASC",
102119
)?;
103-
let rows = stmt.query_map(rusqlite::params![sid, cutoff, max], row_to_reading)?
120+
let rows = stmt
121+
.query_map(rusqlite::params![sid, cutoff, step], row_to_reading)?
104122
.collect::<rusqlite::Result<Vec<_>>>();
105123
rows
106124
}
@@ -111,24 +129,28 @@ impl Storage {
111129
WHERE sensor_id = ?1 AND recorded_at > strftime('%Y-%m-%dT%H:%M:%SZ', 'now', ?2)
112130
ORDER BY recorded_at ASC",
113131
)?;
114-
let rows = stmt.query_map(rusqlite::params![sid, cutoff], row_to_reading)?
132+
let rows = stmt
133+
.query_map(rusqlite::params![sid, cutoff], row_to_reading)?
115134
.collect::<rusqlite::Result<Vec<_>>>();
116135
rows
117136
}
118137
(None, Some(max)) => {
138+
// For 'All Sensors', we use a sub-query to find the average density
139+
// or just apply a global row-number filter per sensor.
119140
let mut stmt = conn.prepare_cached(
120141
"SELECT sensor_id, sensor_name, temperature, room_type, recorded_at
121142
FROM (
122-
SELECT sensor_id, sensor_name, temperature, room_type, recorded_at,
123-
ROW_NUMBER() OVER (PARTITION BY sensor_id ORDER BY recorded_at) AS rn,
124-
COUNT(*) OVER (PARTITION BY sensor_id) AS total
143+
SELECT *, ROW_NUMBER() OVER (PARTITION BY sensor_id ORDER BY recorded_at) AS rn,
144+
COUNT(*) OVER (PARTITION BY sensor_id) as sensor_total
125145
FROM sensor_readings
126146
WHERE recorded_at > strftime('%Y-%m-%dT%H:%M:%SZ', 'now', ?1)
127147
)
128-
WHERE (rn - 1) % MAX(1, (total + ?2 - 1) / ?2) = 0
148+
-- Logic: Only return a row if it hits the step threshold for its specific sensor
149+
WHERE rn % MAX(1, sensor_total / ?2) = 0
129150
ORDER BY recorded_at ASC",
130151
)?;
131-
let rows = stmt.query_map(rusqlite::params![cutoff, max], row_to_reading)?
152+
let rows = stmt
153+
.query_map(rusqlite::params![cutoff, max], row_to_reading)?
132154
.collect::<rusqlite::Result<Vec<_>>>();
133155
rows
134156
}
@@ -139,7 +161,8 @@ impl Storage {
139161
WHERE recorded_at > strftime('%Y-%m-%dT%H:%M:%SZ', 'now', ?1)
140162
ORDER BY recorded_at ASC",
141163
)?;
142-
let rows = stmt.query_map(rusqlite::params![cutoff], row_to_reading)?
164+
let rows = stmt
165+
.query_map(rusqlite::params![cutoff], row_to_reading)?
143166
.collect::<rusqlite::Result<Vec<_>>>();
144167
rows
145168
}
@@ -148,11 +171,9 @@ impl Storage {
148171

149172
pub async fn get_settings(&self) -> rusqlite::Result<String> {
150173
let conn = self.conn.lock().await;
151-
conn.query_row(
152-
"SELECT data FROM user_settings WHERE id = 1",
153-
[],
154-
|row| row.get(0),
155-
)
174+
conn.query_row("SELECT data FROM user_settings WHERE id = 1", [], |row| {
175+
row.get(0)
176+
})
156177
}
157178

158179
pub async fn save_settings(&self, data: &str) -> rusqlite::Result<()> {
@@ -226,7 +247,11 @@ pub fn start_recording_loop(state: Arc<AppState>) {
226247

227248
tick_count += 1;
228249
if tick_count.is_multiple_of(288) {
229-
match state.storage.prune(state.settings.history_retention_days).await {
250+
match state
251+
.storage
252+
.prune(state.settings.history_retention_days)
253+
.await
254+
{
230255
Ok(n) if n > 0 => tracing::info!("Pruned {n} old sensor readings"),
231256
Err(e) => tracing::error!("Failed to prune old readings: {e}"),
232257
_ => {}

0 commit comments

Comments
 (0)