SQLite: обуздать WAL (4.4ГБ!) + принудить частичные индексы топов

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 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-07-08 05:14:44 +05:00
parent b43885c6e1
commit b2fc51bb72

View File

@@ -22,6 +22,10 @@ def open_connection(db_path: Path) -> sqlite3.Connection:
db_path.parent.mkdir(parents=True, exist_ok=True) db_path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(db_path)) conn = sqlite3.connect(str(db_path))
conn.execute("PRAGMA journal_mode=WAL") conn.execute("PRAGMA journal_mode=WAL")
# не давать WAL расти безгранично: при потоке вставок 400/с и постоянных
# читателях (веб) авто-чекпоинт голодает, WAL раздувался до гигабайт и
# замедлял ВСЕ запросы. Лимит + периодический чекпоинт в писателе.
conn.execute("PRAGMA journal_size_limit=67108864") # 64МБ
conn.execute(CREATE_RUNS_TABLE_SQL) conn.execute(CREATE_RUNS_TABLE_SQL)
for statement in CREATE_INDEXES_SQL: for statement in CREATE_INDEXES_SQL:
conn.execute(statement) conn.execute(statement)
@@ -175,12 +179,19 @@ def fetch_runs(
limit: int | None = None, limit: int | None = None,
) -> list[RunRecord]: ) -> list[RunRecord]:
conn.row_factory = sqlite3.Row 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 = [] params: list = []
if feasible is not None: if feasible is not None:
# ЛИТЕРАЛ, не параметр: частичные индексы (… WHERE feasible=1) SQLite
# применяет только когда условие видно при подготовке запроса; с
# `feasible = ?` топ-15 сканировал сотни тысяч строк (~1.2с)
query += f" AND feasible = {int(feasible)}" query += f" AND feasible = {int(feasible)}"
if min_efficiency is not None: if min_efficiency is not None:
query += " AND efficiency >= ?" query += " AND efficiency >= ?"
@@ -219,10 +230,16 @@ def run_writer_process(queue, db_path: Path) -> None:
""" """
import queue as queue_mod import queue as queue_mod
import signal import signal
import time as time_mod
signal.signal(signal.SIGTERM, signal.SIG_IGN) signal.signal(signal.SIGTERM, signal.SIG_IGN)
BATCH = 500 BATCH = 500
CHECKPOINT_EVERY_S = 60.0
conn = open_connection(db_path) conn = open_connection(db_path)
# если база старая (без агрегатов) — пересобрать ДО первых вставок,
# иначе счётчики начнут вестись только с нуля и разойдутся с таблицей
ensure_stats(conn)
last_checkpoint = time_mod.monotonic()
try: try:
finished = False finished = False
while not finished: while not finished:
@@ -246,5 +263,11 @@ def run_writer_process(queue, db_path: Path) -> None:
insert_run(conn, record) insert_run(conn, record)
except sqlite3.Error as exc: except sqlite3.Error as exc:
print(f"[storage] не удалось записать run_id={record.run_id}: {exc}", file=sys.stderr) 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: finally:
conn.execute("PRAGMA wal_checkpoint(PASSIVE)")
conn.close() conn.close()