perf(scenes): indeks pod sortowanie serwisu po dacie premiery + fix wyscigu zapisu wyszukiwania
Zgloszenie 4cccc70f: sxyprn na liscie sites ladowal sie ok 18 sekund. Odtworzone: /scenes?origin=tube:sxyprncom&sort=release_date schodzilo 19,23 s, a to samo z sort=created_at 0,08 s. Ekran serwisu sortuje po dacie premiery, czyli release_date DESC NULLS LAST, created_at DESC, id DESC, a istniejacy ix_scenes_release_date jest ROSNACY: czytany od tylu daje NULL-e na poczatku i nie obsluguje kluczy rozstrzygajacych. Planner leial wiec pelnym skanem scenes (3,9 mln) i playback_sources (3,7 mln). Po zalozeniu pasujacego indeksu strumieniuje w docelowej kolejnosci i konczy po 25 trafieniach: sxyprn 19,23 s -> 0,11 s, hqporner 7,99 s -> 0,87 s. Drugi indeks po originie, bo playback_sources nie mial zadnego. GOON-4Y: add_saved_search sprawdzal istnienie i wstawial w dwoch krokach, wiec dwa rownolegle zapisy tego samego zapytania (podwojne tapniecie, ponowienie po timeoucie) oba widzialy pusto i drugi dostawal 500 na uq_saved_searches_device_query. Teraz IntegrityError konczy sie oddaniem istniejacego wiersza. Test: 6 rownoleglych zapisow -> 6x200 i jeden wiersz. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
65f768cf8a
commit
8f6fa8c732
2 changed files with 69 additions and 1 deletions
51
alembic/versions/20260820_0032_origin_release_sort_idx.py
Normal file
51
alembic/versions/20260820_0032_origin_release_sort_idx.py
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
"""indeksy pod listowanie scen jednego serwisu sortowane po dacie premiery
|
||||
|
||||
Zgłoszenie 4cccc70f: „po kliknięciu sxyprn na liście sites sceny ładują się po ok
|
||||
18 sekundach". Odtworzone co do sekundy: `/scenes?origin=tube:sxyprncom&
|
||||
sort=release_date` schodziło 19,23 s, a to samo z `sort=created_at` 0,08 s.
|
||||
|
||||
Powód: ekran serwisu sortuje po dacie premiery, czyli
|
||||
`release_date DESC NULLS LAST, created_at DESC, id DESC`. Istniejący
|
||||
`ix_scenes_release_date` jest ROSNĄCY, więc czytany od tyłu daje NULL-e na
|
||||
początku, a nie na końcu, i nie obsługuje tego układu ani dwóch kluczy
|
||||
rozstrzygających. Bez pasującego indeksu planner leciał pełnym skanem `scenes`
|
||||
(3,9 mln) i `playback_sources` (3,7 mln), sortował całość i dopiero brał 25 wierszy.
|
||||
|
||||
Z pasującym indeksem strumieniuje sceny już w docelowej kolejności i zatrzymuje się
|
||||
po 25 trafieniach. Po założeniu na produkcji: sxyprn 19,23 s → 0,11 s,
|
||||
hqporner 7,99 s → 0,87 s.
|
||||
|
||||
Drugi indeks (`origin`, `scene_id`) obsługuje wyszukanie źródeł jednego serwisu.
|
||||
Na `playback_sources` nie było ŻADNEGO indeksu po originie.
|
||||
|
||||
`IF NOT EXISTS`, bo na produkcji oba założono od razu (CONCURRENTLY, żeby nie
|
||||
blokować ruchu). Tu chodzi o to, by świeża instalacja miała to samo.
|
||||
|
||||
Revision ID: 0032_origin_release_idx
|
||||
Revises: 0031_duration_backfill
|
||||
Create Date: 2026-08-20
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from alembic import op
|
||||
|
||||
revision: str = "0032_origin_release_idx"
|
||||
down_revision: str | None = "0031_duration_backfill"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_scenes_release_created_id_desc "
|
||||
"ON scenes (release_date DESC NULLS LAST, created_at DESC, id DESC)"
|
||||
)
|
||||
op.execute(
|
||||
"CREATE INDEX IF NOT EXISTS ix_playback_sources_origin_live "
|
||||
"ON playback_sources (origin, scene_id) WHERE dead_at IS NULL"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP INDEX IF EXISTS ix_playback_sources_origin_live")
|
||||
op.execute("DROP INDEX IF EXISTS ix_scenes_release_created_id_desc")
|
||||
|
|
@ -17,6 +17,7 @@ from typing import Annotated
|
|||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from pydantic import BaseModel, field_validator
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.device import get_device_id
|
||||
|
|
@ -89,7 +90,23 @@ def add_saved_search(
|
|||
)
|
||||
row = SavedSearch(device_id=device_id, query=payload.query)
|
||||
session.add(row)
|
||||
session.commit()
|
||||
try:
|
||||
session.commit()
|
||||
except IntegrityError:
|
||||
# Wyscig na `uq_saved_searches_device_query`: sprawdzenie wyzej i INSERT to dwie
|
||||
# osobne operacje, wiec dwa rownolegle zapisy tego samego zapytania (podwojne
|
||||
# tapniecie, ponowienie po timeoucie) oba widza pusto i oba wstawiaja. Drugi
|
||||
# dostawal 500 zamiast idempotentnej odpowiedzi (GOON-4Y). Skoro wiersz juz
|
||||
# istnieje, to dokladnie ten, ktory user chcial zapisac, wiec go oddajemy.
|
||||
session.rollback()
|
||||
existing = session.execute(
|
||||
select(SavedSearch).where(
|
||||
SavedSearch.device_id == device_id, SavedSearch.query == payload.query
|
||||
)
|
||||
).scalar_one_or_none()
|
||||
if existing is None:
|
||||
raise
|
||||
return existing
|
||||
session.refresh(row)
|
||||
return row
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue