Add calendar_events indexes and optimize dashboard queries

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 <noreply@anthropic.com>
This commit is contained in:
Kyle 2026-03-15 00:45:36 +08:00
parent 3e738b18d4
commit e12687ca6f
2 changed files with 47 additions and 4 deletions

View File

@ -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")

View File

@ -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(