Compare commits
No commits in common. "2cdc3d69da123afa29d3711bb17fc105222add54" and "cf9741b3192f571cce40bd72b6bce36865bbbc44" have entirely different histories.
2cdc3d69da
...
cf9741b319
4 changed files with 19 additions and 320 deletions
|
|
@ -1,49 +0,0 @@
|
||||||
"""Indeks pod cale ORDER BY listy scen sortowanej po tytule.
|
|
||||||
|
|
||||||
`sort=title` konczy sie na `title_normalized ASC, created_at DESC, id DESC` (PK jako
|
|
||||||
ostateczny tie-break, zeby strony sie nie nakladaly). Stary `ix_scenes_title_normalized`
|
|
||||||
pokrywal tylko pierwszy czlon, wiec Postgres dokladal Incremental Sort, a ten przed
|
|
||||||
oddaniem PIERWSZEGO wiersza musi zebrac caly kubelek scen o tym samym tytule. Na stronie 1
|
|
||||||
glownego feedu ciagnal przez to 1084 wiersze zamiast 51 (709 ms zimno), a z filtrem taga
|
|
||||||
„blowjob" nie zmiescil sie w 90 s.
|
|
||||||
|
|
||||||
Pomiar na produkcji 2026-09-16, ten sam zestaw filtrow, po rozgrzewce:
|
|
||||||
strona 1 709 ms -> 12 ms (skan czyta 81 wpisow indeksu zamiast 3064)
|
|
||||||
strona 5 27 ms -> 9 ms
|
|
||||||
+ tag >90 s -> 1,1 s
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Revision ID: 0039_scenes_title_created_id_idx
|
|
||||||
Revises: 0038_scenes_studio_effdate_idx
|
|
||||||
Create Date: 2026-09-16
|
|
||||||
"""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from alembic import op
|
|
||||||
|
|
||||||
revision = "0039_scenes_title_created_id_idx"
|
|
||||||
down_revision: str | None = "0038_scenes_studio_effdate_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_scenes_title_created_id_desc "
|
|
||||||
"ON scenes (title_normalized, created_at DESC, id DESC)"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
|
||||||
with op.get_context().autocommit_block():
|
|
||||||
op.execute("DROP INDEX CONCURRENTLY IF EXISTS ix_scenes_title_created_id_desc")
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
"""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)"
|
|
||||||
)
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
"""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")
|
|
||||||
|
|
@ -9,20 +9,9 @@ from typing import Annotated
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
from sqlalchemy import (
|
from sqlalchemy import Date, and_, distinct, exists, false, func, literal_column, select, union
|
||||||
Date,
|
|
||||||
and_,
|
|
||||||
distinct,
|
|
||||||
exists,
|
|
||||||
false,
|
|
||||||
func,
|
|
||||||
literal_column,
|
|
||||||
select,
|
|
||||||
true,
|
|
||||||
union,
|
|
||||||
)
|
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
from sqlalchemy.orm import Session, aliased
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app.auth import require_api_key
|
from app.auth import require_api_key
|
||||||
|
|
||||||
|
|
@ -221,125 +210,9 @@ def blacklist_clauses(session: Session, device_id: str) -> list:
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def _studio_sorted_select(base, inner_limit: int, studio_ids=None):
|
|
||||||
"""`sort=studio` bez globalnego sortu calego przefiltrowanego katalogu.
|
|
||||||
|
|
||||||
Klucz sortu (`studios.name_normalized`) siedzi w DOLACZONEJ tabeli, wiec zaden indeks
|
|
||||||
scen go nie zna. Planer robil wiec jedyne, co mogl: materializowal caly przefiltrowany
|
|
||||||
katalog (ponad 2 mln wierszy) i sortowal go w calosci. Pomiar na produkcji 2026-09-16:
|
|
||||||
strona 1 nie zdazyla sie policzyc w 5 minut, EXPLAIN pokazywal `Sort` nad `Hash Anti
|
|
||||||
Join` bez szansy na urwanie po LIMIT.
|
|
||||||
|
|
||||||
Odwracamy zapytanie: idziemy po studiach w kolejnosci nazwy (`ix_studios_name_normalized`)
|
|
||||||
i dla kazdego dobieramy jego sceny z `ix_scenes_studio_effective_date_desc` (migracja
|
|
||||||
0038). Zewnetrzny skan jest posortowany, wiec LIMIT urywa go po kilkunastu studiach:
|
|
||||||
85 ms zamiast minut, i tak samo z waskim filtrem (tag „blowjob": 132 ms zamiast ponad
|
|
||||||
minuty).
|
|
||||||
|
|
||||||
`inner_limit` to offset+limit strony. Z JEDNEGO studia nigdy nie trzeba wiecej wierszy,
|
|
||||||
niz liczy cala strona razem z offsetem, wiec ten LIMIT nie zmienia wyniku, a trzyma
|
|
||||||
wewnetrzny skan przy ziemi dla gigantow w rodzaju Brazzers.
|
|
||||||
|
|
||||||
`studio_ids` zawezaja SAM zewnetrzny skan, gdy user filtruje po studiach. Bez tego
|
|
||||||
przy `?studio_slugs=vixen` skan szedl przez wszystkie 33 tys. studiow, dla kazdego
|
|
||||||
pytajac o sceny, ktorych tam z definicji nie ma: 100 s na strone 1 (pomiar 2026-09-16).
|
|
||||||
Warunek jest juz w `base` po stronie scen, tu tylko powtorzony po stronie studiow, wiec
|
|
||||||
wynik sie nie zmienia.
|
|
||||||
|
|
||||||
Sceny BEZ studia tu nie wchodza, ma je dobrac `_studio_sorted_rows`.
|
|
||||||
"""
|
|
||||||
st = aliased(Studio, name="st")
|
|
||||||
inner = (
|
|
||||||
base.where(Scene.studio_id == st.id)
|
|
||||||
.order_by(_effective_date_desc(), Scene.created_at.desc(), Scene.id.desc())
|
|
||||||
.limit(literal_column(str(inner_limit)))
|
|
||||||
.subquery()
|
|
||||||
.lateral("sc")
|
|
||||||
)
|
|
||||||
scene = aliased(Scene, inner)
|
|
||||||
outer = select(scene).select_from(st).join(inner, true())
|
|
||||||
if studio_ids is not None:
|
|
||||||
outer = outer.where(st.id.in_(studio_ids))
|
|
||||||
# Dokladnie stary ORDER BY, tylko nazwa studia bierze sie teraz z zewnetrznego skanu.
|
|
||||||
# NULLS LAST jest i tak domyslne dla ASC, wiec `ix_studios_name_normalized` wystarcza.
|
|
||||||
return outer.order_by(
|
|
||||||
st.name_normalized.asc().nullslast(),
|
|
||||||
func.coalesce(
|
|
||||||
inner.c.release_date,
|
|
||||||
func.cast(func.timezone("UTC", inner.c.created_at), Date),
|
|
||||||
).desc(),
|
|
||||||
inner.c.created_at.desc(),
|
|
||||||
inner.c.id.desc(),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _studio_sorted_rows(
|
|
||||||
session: Session, base, off: int, want: int, studio_ids=None
|
|
||||||
) -> list[Scene]:
|
|
||||||
"""Strona `sort=studio`: sceny studiow po nazwie, a za nimi sceny bez studia."""
|
|
||||||
rows = list(
|
|
||||||
session.execute(
|
|
||||||
_studio_sorted_select(base, off + want, studio_ids)
|
|
||||||
.offset(literal_column(str(off)))
|
|
||||||
.limit(literal_column(str(want)))
|
|
||||||
)
|
|
||||||
.scalars()
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
if len(rows) >= want or studio_ids is not None:
|
|
||||||
# Filtr po studiach: scena bez studia i tak by go nie przeszla, wiec ogon odpada.
|
|
||||||
return rows
|
|
||||||
|
|
||||||
# Lista studiow sie skonczyla, wiec dobieramy ogon: sceny bez studia. W starym ORDER BY
|
|
||||||
# siedzialy na koncu przez NULLS LAST i tak zostaje. W glownym feedzie zaczynaja sie
|
|
||||||
# dopiero po ponad 400 tys. wierszy, czyli praktycznie nikt tam nie dojdzie, ale przy
|
|
||||||
# waskim filtrze studia koncza sie szybko i bez tego ogona sort=studio chowalby wszystko,
|
|
||||||
# co studia nie ma (to 62% katalogu).
|
|
||||||
if off == 0 or rows:
|
|
||||||
with_studio = off + len(rows)
|
|
||||||
else:
|
|
||||||
# Offset przeskoczyl cala czesc studiowa, wiec nie wiemy, ile jej bylo. Liczymy,
|
|
||||||
# ale nie wiecej niz do konca zadanej strony.
|
|
||||||
with_studio = session.execute(
|
|
||||||
select(func.count()).select_from(
|
|
||||||
_studio_sorted_select(base, off + want, studio_ids)
|
|
||||||
.limit(literal_column(str(off + want)))
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
).scalar_one()
|
|
||||||
rows.extend(
|
|
||||||
session.execute(
|
|
||||||
base.where(Scene.studio_id.is_(None))
|
|
||||||
.order_by(_effective_date_desc(), Scene.created_at.desc(), Scene.id.desc())
|
|
||||||
.offset(literal_column(str(max(0, off - with_studio))))
|
|
||||||
.limit(literal_column(str(want - len(rows))))
|
|
||||||
)
|
|
||||||
.scalars()
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
return 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)
|
||||||
|
|
@ -347,7 +220,8 @@ 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]))
|
||||||
)
|
)
|
||||||
return Scene.release_date.is_not(None) | has_performer | canonical_exists
|
has_performer = exists(select(1).where(ScenePerformer.scene_id == Scene.id))
|
||||||
|
return Scene.release_date.is_not(None) | canonical_exists | has_performer
|
||||||
|
|
||||||
|
|
||||||
@router.get("", response_model=SceneListOut)
|
@router.get("", response_model=SceneListOut)
|
||||||
|
|
@ -504,18 +378,9 @@ def list_scenes(
|
||||||
)
|
)
|
||||||
|
|
||||||
if has_playback is True:
|
if has_playback is True:
|
||||||
# Przy filtrze originu ta klauzula jest nadmiarowa: `origin` nizej sam wymaga
|
base = base.where(live_playback_exists())
|
||||||
# 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:
|
elif has_playback is False:
|
||||||
# `has_playback=false` RAZEM z filtrem originu to warunek sprzeczny: origin wymaga
|
base = base.where(~live_playback_exists())
|
||||||
# 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
|
||||||
|
|
@ -606,12 +471,6 @@ def list_scenes(
|
||||||
total_capped = False
|
total_capped = False
|
||||||
total: int | None = _default_scene_count(session) if _is_pure_default else None
|
total: int | None = _default_scene_count(session) if _is_pure_default else None
|
||||||
|
|
||||||
# Offset i rozmiar pobrania sa potrzebne juz przy budowie sortu: `sort=studio` musi
|
|
||||||
# wiedziec, ile wierszy moze potrzebowac z jednego studia (patrz _studio_sorted_select).
|
|
||||||
_off = (page - 1) * per_page
|
|
||||||
_want = per_page + 1
|
|
||||||
studio_rows: list[Scene] | None = None
|
|
||||||
|
|
||||||
# Sort: KAŻDY tryb kończy się `Scene.id.desc()`, unikalny PK jako ostateczny
|
# Sort: KAŻDY tryb kończy się `Scene.id.desc()`, unikalny PK jako ostateczny
|
||||||
# tie-break. Bez tego przy OFFSET-paginacji sceny z IDENTYCZNYM created_at (masowy
|
# tie-break. Bez tego przy OFFSET-paginacji sceny z IDENTYCZNYM created_at (masowy
|
||||||
# ingest = ten sam timestamp co do sekundy) i release_date mają NIEustalony porządek,
|
# ingest = ten sam timestamp co do sekundy) i release_date mają NIEustalony porządek,
|
||||||
|
|
@ -632,31 +491,19 @@ def list_scenes(
|
||||||
_effective_date_desc(), Scene.created_at.desc(), Scene.id.desc()
|
_effective_date_desc(), Scene.created_at.desc(), Scene.id.desc()
|
||||||
)
|
)
|
||||||
elif sort == "title":
|
elif sort == "title":
|
||||||
# Cale ORDER BY (razem z tie-breakami) pokrywa ix_scenes_title_created_id_desc
|
|
||||||
# (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(
|
ordered = base.order_by(
|
||||||
Scene.title_normalized.asc(), Scene.created_at.desc(), Scene.id.desc()
|
Scene.title_normalized.asc(), Scene.created_at.desc(), Scene.id.desc()
|
||||||
)
|
)
|
||||||
elif sort == "studio":
|
elif sort == "studio":
|
||||||
# Sceny bez studio na końcu; w obrębie studio, najświeższe pierwsze. Fetch idzie
|
# Sceny bez studio na końcu; w obrębie studio, najświeższe pierwsze.
|
||||||
# osobna sciezka (LATERAL po studiach), bo jeden SELECT z LEFT JOIN nie da sie tu
|
ordered = (
|
||||||
# urwac po LIMIT: uzasadnienie i pomiary w _studio_sorted_select.
|
base.outerjoin(Studio, Studio.id == Scene.studio_id)
|
||||||
ordered = None
|
.order_by(
|
||||||
studio_rows = _studio_sorted_rows(
|
Studio.name_normalized.asc().nullslast(),
|
||||||
session,
|
_effective_date_desc(),
|
||||||
base,
|
Scene.created_at.desc(),
|
||||||
_off,
|
Scene.id.desc(),
|
||||||
_want,
|
)
|
||||||
studio_ids=(
|
|
||||||
select(Studio.id).where(Studio.slug.in_(studio_slug_list))
|
|
||||||
if studio_slug_list
|
|
||||||
else None
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
elif sort in ("duration_desc", "duration_asc"):
|
elif sort in ("duration_desc", "duration_asc"):
|
||||||
# Najdluzsze / najkrotsze (zgloszenie ae6a045b). Sceny bez dlugosci odpadaja,
|
# Najdluzsze / najkrotsze (zgloszenie ae6a045b). Sceny bez dlugosci odpadaja,
|
||||||
|
|
@ -703,11 +550,10 @@ def list_scenes(
|
||||||
# psuje early-termination i przy filtrach EXISTS planer robi gather-all+sort (sekundy)
|
# psuje early-termination i przy filtrach EXISTS planer robi gather-all+sort (sekundy)
|
||||||
# zamiast limit-aware index-walk po `ix_scenes_created_at_desc`. page/per_page to
|
# zamiast limit-aware index-walk po `ix_scenes_created_at_desc`. page/per_page to
|
||||||
# walidowane inty (Query ge=1, le=200), więc literal_column jest bezpieczne.
|
# walidowane inty (Query ge=1, le=200), więc literal_column jest bezpieczne.
|
||||||
|
_off = (page - 1) * per_page
|
||||||
rows = (
|
rows = (
|
||||||
studio_rows
|
session.execute(
|
||||||
if studio_rows is not None
|
ordered.offset(literal_column(str(_off))).limit(literal_column(str(per_page + 1)))
|
||||||
else session.execute(
|
|
||||||
ordered.offset(literal_column(str(_off))).limit(literal_column(str(_want)))
|
|
||||||
)
|
)
|
||||||
.scalars()
|
.scalars()
|
||||||
.all()
|
.all()
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue