diff --git a/steward/migrations/versions/0023_plugin_metrics_indexes.py b/steward/migrations/versions/0023_plugin_metrics_indexes.py new file mode 100644 index 0000000..89630c4 --- /dev/null +++ b/steward/migrations/versions/0023_plugin_metrics_indexes.py @@ -0,0 +1,39 @@ +"""Index plugin_metrics for host/dashboard reads + +plugin_metrics had only a PK on id, so every host-detail / full-metrics / +dashboard-widget query (all filter by source_module + resource_name over a +recorded_at range) sequentially scanned the whole time-series table — slower as +samples accumulate. Add the two composite indexes that match those query shapes. + +The non-concurrent CREATE INDEX takes a brief exclusive lock; it runs once at +startup migration time, acceptable for this table. + +Revision ID: 0023_plugin_metrics_indexes +Revises: 0022_unify_monitors +Create Date: 2026-06-20 +""" +from typing import Sequence, Union +from alembic import op + +revision: str = "0023_plugin_metrics_indexes" +down_revision: Union[str, None] = "0022_unify_monitors" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + op.create_index( + "ix_plugin_metrics_module_resource_recorded", + "plugin_metrics", + ["source_module", "resource_name", "recorded_at"], + ) + op.create_index( + "ix_plugin_metrics_module_resource_metric_recorded", + "plugin_metrics", + ["source_module", "resource_name", "metric_name", "recorded_at"], + ) + + +def downgrade() -> None: + op.drop_index("ix_plugin_metrics_module_resource_metric_recorded", "plugin_metrics") + op.drop_index("ix_plugin_metrics_module_resource_recorded", "plugin_metrics") diff --git a/steward/models/metrics.py b/steward/models/metrics.py index 85a8429..979b68f 100644 --- a/steward/models/metrics.py +++ b/steward/models/metrics.py @@ -1,7 +1,7 @@ from __future__ import annotations import uuid from datetime import datetime, timezone -from sqlalchemy import DateTime, Float, String +from sqlalchemy import DateTime, Float, Index, String from sqlalchemy.orm import Mapped, mapped_column from .base import Base @@ -17,3 +17,14 @@ class PluginMetric(Base): recorded_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc) ) + + # This time-series table grows by (sources × resources × sample cadence); every + # host-detail / full-metrics / dashboard-widget read filters by + # (source_module, resource_name) over a recorded_at range. Without these it's a + # full sequential scan on every load. + __table_args__ = ( + Index("ix_plugin_metrics_module_resource_recorded", + "source_module", "resource_name", "recorded_at"), + Index("ix_plugin_metrics_module_resource_metric_recorded", + "source_module", "resource_name", "metric_name", "recorded_at"), + )