06/08 Optimize queries with indexes

This commit is contained in:
2026-06-08 14:45:04 -04:00
parent 9eec6ff94a
commit a548cc0b8e
5 changed files with 98 additions and 2 deletions
+12 -1
View File
@@ -559,7 +559,18 @@ phase1_projects_roles → phase6_features → phase7_mobile_api → phase8_notif
→ phase17_notification_event_type → phase17_notification_event_type
→ phase18_issue_reported_by → phase18_issue_reported_by
→ phase19_issue_mobile_photos → 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 ### phase19_issue_mobile_photos
+7 -1
View File
@@ -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.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE, ACTION_EXPORT
from app.utils.scope import get_customer_scope, get_inspector_scope from app.utils.scope import get_customer_scope, get_inspector_scope
from sqlalchemy.orm import joinedload
bp = Blueprint('inspections', __name__, url_prefix='/inspections') bp = Blueprint('inspections', __name__, url_prefix='/inspections')
@@ -198,7 +199,12 @@ def _validate_required(form_fields, responses):
@login_required @login_required
def index(): def index():
page = request.args.get('page', 1, type=int) 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': if current_user.role == 'inspector':
fids = get_inspector_scope(current_user) fids = get_inspector_scope(current_user)
+6
View File
@@ -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.audit import log_action, ACTION_CREATE, ACTION_UPDATE, ACTION_DELETE
from app.utils.scope import get_customer_scope, get_inspector_scope from app.utils.scope import get_customer_scope, get_inspector_scope
from app.utils.sla import sla_status from app.utils.sla import sla_status
from sqlalchemy.orm import joinedload, contains_eager
bp = Blueprint('issues', __name__, url_prefix='/issues') bp = Blueprint('issues', __name__, url_prefix='/issues')
@@ -76,6 +77,11 @@ def index():
q = ( q = (
Issue.query Issue.query
.outerjoin(Area, Issue.area_id == Area.id) .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()) .order_by(Issue.reported_at.desc())
) )
+6
View File
@@ -7,6 +7,7 @@ from flask import (Blueprint, render_template, request,
Response, stream_with_context, abort) Response, stream_with_context, abort)
from flask_login import login_required, current_user from flask_login import login_required, current_user
from sqlalchemy import func from sqlalchemy import func
from sqlalchemy.orm import joinedload
from app import db from app import db
from app.models.inspection import Inspection, InspectionTemplate from app.models.inspection import Inspection, InspectionTemplate
from app.models.facility import Facility, Area from app.models.facility import Facility, Area
@@ -677,6 +678,11 @@ def inspector_performance():
recent_inspections = ( recent_inspections = (
Inspection.query Inspection.query
.options(
joinedload(Inspection.facility),
joinedload(Inspection.area),
joinedload(Inspection.template),
)
.filter( .filter(
Inspection.inspector_id == selected_id, Inspection.inspector_id == selected_id,
Inspection.inspection_date >= start, Inspection.inspection_date >= start,
@@ -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}'
))