4.2 KiB
Plan 3 — Performance
Items covered: #4 (missing database indexes), #2 (O(n³) timetable loop), #3 (in-Python aggregation in other stats endpoints)
Context
You are working on a FastAPI backend for a baby monitor / sleep tracker app. The codebase lives under app/. This plan replaces expensive Python loops in app/routers/stats.py with SQL queries, and adds database indexes to make those queries fast. The index fix (#4) must be done first because the SQL queries depend on it being effective.
Do one task at a time. After each file change, re-read the file to verify it is correct. Run the tests between each task.
Tasks
1. Add database indexes (app/models.py)
Step 1 — Read the file first
Read app/models.py in full to understand the SleepSession model structure and current field definitions.
Step 2 — Add indexes
Add indexes on start_time and end_time. The preferred approach is inline with sa_column:
from sqlalchemy import Column, DateTime
from sqlmodel import Field, SQLModel
class SleepSession(SQLModel, table=True):
...
start_time: datetime = Field(sa_column=Column(DateTime, index=True))
end_time: Optional[datetime] = Field(default=None, sa_column=Column(DateTime, index=True, nullable=True))
If the fields already use sa_column, just add index=True to the existing Column(...) calls. Do not duplicate field definitions.
Step 3 — Verify
Run the tests to make sure the model change doesn't break anything:
pytest
2. Fix the O(n³) timetable query (app/routers/stats.py:77-99)
Step 1 — Read the file first
Read app/routers/stats.py in full before making any changes. Understand the full timetable endpoint: its parameters, what it returns, and the structure of the current triple-nested loop.
Step 2 — Rewrite the loop
The current code iterates over every day, then every 15-minute slot, then every session in memory — ~2.88 million comparisons for 30 days × 1,000 sessions.
Replace it with a per-day SQL query approach:
For each day in the date range:
- Query the database for sessions that overlap that day:
WHERE start_time < day_end AND (end_time IS NULL OR end_time > day_start) - Build the 96 fifteen-minute slots comparing only against the small set of sessions returned for that day.
This keeps the slot-building logic in Python (which is fine) but moves the session filtering into SQL (which uses the index). The number of sessions compared per day drops from ALL sessions to only the ones active that day.
Keep the return format identical to the current implementation — do not change the shape of the response.
Step 3 — Verify
Run the tests:
pytest
All timetable tests must pass. If there is only one happy-path test, that is expected — new tests are added in Plan 5.
3. Fix in-Python aggregation in the other two stats endpoints (app/routers/stats.py:28-47 and 119-134)
Read the file again (it may have changed from the previous step) before editing.
daily-summary endpoint (around lines 28-47)
The current code loads all sessions for a date range and loops through them in Python to compute per-day totals. Replace with a single SQL GROUP BY query:
SELECT DATE(start_time), SUM(<duration_expression>), COUNT(*)
FROM sleep_session
WHERE start_time >= :from_date AND start_time < :to_date
GROUP BY DATE(start_time)
Use SQLAlchemy's query API or a text() query to express this. Map the results back to the same response format the endpoint currently returns.
wake-sleep-times endpoint (around lines 119-134)
The current code loads all sessions and loops per day to find the first wakeup and last bedtime. Replace with:
SELECT DATE(start_time), MIN(start_time), MAX(end_time)
FROM sleep_session
WHERE start_time >= :from_date AND start_time < :to_date
GROUP BY DATE(start_time)
Map the results back to the same response format.
Important
Do not change the response shape of either endpoint. The goal is identical output, produced more efficiently.
Step — Verify
Run the full test suite:
pytest
All tests must pass before declaring this plan complete. Do not move on to Plan 4 until the suite is green.