# 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`: ```python 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: ```bash 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: 1. Query the database for sessions that overlap that day: `WHERE start_time < day_end AND (end_time IS NULL OR end_time > day_start)` 2. 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: ```bash 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: ```sql SELECT DATE(start_time), SUM(), 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: ```sql 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: ```bash pytest ``` All tests must pass before declaring this plan complete. Do not move on to Plan 4 until the suite is green.