From e12687ca6fe36d886b503b187ba82ed0d60d21ee Mon Sep 17 00:00:00 2001 From: Kyle Pope Date: Sun, 15 Mar 2026 00:45:36 +0800 Subject: [PATCH] Add calendar_events indexes and optimize dashboard queries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migration 054: three indexes on calendar_events table: - (calendar_id, start_datetime) for range queries - (parent_event_id) for recurrence bulk operations - (calendar_id, is_starred, start_datetime) for starred widget Dashboard: replaced correlated subquery with single materialized list fetch for user_calendar_ids in both /dashboard and /upcoming handlers — eliminates 2 redundant subquery evaluations per request. Co-Authored-By: Claude Opus 4.6 --- .../054_add_calendar_event_indexes.py | 36 +++++++++++++++++++ backend/app/routers/dashboard.py | 15 +++++--- 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 backend/alembic/versions/054_add_calendar_event_indexes.py diff --git a/backend/alembic/versions/054_add_calendar_event_indexes.py b/backend/alembic/versions/054_add_calendar_event_indexes.py new file mode 100644 index 0000000..e69a8c2 --- /dev/null +++ b/backend/alembic/versions/054_add_calendar_event_indexes.py @@ -0,0 +1,36 @@ +"""Add performance indexes to calendar_events + +Revision ID: 054 +Revises: 053 +""" +from alembic import op + +revision = "054" +down_revision = "053" + + +def upgrade(): + # Covers range queries in dashboard today's events and events list + op.create_index( + "ix_calendar_events_calendar_start", + "calendar_events", + ["calendar_id", "start_datetime"], + ) + # Covers bulk DELETE on recurrence edit/regeneration and sibling lookups + op.create_index( + "ix_calendar_events_parent_event_id", + "calendar_events", + ["parent_event_id"], + ) + # Covers starred widget query (calendar_id + is_starred + start_datetime) + op.create_index( + "ix_calendar_events_calendar_starred_start", + "calendar_events", + ["calendar_id", "is_starred", "start_datetime"], + ) + + +def downgrade(): + op.drop_index("ix_calendar_events_calendar_starred_start", table_name="calendar_events") + op.drop_index("ix_calendar_events_parent_event_id", table_name="calendar_events") + op.drop_index("ix_calendar_events_calendar_start", table_name="calendar_events") diff --git a/backend/app/routers/dashboard.py b/backend/app/routers/dashboard.py index e69a7e6..b131e77 100644 --- a/backend/app/routers/dashboard.py +++ b/backend/app/routers/dashboard.py @@ -35,8 +35,12 @@ async def get_dashboard( today = client_date or date.today() upcoming_cutoff = today + timedelta(days=current_settings.upcoming_days) - # Subquery: calendar IDs belonging to this user (for event scoping) - user_calendar_ids = select(Calendar.id).where(Calendar.user_id == current_user.id) + # Fetch calendar IDs once as a plain list — PostgreSQL handles IN (1,2,3) more + # efficiently than re-evaluating a correlated subquery for each of the 3 queries below. + calendar_id_result = await db.execute( + select(Calendar.id).where(Calendar.user_id == current_user.id) + ) + user_calendar_ids = [row[0] for row in calendar_id_result.all()] # Today's events (exclude parent templates — they are hidden, children are shown) today_start = datetime.combine(today, datetime.min.time()) @@ -173,8 +177,11 @@ async def get_upcoming( overdue_floor = today - timedelta(days=30) overdue_floor_dt = datetime.combine(overdue_floor, datetime.min.time()) - # Subquery: calendar IDs belonging to this user - user_calendar_ids = select(Calendar.id).where(Calendar.user_id == current_user.id) + # Fetch calendar IDs once as a plain list (same rationale as /dashboard handler) + calendar_id_result = await db.execute( + select(Calendar.id).where(Calendar.user_id == current_user.id) + ) + user_calendar_ids = [row[0] for row in calendar_id_result.all()] # Build queries — include overdue todos (up to 30 days back) and snoozed reminders todos_query = select(Todo).where(