From a548cc0b8ecaa1ccee264aefb083ffb3649b26f3 Mon Sep 17 00:00:00 2001 From: NguyenND Date: Mon, 8 Jun 2026 14:45:04 -0400 Subject: [PATCH] 06/08 Optimize queries with indexes --- CLAUDE.md | 13 +++- app/routes/inspections.py | 8 ++- app/routes/issues.py | 6 ++ app/routes/reports.py | 6 ++ .../versions/phase21_performance_indexes.py | 67 +++++++++++++++++++ 5 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 migrations/versions/phase21_performance_indexes.py diff --git a/CLAUDE.md b/CLAUDE.md index cf6df97..9959f3b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -559,7 +559,18 @@ phase1_projects_roles → phase6_features → phase7_mobile_api → phase8_notif → phase17_notification_event_type → phase18_issue_reported_by → phase19_issue_mobile_photos - → phase20_inspector_assignments ← HEAD + → phase20_inspector_assignments + → phase21_performance_indexes ← HEAD +``` + +### phase21_performance_indexes + +Adds four composite indexes covering the highest-traffic multi-column query patterns: `(facility_id, inspection_date)` and `(inspector_id, inspection_date)` and `(status, inspection_date)` on `inspections`; `(facility_id, status)` on `issues`. All single-column indexes already exist from phase12. Uses `INFORMATION_SCHEMA.STATISTICS` existence check — safe to re-run. + +**Deploy order for phase21:** +```bash +flask db upgrade +sudo systemctl restart gunicorn ``` ### phase19_issue_mobile_photos diff --git a/app/routes/inspections.py b/app/routes/inspections.py index 4a950fd..c14eb3b 100644 --- a/app/routes/inspections.py +++ b/app/routes/inspections.py @@ -24,6 +24,7 @@ from app.models.notification import ( ) from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE, ACTION_EXPORT from app.utils.scope import get_customer_scope, get_inspector_scope +from sqlalchemy.orm import joinedload bp = Blueprint('inspections', __name__, url_prefix='/inspections') @@ -198,7 +199,12 @@ def _validate_required(form_fields, responses): @login_required def index(): page = request.args.get('page', 1, type=int) - q = Inspection.query.order_by(Inspection.inspection_date.desc()) + q = Inspection.query.options( + joinedload(Inspection.facility), + joinedload(Inspection.template), + joinedload(Inspection.inspector), + joinedload(Inspection.area), + ).order_by(Inspection.inspection_date.desc()) if current_user.role == 'inspector': fids = get_inspector_scope(current_user) diff --git a/app/routes/issues.py b/app/routes/issues.py index 630a5a0..9ca7303 100644 --- a/app/routes/issues.py +++ b/app/routes/issues.py @@ -19,6 +19,7 @@ from app.utils.notifications import notify, notify_customers_for_facility, notif from app.utils.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE from app.utils.scope import get_customer_scope, get_inspector_scope from app.utils.sla import sla_status +from sqlalchemy.orm import joinedload, contains_eager bp = Blueprint('issues', __name__, url_prefix='/issues') @@ -76,6 +77,11 @@ def index(): q = ( Issue.query .outerjoin(Area, Issue.area_id == Area.id) + .options( + contains_eager(Issue.area), + joinedload(Issue.facility), + joinedload(Issue.assigned_user), + ) .order_by(Issue.reported_at.desc()) ) diff --git a/app/routes/reports.py b/app/routes/reports.py index af30b2b..ef503fe 100644 --- a/app/routes/reports.py +++ b/app/routes/reports.py @@ -7,6 +7,7 @@ from flask import (Blueprint, render_template, request, Response, stream_with_context, abort) from flask_login import login_required, current_user from sqlalchemy import func +from sqlalchemy.orm import joinedload from app import db from app.models.inspection import Inspection, InspectionTemplate from app.models.facility import Facility, Area @@ -677,6 +678,11 @@ def inspector_performance(): recent_inspections = ( Inspection.query + .options( + joinedload(Inspection.facility), + joinedload(Inspection.area), + joinedload(Inspection.template), + ) .filter( Inspection.inspector_id == selected_id, Inspection.inspection_date >= start, diff --git a/migrations/versions/phase21_performance_indexes.py b/migrations/versions/phase21_performance_indexes.py new file mode 100644 index 0000000..c6de309 --- /dev/null +++ b/migrations/versions/phase21_performance_indexes.py @@ -0,0 +1,67 @@ +"""phase21 — composite performance indexes + +Adds composite (multi-column) indexes on the highest-traffic query patterns. +Phase 12 already covers single-column indexes; these target the multi-column +WHERE clauses that appear on every Reports, Inspections list, and Issues list +page load. + + inspections (facility_id, inspection_date) + — facility-scoped date-range queries on every list page and report + + inspections (inspector_id, inspection_date) + — inspector-scoped date-range queries on the Performance page and API stats + + inspections (status, inspection_date) + — "completed inspections in date range" pattern used by all score aggregations + + issues (facility_id, status) + — "open issues at this facility" pattern used by reports and dashboard + +All existence checks use INFORMATION_SCHEMA.STATISTICS — safe to re-run on +any MySQL version (compatible back to 5.7). +""" + +import sqlalchemy as sa +from alembic import op + +revision = 'phase21_performance_indexes' +down_revision = 'phase20_inspector_assignments' +branch_labels = None +depends_on = None + + +def _index_exists(bind, table: str, index_name: str) -> bool: + result = bind.execute(sa.text( + "SELECT COUNT(*) FROM information_schema.statistics " + "WHERE table_schema = DATABASE() " + " AND table_name = :table " + " AND index_name = :index" + ), {'table': table, 'index': index_name}) + return result.scalar() > 0 + + +# (table, index_name, columns) +INDEXES = [ + ('inspections', 'ix_inspections_facility_date', 'facility_id, inspection_date'), + ('inspections', 'ix_inspections_inspector_date', 'inspector_id, inspection_date'), + ('inspections', 'ix_inspections_status_date', 'status, inspection_date'), + ('issues', 'ix_issues_facility_status', 'facility_id, status'), +] + + +def upgrade(): + bind = op.get_bind() + for table, index_name, columns in INDEXES: + if not _index_exists(bind, table, index_name): + op.execute(sa.text( + f'CREATE INDEX {index_name} ON {table} ({columns})' + )) + + +def downgrade(): + bind = op.get_bind() + for table, index_name, _columns in INDEXES: + if _index_exists(bind, table, index_name): + op.execute(sa.text( + f'DROP INDEX {index_name} ON {table}' + ))