From b2fc51bb72f8caeaf1849c0a37a2fe9363bd5396 Mon Sep 17 00:00:00 2001 From: jze9 Date: Wed, 8 Jul 2026 05:14:44 +0500 Subject: [PATCH] =?UTF-8?q?SQLite:=20=D0=BE=D0=B1=D1=83=D0=B7=D0=B4=D0=B0?= =?UTF-8?q?=D1=82=D1=8C=20WAL=20(4.4=D0=93=D0=91!)=20+=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B8=D0=BD=D1=83=D0=B4=D0=B8=D1=82=D1=8C=20=D1=87=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D0=B8=D1=87=D0=BD=D1=8B=D0=B5=20=D0=B8=D0=BD=D0=B4=D0=B5?= =?UTF-8?q?=D0=BA=D1=81=D1=8B=20=D1=82=D0=BE=D0=BF=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WAL раздувался до гигабайт (авто-чекпоинт голодал при 400 вставках/с и постоянных читателях) и замедлял ВСЕ запросы: journal_size_limit=64МБ + периодический wal_checkpoint(PASSIVE) в писателе. Планировщик игнорировал частичные DESC-индексы (брал idx_runs_feasible + TEMP B-TREE сортировку 620к строк) — принуждаем INDEXED BY. ensure_stats при старте писателя — закрыта гонка миграции счётчиков. Co-Authored-By: Claude Fable 5 --- src/gausse/storage/database.py | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) 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()