From 20a8dc8e2751f84d7f3d4c7ae78e678fc89adf61 Mon Sep 17 00:00:00 2001 From: jtrzupek Date: Tue, 2 Jun 2026 11:14:38 +0200 Subject: [PATCH] perf(scenes): count over PK, not whole entity, in filtered list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bounded count for filtered scene lists ran `SELECT count(*) FROM (SELECT scenes.* ... LIMIT 1001)` because the base query selects the full Scene entity. Counting over all columns made the planner pick a far worse plan via psycopg bound params (~4s for has_playback) than the same logic over the PK (~30-400ms). Count semantics are unchanged — we only need rows to exist — so count over `base.with_only_columns(Scene.id)`. Partial: this fixes the count leg. The main ordered fetch on filtered lists (has_playback / tags) can still pick a gather-all-then-sort plan under bound params (fast with literal binds, slow parameterized) — tracked separately. Co-Authored-By: Claude Opus 4.8 (1M context) --- app/api/scenes.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/api/scenes.py b/app/api/scenes.py index 8d30810..3a6f809 100644 --- a/app/api/scenes.py +++ b/app/api/scenes.py @@ -328,8 +328,13 @@ def list_scenes( if _is_pure_default: total = _default_scene_count(session) else: + # PERF (2026-06-02): `base` to `select(Scene)` (wszystkie kolumny). Count nad + # `SELECT scenes.* ... LIMIT 1001` z bound-params psycopg dobierał zły plan i + # zajmował ~4s (has_playback) / ~6s (tags) — mimo że to samo z `SELECT id` / + # literałami robi ~30ms. Liczymy nad samym PK: identyczna semantyka, ~100× szybciej. + count_base = base.with_only_columns(Scene.id) cnt = session.execute( - select(func.count()).select_from(base.limit(_COUNT_CAP + 1).subquery()) + select(func.count()).select_from(count_base.limit(_COUNT_CAP + 1).subquery()) ).scalar_one() if cnt > _COUNT_CAP: total, total_capped = _COUNT_CAP, True