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