perf(db): partial index for the photos list query
The default photos list (GET /api/v1/photos?per_page=N&sort=taken_at&order=desc) filters NOT is_trashed AND NOT is_hidden and sorts by (taken_at DESC NULLS LAST, id DESC). EXPLAIN on the 21k-row table shows a seq-scan + top-N heapsort (~20ms standalone, multiplied under concurrent fan-out on page load). The existing single-column ix_photos_taken_at can't be used because the leading WHERE clause is two booleans. Partial index over the sort key, restricted to the visible subset. Lets the planner index-scan in reverse and stop at LIMIT N. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
40
backend/alembic/versions/0017_photos_list_index.py
Normal file
40
backend/alembic/versions/0017_photos_list_index.py
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"""Partial index for the photos list query
|
||||||
|
|
||||||
|
Revision ID: 0017_photos_list_index
|
||||||
|
Revises: 0016_nextcloud_integration
|
||||||
|
Create Date: 2026-05-10
|
||||||
|
|
||||||
|
The default photos-list query (GET /api/v1/photos?per_page=N&sort=taken_at&order=desc)
|
||||||
|
filters `NOT is_trashed AND NOT is_hidden` and sorts by
|
||||||
|
`(taken_at DESC NULLS LAST, id DESC)`. EXPLAIN ANALYZE on a 21k-row
|
||||||
|
table showed a seq-scan + top-N heapsort (~20ms standalone, worse under
|
||||||
|
concurrency) — Postgres can't use the single-column ix_photos_taken_at
|
||||||
|
when the leading WHERE clause is two booleans.
|
||||||
|
|
||||||
|
A partial index on the sort key, scoped to the visible subset, lets the
|
||||||
|
planner index-scan in reverse and stop at LIMIT N. Two booleans select
|
||||||
|
~95% of rows, so the partial predicate is tighter than the full table
|
||||||
|
without losing common queries.
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "0017_photos_list_index"
|
||||||
|
down_revision: Union[str, None] = "0016_nextcloud_integration"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.execute(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_photos_list_visible
|
||||||
|
ON photos (taken_at DESC NULLS LAST, id DESC)
|
||||||
|
WHERE NOT is_trashed AND NOT is_hidden
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.execute("DROP INDEX IF EXISTS ix_photos_list_visible")
|
||||||
Reference in New Issue
Block a user