perf(db): kasuje ix_scenes_title_normalized, zbedny prefiks indeksu z 0039

ix_scenes_title_created_id_desc zaczyna sie od tej samej kolumny, wiec obsluguje wszystko,
co obslugiwal stary indeks: lookupy `title_normalized = X` z resolvera i skan zakresu przy
sortowaniu po tytule. Trzymanie obu to 367 MB na dysku i jeden indeks wiecej do utrzymania
przy kazdym zapisie do scenes, a ingest pisze tam caly czas.

Sprawdzone na produkcji przez BEGIN; DROP INDEX; EXPLAIN ANALYZE; ROLLBACK, zeby zobaczyc
wybor planera bez starego indeksu, a potem jeszcze raz po samym dropie:

  exact title lookup        1,76 ms -> 0,07 ms, Index Scan po nowym indeksie
  OR studio + exact title   ten sam ksztalt planu (BitmapOr), 3,7 ms
  OR data + exact title     Seq Scan przy szerokim oknie dat, ale to decyzja o release_date:
                            z enable_seqscan=off BitmapOr siega po nowy indeks
  trgm ILIKE                bez zmian, swoj indeks GIN

W wariancie z OR planer i tak siegal juz po nowy indeks, jeszcze zanim stary znikl.
Indeksy scenes: 3544 MB -> 3177 MB. Listy scen po dropie bez zmian (tytul 22-54 ms,
studio 38-73 ms na stronach 1-5).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
goon-foss 2026-09-16 10:47:24 +02:00
parent 470beb4aa6
commit b6a075b28a
3 changed files with 55 additions and 7 deletions

View file

@ -15,8 +15,9 @@ Pomiar na produkcji 2026-09-16, ten sam zestaw filtrow, po rozgrzewce:
Kierunki w indeksie musza byc dokladnie takie jak w ORDER BY, bo sort jest mieszany
(ASC, DESC, DESC) i inaczej skan nie przejdzie po kolei.
`ix_scenes_title_normalized` zostaje: sluzy tez zwyklym lookupom po tytule (2,2 mln
skanow), a odpiecie go od tego indeksu to osobna decyzja, nie skutek uboczny tej migracji.
Ten indeks zaczyna sie od `title_normalized`, wiec obsluguje tez zwykle lookupy po tytule
(2,2 mln skanow), przez ktore stary `ix_scenes_title_normalized` staje sie zbedny. Usuwa go
migracja 0040, osobno, zeby dalo sie cofnac samo usuniecie.
CONCURRENTLY w autocommit_block, jak w 0035 i 0038. Nieudany CONCURRENTLY zostawia indeks
INVALID; wtedy DROP INDEX i ponownie.

View file

@ -0,0 +1,46 @@
"""Usuwa ix_scenes_title_normalized, zastapiony prefiksem indeksu z 0039.
`ix_scenes_title_created_id_desc (title_normalized, created_at DESC, id DESC)` zaczyna sie
od tej samej kolumny, wiec obsluguje wszystko, co obslugiwal stary indeks: lookupy
`title_normalized = X` z resolvera (`scene_match.blocking_candidates`,
`scene_resolver` przy trybie agregatora) i skanowanie zakresu przy sortowaniu po tytule.
Trzymanie obu to 367 MB na dysku i jeden indeks wiecej do utrzymania przy kazdym zapisie
do `scenes`, a ingest pisze do tej tabeli caly czas.
Sprawdzone na produkcji 2026-09-16 przez `BEGIN; DROP INDEX ...; EXPLAIN ANALYZE ...;
ROLLBACK;`, zeby zobaczyc wybor planera bez starego indeksu:
exact title lookup Index Scan ix_scenes_title_normalized 1,76 ms
-> Index Scan ix_scenes_title_created_id_desc 0,05 ms
OR studio + exact title Bitmap Index Scan, ten sam ksztalt planu
W wariancie z OR planer i tak siegal juz po nowy indeks, jeszcze zanim stary znikl.
CONCURRENTLY w autocommit_block: DROP bez CONCURRENTLY bierze ACCESS EXCLUSIVE i zatrzymuje
ingest na czas operacji.
Revision ID: 0040_drop_scenes_title_norm_idx
Revises: 0039_scenes_title_created_id_idx
Create Date: 2026-09-16
"""
from __future__ import annotations
from alembic import op
revision = "0040_drop_scenes_title_norm_idx"
down_revision: str | None = "0039_scenes_title_created_id_idx"
branch_labels = None
depends_on = None
def upgrade() -> None:
with op.get_context().autocommit_block():
op.execute("DROP INDEX CONCURRENTLY IF EXISTS ix_scenes_title_normalized")
def downgrade() -> None:
with op.get_context().autocommit_block():
op.execute(
"CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_scenes_title_normalized "
"ON scenes (title_normalized)"
)

View file

@ -608,11 +608,12 @@ def list_scenes(
)
elif sort == "title":
# Cale ORDER BY (razem z tie-breakami) pokrywa ix_scenes_title_created_id_desc
# (migracja 0039). Sam `ix_scenes_title_normalized` zostawial Postgresowi
# Incremental Sort, ktory przed oddaniem czegokolwiek MUSI zebrac caly kubelek
# o tym samym tytule: na stronie 1 glownego feedu ciagnal 1084 wiersze zamiast
# 51 (709 ms), a z filtrem taga szedl w minuty. Z pelnym indeksem sortu nie ma
# wcale, skan urywa sie na 51. wierszu (5-12 ms).
# (migracja 0039). Wczesniejszy indeks na samym `title_normalized` zostawial
# Postgresowi Incremental Sort, a ten przed oddaniem czegokolwiek MUSI zebrac caly
# kubelek scen o tym samym tytule: na stronie 1 glownego feedu ciagnal 1084 wiersze
# zamiast 51 (709 ms), a z filtrem taga szedl w minuty. Z pelnym indeksem wezla
# sortu nie ma wcale, skan urywa sie na 51. wierszu (5-12 ms). Stary indeks byl
# prefiksem nowego, wiec 0040 go kasuje.
ordered = base.order_by(
Scene.title_normalized.asc(), Scene.created_at.desc(), Scene.id.desc()
)