diff --git a/src/gausse/storage/database.py b/src/gausse/storage/database.py index 031a58d..57f695f 100644 --- a/src/gausse/storage/database.py +++ b/src/gausse/storage/database.py @@ -22,6 +22,10 @@ def open_connection(db_path: Path) -> sqlite3.Connection: db_path.parent.mkdir(parents=True, exist_ok=True) conn = sqlite3.connect(str(db_path)) conn.execute("PRAGMA journal_mode=WAL") + # не давать WAL расти безгранично: при потоке вставок 400/с и постоянных + # читателях (веб) авто-чекпоинт голодает, WAL раздувался до гигабайт и + # замедлял ВСЕ запросы. Лимит + периодический чекпоинт в писателе. + conn.execute("PRAGMA journal_size_limit=67108864") # 64МБ conn.execute(CREATE_RUNS_TABLE_SQL) for statement in CREATE_INDEXES_SQL: conn.execute(statement) @@ -175,12 +179,19 @@ def fetch_runs( limit: int | None = None, ) -> list[RunRecord]: conn.row_factory = sqlite3.Row - query = "SELECT * FROM runs WHERE 1=1" + # топы дашборда: принуждаем частичный DESC-индекс (INDEXED BY) — иначе + # планировщик берёт idx_runs_feasible и сортирует сотни тысяч строк + # TEMP B-TREE'ом (~2с на запрос). Условие feasible=1 обязано быть + # ЛИТЕРАЛОМ: частичный индекс не матчится с bound-параметром. + forced_index = "" + if feasible is True and min_efficiency is None: + if order_by_velocity_desc: + forced_index = " INDEXED BY idx_runs_feas_vel" + elif order_by_efficiency_desc: + forced_index = " INDEXED BY idx_runs_feas_eff" + query = f"SELECT * FROM runs{forced_index} WHERE 1=1" params: list = [] if feasible is not None: - # ЛИТЕРАЛ, не параметр: частичные индексы (… WHERE feasible=1) SQLite - # применяет только когда условие видно при подготовке запроса; с - # `feasible = ?` топ-15 сканировал сотни тысяч строк (~1.2с) query += f" AND feasible = {int(feasible)}" if min_efficiency is not None: query += " AND efficiency >= ?" @@ -219,10 +230,16 @@ def run_writer_process(queue, db_path: Path) -> None: """ import queue as queue_mod import signal + import time as time_mod signal.signal(signal.SIGTERM, signal.SIG_IGN) BATCH = 500 + CHECKPOINT_EVERY_S = 60.0 conn = open_connection(db_path) + # если база старая (без агрегатов) — пересобрать ДО первых вставок, + # иначе счётчики начнут вестись только с нуля и разойдутся с таблицей + ensure_stats(conn) + last_checkpoint = time_mod.monotonic() try: finished = False while not finished: @@ -246,5 +263,11 @@ def run_writer_process(queue, db_path: Path) -> None: insert_run(conn, record) except sqlite3.Error as exc: print(f"[storage] не удалось записать run_id={record.run_id}: {exc}", file=sys.stderr) + # авто-чекпоинт голодает при постоянном потоке — двигаем WAL сами, + # PASSIVE не блокирует читателей + if time_mod.monotonic() - last_checkpoint > CHECKPOINT_EVERY_S: + conn.execute("PRAGMA wal_checkpoint(PASSIVE)") + last_checkpoint = time_mod.monotonic() finally: + conn.execute("PRAGMA wal_checkpoint(PASSIVE)") conn.close()