Skip to content

Commit d20ca55

Browse files
Merge pull request #291 from TemoaProject/perf/sqllite_optimization
perf: sqlite performance improvements
2 parents ec0e2e5 + ccfa004 commit d20ca55

6 files changed

Lines changed: 146 additions & 2 deletions

File tree

docs/source/database.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,5 +213,18 @@ Users can configure the cycle detection behavior using the following settings:
213213
Note that the myopic mode *requires* the use of Source Tracing to ensure accuracy as some orphans
214214
may be produced by endogenous decisions in myopic runs.
215215

216+
SQLite Performance Tuning
217+
-------------------------
218+
219+
For large-scale models or long-running simulation modes (such as myopic or MGA), database I/O can become a performance bottleneck. Temoa allows you to tune the SQLite connection parameters in your configuration file under the ``[sqlite]`` section.
220+
221+
The following settings are available:
222+
223+
* **journal_mode**: Sets the SQLite journaling mode. Default is ``WAL`` (Write-Ahead Logging), which provides better performance and concurrency. Note that this will create temporary ``-wal`` and ``-shm`` files alongside your database during execution.
224+
* **synchronous**: Controls how frequently SQLite flushes data to disk. Default is ``NORMAL``, which provides a good balance between speed and safety.
225+
* **mmap_size**: The maximum number of bytes for memory-mapped I/O. Default is 8GB (``8589934592``). This allows SQLite to access the database file directly from memory, significantly speeding up reads for large databases.
226+
* **cache_size**: The number of pages or the size in KiB for the SQLite page cache. If negative, it specifies size in KiB. Default is 500MiB (``-512000``).
227+
228+
These settings are especially impactful in **myopic mode**, where Temoa frequently updates and queries the database between period iterations. By default, Temoa also disables the per-period ``VACUUM`` operation in myopic runs to avoid redundant and expensive full-database rewrites.
216229

217230
.. _sqlite: https://www.sqlite.org/

temoa/_internal/temoa_sequencer.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from temoa.extensions.single_vector_mga.sv_mga_sequencer import SvMgaSequencer
3636
from temoa.extensions.stochastics.stochastic_sequencer import StochasticSequencer
3737
from temoa.model_checking.pricing_check import price_checker
38+
from temoa.utilities.sqlite_utils import tune_sqlite_connection
3839

3940
if TYPE_CHECKING:
4041
import pyomo.opt
@@ -134,6 +135,7 @@ def build_model(self) -> TemoaModel:
134135
raise RuntimeError('Database version check failed. See log file for details.')
135136

136137
with sqlite3.connect(self.config.input_database) as con:
138+
tune_sqlite_connection(con, self.config)
137139
hybrid_loader = HybridLoader(db_connection=con, config=self.config)
138140
data_portal = hybrid_loader.load_data_portal(myopic_index=None)
139141
instance = build_instance(data_portal, silent=self.config.silent)
@@ -203,6 +205,7 @@ def start(self) -> None:
203205
def _run_check_mode(self) -> None:
204206
"""Encapsulated logic for the CHECK mode."""
205207
with sqlite3.connect(self.config.input_database) as con:
208+
tune_sqlite_connection(con, self.config)
206209
if not self.config.source_trace:
207210
logger.warning('Source trace is automatically enabled for CHECK mode.')
208211
self.config.source_trace = True
@@ -221,6 +224,7 @@ def _run_check_mode(self) -> None:
221224
def _run_perfect_foresight(self) -> None:
222225
"""Encapsulated logic for the PERFECT_FORESIGHT mode."""
223226
with sqlite3.connect(self.config.input_database) as con:
227+
tune_sqlite_connection(con, self.config)
224228
hybrid_loader = HybridLoader(db_connection=con, config=self.config)
225229
data_portal = hybrid_loader.load_data_portal(myopic_index=None)
226230
instance = build_instance(

temoa/core/config.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,50 @@ def __init__(
159159
self.output_threshold_activity = output_threshold_activity
160160
self.output_threshold_emission = output_threshold_emission
161161
self.output_threshold_cost = output_threshold_cost
162+
self.sqlite_inputs = sqlite or {}
163+
164+
# SQLite performance settings
165+
# journal_mode: DELETE | TRUNCATE | PERSIST | MEMORY | WAL | OFF
166+
jm_allowed = {'DELETE', 'TRUNCATE', 'PERSIST', 'MEMORY', 'WAL', 'OFF'}
167+
jm = self.sqlite_inputs.get('journal_mode', 'WAL')
168+
if isinstance(jm, str) and jm.upper() in jm_allowed:
169+
self.sqlite_journal_mode: str | int = jm.upper()
170+
elif isinstance(jm, (int, float, str)) and str(jm).isdigit():
171+
self.sqlite_journal_mode = int(jm)
172+
else:
173+
self.sqlite_journal_mode = 'WAL'
174+
175+
# synchronous: OFF (0) | NORMAL (1) | FULL (2) | EXTRA (3)
176+
sync_allowed = {'OFF', 'NORMAL', 'FULL', 'EXTRA'}
177+
sync = self.sqlite_inputs.get('synchronous', 'NORMAL')
178+
if isinstance(sync, str) and sync.upper() in sync_allowed:
179+
self.sqlite_synchronous: str | int = sync.upper()
180+
elif isinstance(sync, (int, float, str)) and str(sync).isdigit():
181+
self.sqlite_synchronous = int(sync)
182+
else:
183+
self.sqlite_synchronous = 'NORMAL'
184+
185+
# temp_store: DEFAULT (0) | FILE (1) | MEMORY (2)
186+
temp_allowed = {'DEFAULT', 'FILE', 'MEMORY'}
187+
ts = self.sqlite_inputs.get('temp_store', 'MEMORY')
188+
if isinstance(ts, str) and ts.upper() in temp_allowed:
189+
self.sqlite_temp_store: str | int = ts.upper()
190+
elif isinstance(ts, (int, float, str)) and str(ts).isdigit():
191+
self.sqlite_temp_store = int(ts)
192+
else:
193+
self.sqlite_temp_store = 'MEMORY'
194+
195+
mmap_size = self.sqlite_inputs.get('mmap_size', 8589934592)
196+
if isinstance(mmap_size, (int, float, str)):
197+
self.sqlite_mmap_size = int(mmap_size)
198+
else:
199+
self.sqlite_mmap_size = 8589934592
200+
201+
cache_size = self.sqlite_inputs.get('cache_size', -512000)
202+
if isinstance(cache_size, (int, float, str)):
203+
self.sqlite_cache_size = int(cache_size)
204+
else:
205+
self.sqlite_cache_size = -512000
162206

163207
# Cycle detection limits
164208
if not isinstance(cycle_count_limit, int) or cycle_count_limit < -1:
@@ -306,6 +350,15 @@ def __repr__(self) -> str:
306350
msg += '{:>{}s}: {}\n'.format('Save duals to output db', width, self.save_duals)
307351
msg += '{:>{}s}: {}\n'.format('Save storage to output db', width, self.save_storage_levels)
308352

353+
msg += spacer
354+
msg += '{:>{}s}: {}\n'.format('SQLite journal mode', width, self.sqlite_journal_mode)
355+
msg += '{:>{}s}: {}\n'.format('SQLite synchronous', width, self.sqlite_synchronous)
356+
msg += '{:>{}s}: {}\n'.format('SQLite temp store', width, self.sqlite_temp_store)
357+
msg += '{:>{}s}: {}\n'.format('SQLite mmap size (bytes)', width, self.sqlite_mmap_size)
358+
msg += '{:>{}s}: {}\n'.format(
359+
'SQLite cache size (pages or KiB if negative)', width, self.sqlite_cache_size
360+
)
361+
309362
msg += spacer
310363
msg += '{:>{}s}: {}\n'.format('Time sequencing', width, self.time_sequencing)
311364
msg += '{:>{}s}: {}\n'.format('Days per period', width, self.days_per_period)

temoa/extensions/myopic/myopic_sequencer.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from temoa.extensions.myopic.myopic_index import MyopicIndex
2222
from temoa.extensions.myopic.myopic_progress_mapper import MyopicProgressMapper
2323
from temoa.model_checking.pricing_check import price_checker
24+
from temoa.utilities.sqlite_utils import tune_sqlite_connection
2425

2526
logger = logging.getLogger(__name__)
2627

@@ -164,6 +165,8 @@ def get_connection(self) -> Connection:
164165
logger.error('Run aborted. I/O database pointers are different')
165166
sys.exit(-1)
166167

168+
tune_sqlite_connection(con, self.config)
169+
167170
return con
168171

169172
def start(self) -> None:
@@ -303,8 +306,6 @@ def start(self) -> None:
303306
)
304307
self.output_con.commit()
305308

306-
# 11. Compact the db... lots of writes/deletes leads to bloat
307-
self.output_con.execute('VACUUM;')
308309

309310
# Total system cost is, theoretically, sum of discounted costs from output_cost table
310311
total_cost = self.get_current_total_cost(last_base_year if last_base_year is not None else 0)

temoa/tutorial_assets/config_sample.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,30 @@ cycle_count_limit = 100
5959
# Use this to filter out very small cycles if needed
6060
cycle_length_limit = 1
6161

62+
# ------------------------------------
63+
# SQLITE PERFORMANCE TUNING
64+
# ------------------------------------
65+
66+
[sqlite]
67+
# These settings improve database performance, especially for large-scale
68+
# runs and myopic/MGA modes which perform many small writes.
69+
70+
# journal_mode: WAL (Write-Ahead Logging) provides better concurrency and speed.
71+
# Note: This creates sidecar files (-wal and -shm) during execution.
72+
journal_mode = 'WAL'
73+
74+
# synchronous: NORMAL reduces disk flushes while remaining safe against
75+
# application-level crashes.
76+
synchronous = 'NORMAL'
77+
78+
# mmap_size: Memory-map the database file for faster reads (bytes).
79+
# 8589934592 = 8GB
80+
mmap_size = 8589934592
81+
82+
# cache_size: SQLite page cache size. Negative values specify size in KiB.
83+
# -512000 = 500MiB
84+
cache_size = -512000
85+
6286
# ------------------------------------
6387
# SOLVER
6488
# Solver Selection

temoa/utilities/sqlite_utils.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""
2+
Utilities for SQLite performance tuning in Temoa.
3+
"""
4+
5+
import logging
6+
import sqlite3
7+
from typing import TYPE_CHECKING
8+
9+
if TYPE_CHECKING:
10+
from temoa.core.config import TemoaConfig
11+
12+
logger = logging.getLogger(__name__)
13+
14+
15+
def tune_sqlite_connection(con: sqlite3.Connection, config: 'TemoaConfig | None' = None) -> None:
16+
"""
17+
Apply performance-tuning PRAGMAs to a SQLite connection.
18+
19+
Args:
20+
con: The sqlite3.Connection object to tune.
21+
config: Optional TemoaConfig object to override defaults.
22+
"""
23+
journal_mode = 'WAL'
24+
synchronous = 'NORMAL'
25+
temp_store = 'MEMORY'
26+
mmap_size = 8589934592 # 8GB
27+
cache_size = -512000 # 500MB (negative means KiB)
28+
29+
if config:
30+
journal_mode = getattr(config, 'sqlite_journal_mode', journal_mode)
31+
synchronous = getattr(config, 'sqlite_synchronous', synchronous)
32+
temp_store = getattr(config, 'sqlite_temp_store', temp_store)
33+
mmap_size = getattr(config, 'sqlite_mmap_size', mmap_size)
34+
cache_size = getattr(config, 'sqlite_cache_size', cache_size)
35+
36+
pragmas = [
37+
('journal_mode', journal_mode),
38+
('synchronous', synchronous),
39+
('temp_store', temp_store),
40+
('mmap_size', mmap_size),
41+
('cache_size', cache_size),
42+
]
43+
44+
for name, value in pragmas:
45+
try:
46+
con.execute(f'PRAGMA {name} = {value}')
47+
logger.debug('Applied SQLite PRAGMA: %s = %s', name, value)
48+
except sqlite3.Error as e:
49+
logger.warning('Failed to apply SQLite PRAGMA %s: %s', name, e)

0 commit comments

Comments
 (0)