# Plan 011: Add Database Indexes **Priority**: MEDIUM **Effort**: 1 hour **Status**: NOT STARTED **Risk Level**: LOW - Performance optimization --- ## Problem Statement The database schema is missing composite indexes for common query patterns: - `(game_id, play_number)` on plays table - `(game_id, team_id)` on lineups table - `(game_id, is_active)` on lineups table This causes sequential scans on tables that could use index lookups. ## Impact - **Performance**: Slower queries for game recovery - **Scalability**: Performance degrades with more data - **Cost**: Higher database CPU usage ## Current Query Patterns ### 1. Get Plays for Game (Recovery) ```python # operations.py:467-468 select(Play).where(Play.game_id == game_id).order_by(Play.play_number) ``` **Current**: Index on `game_id`, sequential scan for ordering **Needed**: Composite index `(game_id, play_number)` ### 2. Get Lineups for Team ```python # operations.py:488-493 select(Lineup).where( Lineup.game_id == game_id, Lineup.team_id == team_id, Lineup.is_active == True ) ``` **Current**: Index on `game_id`, filter on `team_id` and `is_active` **Needed**: Composite index `(game_id, team_id, is_active)` ### 3. Get Active Players ```python select(Lineup).where( Lineup.game_id == game_id, Lineup.is_active == True ) ``` **Current**: Index on `game_id`, filter on `is_active` **Needed**: Composite index `(game_id, is_active)` ## Implementation Steps ### Step 1: Create Migration (15 min) ```bash cd /mnt/NV2/Development/strat-gameplay-webapp/backend alembic revision -m "Add composite indexes for common queries" ``` Create migration file: ```python """Add composite indexes for common queries Revision ID: 005 Revises: 004 Create Date: 2025-01-27 """ from typing import Sequence, Union from alembic import op revision: str = '005' down_revision: str = '004' branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: # Plays table - optimize game recovery and play history op.create_index( 'idx_play_game_number', 'plays', ['game_id', 'play_number'], unique=False ) # Lineups table - optimize team lineup queries op.create_index( 'idx_lineup_game_team_active', 'lineups', ['game_id', 'team_id', 'is_active'], unique=False ) # Lineups table - optimize active player queries op.create_index( 'idx_lineup_game_active', 'lineups', ['game_id', 'is_active'], unique=False ) # Rolls table - optimize roll history queries op.create_index( 'idx_roll_game_type', 'rolls', ['game_id', 'roll_type'], unique=False ) # Games table - optimize status queries op.create_index( 'idx_game_status_created', 'games', ['status', 'created_at'], unique=False ) def downgrade() -> None: op.drop_index('idx_play_game_number', table_name='plays') op.drop_index('idx_lineup_game_team_active', table_name='lineups') op.drop_index('idx_lineup_game_active', table_name='lineups') op.drop_index('idx_roll_game_type', table_name='rolls') op.drop_index('idx_game_status_created', table_name='games') ``` ### Step 2: Update Models (Optional - for documentation) (15 min) Update `backend/app/models/db_models.py` to document indexes: ```python class Play(Base): __tablename__ = "plays" # ... columns ... __table_args__ = ( Index('idx_play_game_number', 'game_id', 'play_number'), # ... other constraints ... ) class Lineup(Base): __tablename__ = "lineups" # ... columns ... __table_args__ = ( Index('idx_lineup_game_team_active', 'game_id', 'team_id', 'is_active'), Index('idx_lineup_game_active', 'game_id', 'is_active'), # ... other constraints ... ) ``` ### Step 3: Apply Migration (5 min) ```bash cd /mnt/NV2/Development/strat-gameplay-webapp/backend # Apply migration alembic upgrade head # Verify indexes created psql -d strat_gameplay -c "\di" ``` ### Step 4: Verify Query Plans (15 min) Test that queries use the new indexes: ```sql -- Check play query uses index EXPLAIN ANALYZE SELECT * FROM plays WHERE game_id = 'some-uuid' ORDER BY play_number; -- Should show: Index Scan using idx_play_game_number -- Check lineup query uses index EXPLAIN ANALYZE SELECT * FROM lineups WHERE game_id = 'some-uuid' AND team_id = 1 AND is_active = true; -- Should show: Index Scan using idx_lineup_game_team_active ``` ### Step 5: Add Performance Test (15 min) Create `backend/tests/integration/test_query_performance.py`: ```python """Performance tests for database queries.""" import pytest import time from uuid import uuid4 class TestQueryPerformance: """Tests that queries use indexes efficiently.""" @pytest.mark.integration @pytest.mark.asyncio async def test_get_plays_uses_index(self, db_ops, sample_game): """Play retrieval should use composite index.""" # Create 100 plays for i in range(100): await db_ops.save_play({ "game_id": sample_game, "play_number": i + 1, # ... other fields }) # Time the query start = time.perf_counter() plays = await db_ops.get_plays(sample_game) duration = time.perf_counter() - start assert len(plays) == 100 assert duration < 0.1 # Should be < 100ms with index @pytest.mark.integration @pytest.mark.asyncio async def test_get_lineups_uses_index(self, db_ops, sample_game, sample_lineups): """Lineup retrieval should use composite index.""" start = time.perf_counter() lineups = await db_ops.get_lineups(sample_game, team_id=1) duration = time.perf_counter() - start assert len(lineups) == 9 assert duration < 0.05 # Should be < 50ms with index ``` ## Index Summary | Index Name | Table | Columns | Purpose | |------------|-------|---------|---------| | `idx_play_game_number` | plays | (game_id, play_number) | Game recovery, play history | | `idx_lineup_game_team_active` | lineups | (game_id, team_id, is_active) | Team lineup queries | | `idx_lineup_game_active` | lineups | (game_id, is_active) | Active player queries | | `idx_roll_game_type` | rolls | (game_id, roll_type) | Roll history queries | | `idx_game_status_created` | games | (status, created_at) | Game listing queries | ## Expected Performance Improvement | Query | Before | After | Improvement | |-------|--------|-------|-------------| | Get 100 plays | ~50ms | ~5ms | 10x | | Get team lineup | ~20ms | ~2ms | 10x | | List active games | ~100ms | ~10ms | 10x | ## Verification Checklist - [ ] Migration created and reviewed - [ ] Migration applied successfully - [ ] EXPLAIN shows index usage - [ ] Performance tests pass - [ ] No regression in other queries ## Rollback Plan ```bash # Revert migration alembic downgrade -1 ``` Indexes are non-destructive - removing them just reverts to slower queries. ## Dependencies - Plan 004 (Alembic Migrations) - need migration infrastructure ## Notes - Monitor index usage with `pg_stat_user_indexes` - Consider partial indexes for very large tables - Future: Add covering indexes if SELECT includes specific columns