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():
|
def stub_exclusion_clause():
|
||||||
"""Odsiew stub-scen: tube-only bez release_date AND bez canonical (TPDB/StashDB) AND
|
"""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(
|
canonical_exists = exists(
|
||||||
select(1)
|
select(1)
|
||||||
.select_from(SceneExternalRef)
|
.select_from(SceneExternalRef)
|
||||||
|
|
@ -330,8 +347,7 @@ def stub_exclusion_clause():
|
||||||
.where(SceneExternalRef.scene_id == Scene.id)
|
.where(SceneExternalRef.scene_id == Scene.id)
|
||||||
.where(Source.kind.in_([SourceKind.tpdb, SourceKind.stashdb]))
|
.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) | has_performer | canonical_exists
|
||||||
return Scene.release_date.is_not(None) | canonical_exists | has_performer
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=SceneListOut)
|
@router.get("", response_model=SceneListOut)
|
||||||
|
|
@ -488,9 +504,18 @@ def list_scenes(
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_playback is True:
|
if has_playback is True:
|
||||||
|
# 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())
|
base = base.where(live_playback_exists())
|
||||||
elif has_playback is False:
|
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:
|
if origin:
|
||||||
# Pelny origin z ekranu strony ('tube:hdporngg') porownujemy wprost: rownosc idzie
|
# Pelny origin z ekranu strony ('tube:hdporngg') porownujemy wprost: rownosc idzie
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue