perf(api): tanszy odsiew stub-scen i zwarcie sprzecznego filtra playbacku
Some checks failed
Backend tests / test (push) Has been cancelled
Some checks failed
Backend tests / test (push) Has been cancelled
Trzy zmiany wokol listy scen z filtrem originu, gdzie kandydatow sa dziesiatki tysiecy. 1. Kolejnosc czlonow OR w stub_exclusion_clause. Postgres liczy OR po kolei i przerywa na pierwszym trafieniu, a sam czlonow nie przestawia. "Ma kanon" (jedyny czlon z JOIN-em do sources) stal przed tanszym "ma performera", wiec placilismy za niego takze za sceny, ktore performera mialy. Po zamianie ten czlon liczy sie 5146 razy zamiast 16 191. 2. Indeks scene_external_refs (scene_id, source_id), migracja 0041. Podzapytanie "ma kanon" dociagalo source_id z heapu, bo stary indeks zna tylko scene_id. Teraz idzie Index Only Scanem: Heap Fetches 16 191 -> 1241. Razem: sort=title + origin=hqporner, strona 5 czyta 251 tys. blokow zamiast 294 tys., a na rozgrzanym cache trwa 305 ms zamiast 371 ms (mediana z 3 rund z rotowana kolejnoscia). Zysk jest skromny. Pierwsze wykonanie na zimnym cache trwa 3-5 s i tak zostaje, bo trzeba dotknac ~2 GB; sprawdzilem tez wariant z materializacja scen originu i CTE, czyta wiecej blokow (302 tys.), wiec obecny plan zostaje. 3. has_playback=false RAZEM z filtrem originu to warunek sprzeczny, bo origin wymaga zrodla z dead_at IS NULL. Wynik jest pusty z definicji, ale Postgres przemielal caly katalog, zeby to stwierdzic: 148 s. Teraz zwarte na false(), 4,8 ms, dalej zero wynikow. Przy has_playback=true z originem pomijamy z kolei nadmiarowe EXISTS o zywym playbacku, bo origin sam go wymaga, a kosztowalo sonde na kazdym kandydacie. Wyniki bez zmian: strony 1-5 dla origin=hqporner i dla glownego feedu porownane pozycja po pozycji ze stara wersja daja zero roznic, a sam predykat stub zgadza sie na probce 200 tys. scen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
b6a075b28a
commit
2cdc3d69da
2 changed files with 82 additions and 5 deletions
|
|
@ -0,0 +1,52 @@
|
|||
"""Indeks pod czlon "ma kanon" w odsiewie stub-scen.
|
||||
|
||||
`stub_exclusion_clause` pyta, czy scena ma wpis z TPDB albo StashDB. To podzapytanie leci
|
||||
dla kazdego kandydata KAZDEJ listy scen, a `ix_scene_external_refs_scene_id` zna tylko
|
||||
`scene_id`, wiec `source_id` trzeba bylo dociagnac z heapu: jeden losowy odczyt na wpis,
|
||||
przy 5,1 mln wierszy i 2,2 GB tabeli.
|
||||
|
||||
Z `(scene_id, source_id)` to samo podzapytanie idzie Index Only Scanem. Pomiar na
|
||||
produkcji 2026-09-16 (sort=title + origin=hqporner, strona 5, 16 tys. wywolan tego
|
||||
podzapytania): Heap Fetches 16 191 -> 1241.
|
||||
|
||||
Razem ze zmiana kolejnosci OR w `stub_exclusion_clause` (tanszy czlon "ma performera"
|
||||
przed drozszym "ma kanon") to zapytanie czyta 251 tys. blokow zamiast 294 tys., a na
|
||||
rozgrzanym cache trwa 305 ms zamiast 371 ms (mediana z 3 rund z rotowana kolejnoscia).
|
||||
Zysk jest skromny i ograniczony do list, gdzie kandydatow sa dziesiatki tysiecy; na
|
||||
glownym feedzie to podzapytanie leci kilka razy na strone i zmiany nie widac.
|
||||
|
||||
Uwaga do pomiarow: pierwsze wykonanie tego zapytania na zimnym cache trwa 3-5 s i tak
|
||||
zostaje, bo trzeba dotknac ~2 GB. Wariant puszczany jako pierwszy wychodzi przez to
|
||||
kilkanascie razy wolniej, co przy pierwszym podejsciu dalo falszywe "10x szybciej".
|
||||
|
||||
`ix_scene_external_refs_scene_id` zostaje na razie, mimo ze jest prefiksem tego indeksu.
|
||||
Osobna decyzja, tak jak przy 0040.
|
||||
|
||||
CONCURRENTLY w autocommit_block, jak w 0035, 0038 i 0039. Nieudany CONCURRENTLY zostawia
|
||||
indeks INVALID; wtedy DROP INDEX i ponownie.
|
||||
|
||||
Revision ID: 0041_scene_ext_refs_scene_source
|
||||
Revises: 0040_drop_scenes_title_norm_idx
|
||||
Create Date: 2026-09-16
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision = "0041_scene_ext_refs_scene_source"
|
||||
down_revision: str | None = "0040_drop_scenes_title_norm_idx"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
with op.get_context().autocommit_block():
|
||||
op.execute(
|
||||
"CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_scene_external_refs_scene_source "
|
||||
"ON scene_external_refs (scene_id, source_id)"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
with op.get_context().autocommit_block():
|
||||
op.execute("DROP INDEX CONCURRENTLY IF EXISTS ix_scene_external_refs_scene_source")
|
||||
|
|
@ -322,7 +322,24 @@ def _studio_sorted_rows(
|
|||
|
||||
def stub_exclusion_clause():
|
||||
"""Odsiew stub-scen: tube-only bez release_date AND bez canonical (TPDB/StashDB) AND
|
||||
bez performera. NOT stub gdy ma release_date OR canonical OR performera."""
|
||||
bez performera. NOT stub gdy ma release_date OR canonical OR performera.
|
||||
|
||||
KOLEJNOSC CZLONOW OR MA ZNACZENIE i jest tu celowa, od najtanszego do najdrozszego.
|
||||
Postgres liczy OR po kolei i przerywa na pierwszym trafieniu, a sam czlonow nie
|
||||
przestawia. `release_date IS NOT NULL` idzie pierwszy, bo czyta tylko wiersz sceny.
|
||||
Kanon jest ostatni, bo to jedyny czlon z JOIN-em (`sources`).
|
||||
|
||||
Pomiar na produkcji 2026-09-16, sort=title + origin=hqporner, strona 5 (najgorszy
|
||||
przypadek, 33 590 kandydatow): czlon "ma kanon" liczony 5146 razy zamiast 16 191, bo
|
||||
"ma performera" odsiewa go wczesniej. Z indeksem z migracji 0041 spadly przy tym
|
||||
odczyty z heapu `scene_external_refs`, 16 191 -> 1241.
|
||||
|
||||
Zysk jest skromny i widac go tylko tam, gdzie kandydatow sa dziesiatki tysiecy: cale
|
||||
zapytanie czyta 251 tys. blokow zamiast 294 tys. (-15%), a czas na rozgrzanym cache to
|
||||
305 ms zamiast 371 ms (mediana z 3 rund z rotowana kolejnoscia). Na glownym feedzie
|
||||
czlon "ma kanon" wykonuje sie kilka razy na strone, wiec tam zmiana nie robi nic.
|
||||
"""
|
||||
has_performer = exists(select(1).where(ScenePerformer.scene_id == Scene.id))
|
||||
canonical_exists = exists(
|
||||
select(1)
|
||||
.select_from(SceneExternalRef)
|
||||
|
|
@ -330,8 +347,7 @@ def stub_exclusion_clause():
|
|||
.where(SceneExternalRef.scene_id == Scene.id)
|
||||
.where(Source.kind.in_([SourceKind.tpdb, SourceKind.stashdb]))
|
||||
)
|
||||
has_performer = exists(select(1).where(ScenePerformer.scene_id == Scene.id))
|
||||
return Scene.release_date.is_not(None) | canonical_exists | has_performer
|
||||
return Scene.release_date.is_not(None) | has_performer | canonical_exists
|
||||
|
||||
|
||||
@router.get("", response_model=SceneListOut)
|
||||
|
|
@ -488,9 +504,18 @@ def list_scenes(
|
|||
)
|
||||
|
||||
if has_playback is True:
|
||||
base = base.where(live_playback_exists())
|
||||
# Przy filtrze originu ta klauzula jest nadmiarowa: `origin` nizej sam wymaga
|
||||
# zrodla z `dead_at IS NULL`, wiec scena, ktora go przechodzi, ma z definicji zywy
|
||||
# playback. Kosztowala natomiast kolejna sonde `playback_sources` na KAZDYM
|
||||
# kandydacie, a przy waskim originie kandydatow sa dziesiatki tysiecy.
|
||||
if not origin:
|
||||
base = base.where(live_playback_exists())
|
||||
elif has_playback is False:
|
||||
base = base.where(~live_playback_exists())
|
||||
# `has_playback=false` RAZEM z filtrem originu to warunek sprzeczny: origin wymaga
|
||||
# zrodla z `dead_at IS NULL`, a `has_playback=false` zabrania takiego zrodla. Wynik
|
||||
# jest pusty z definicji, ale Postgres i tak przemielal caly katalog, zeby to
|
||||
# stwierdzic: 148 s na produkcji (pomiar 2026-09-16). Zwieramy od razu.
|
||||
base = base.where(false() if origin else ~live_playback_exists())
|
||||
|
||||
if origin:
|
||||
# Pelny origin z ekranu strony ('tube:hdporngg') porownujemy wprost: rownosc idzie
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue