06/08 Optimize queries with indexes
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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())
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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}'
|
||||||
|
))
|
||||||
Reference in New Issue
Block a user