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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user