From 17cac8543ff4cd8f9db0235f28d2af6dea97a259 Mon Sep 17 00:00:00 2001 From: patrick Date: Tue, 8 Sep 2026 11:31:49 +0200 Subject: [PATCH] =?UTF-8?q?perf(db):=2080=20fehlende=20Indizes=20auf=20Fre?= =?UTF-8?q?mdschl=C3=BCssel-Spalten=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Postgres legt für FK-Spalten selbst KEINEN Index an (nur für die referenzierte PK-Seite) - grep über alle Models ergab 0 explizite Indizes im gesamten Schema trotz 86 Fremdschlüsseln. Ohne Index braucht jede Lookup-Query von der "viele"-Seite (z.B. "alle Fehlbestände zu diesem Objekt", "alle Kontrollen zu diesem Benutzer") einen Sequential Scan - bei aktueller Datenmenge unauffällig, würde mit wachsendem Bestand aber zum Problem. 5 Spalten ausgelassen, weil bereits über eine UniqueConstraint als führende Spalte indiziert (Postgres nutzt den führenden Teil eines zusammengesetzten Unique-Index für Einzelspalten-Lookups genauso): objektposition.objekt_id, bestand.lagerplatz_id, beladungsvorlage. objekttyp_id, vorlagenposition.vorlage_id, benutzer_qualifikation. benutzer_id. dokument.vorgaenger_id hatte bereits einen Index (Migration 0027). Fund beim Abgleich gegen die database-optimization-Checkliste aus ai-coding-starter-kit. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01CVgbozhYmuEhiEJHffRXCV --- backend/alembic/versions/0028_fk_indizes.py | 120 ++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 backend/alembic/versions/0028_fk_indizes.py diff --git a/backend/alembic/versions/0028_fk_indizes.py b/backend/alembic/versions/0028_fk_indizes.py new file mode 100644 index 0000000..1c25e27 --- /dev/null +++ b/backend/alembic/versions/0028_fk_indizes.py @@ -0,0 +1,120 @@ +"""Fehlende Indizes auf Fremdschlüssel-Spalten (Postgres legt für FK-Spalten +selbst KEINEN Index an, nur für die referenzierte PK-Seite - ohne expliziten +Index braucht jede Lookup-Query von der "viele"-Seite (z.B. "alle Positionen +zu diesem Objekt", "alle Fehlbestände zu diesem Material") einen Sequential +Scan). Gefunden beim Abgleich gegen ai-coding-starter-kit-Checkliste +(docs/production/database-optimization.md) - 0 explizite Indizes im +gesamten Modellbestand. + +Spalten, die bereits über eine UniqueConstraint als führende Spalte indiziert +sind, werden hier bewusst ausgelassen (Postgres nutzt den führenden Teil +eines zusammengesetzten Unique-Index genauso für Einzelspalten-Lookups): +objektposition.objekt_id, bestand.lagerplatz_id, beladungsvorlage.objekttyp_id, +vorlagenposition.vorlage_id, benutzer_qualifikation.benutzer_id. +dokument.vorgaenger_id hat bereits einen Index aus Migration 0027. + +Revision ID: 0028_fk_indizes +Revises: 0027_dokument_versionierung +Create Date: 2026-09-08 +""" +from typing import Sequence, Union + +from alembic import op + +revision: str = "0028_fk_indizes" +down_revision: Union[str, None] = "0027_dokument_versionierung" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + +_INDIZES = [ + ("ausgabe", "material_id"), + ("ausgabe", "geraet_instanz_id"), + ("ausgabe", "empfaenger_id"), + ("ausgabe", "ausgegeben_von"), + ("benutzer", "einheit_id"), + ("benutzer_rolle", "benutzer_id"), + ("dokument", "hochgeladen_von"), + ("fahrzeugdetails", "objekt_id"), + ("fehlbestand", "erzeugt_von_server_id"), + ("fehlbestand", "objekt_id"), + ("fehlbestand", "material_id"), + ("fehlbestand", "standort_id"), + ("fehlbestand", "festgestellt_von"), + ("fehlbestand", "kontrolle_id"), + ("fehlbestand", "verantwortlicher_id"), + ("geraet_instanz", "objektposition_id"), + ("historie", "erzeugt_von_server_id"), + ("historie", "benutzer_id"), + ("kontrolle", "erzeugt_von_server_id"), + ("kontrolle", "objekt_id"), + ("kontrolle", "benutzer_id"), + ("kontrollposition", "kontrolle_id"), + ("kontrollposition", "material_id"), + ("lagerort", "parent_id"), + ("lagerplatz", "lagerort_id"), + ("bestand", "material_id"), + ("materialbewegung", "material_id"), + ("materialbewegung", "von_lagerplatz_id"), + ("materialbewegung", "nach_lagerplatz_id"), + ("materialbewegung", "benutzer_id"), + ("lagerbewegung", "objekt_id"), + ("lagerbewegung", "von_standort_id"), + ("lagerbewegung", "nach_standort_id"), + ("lagerbewegung", "benutzer_id"), + ("mangel", "objekt_id"), + ("mangel", "gemeldet_von"), + ("mangel", "erledigt_von"), + ("mindermengen_genehmigung", "fehlbestand_id"), + ("mindermengen_genehmigung", "genehmigt_von"), + ("mindermengen_genehmigung", "ausloesende_kontrolle_id"), + ("mindermengen_genehmigung", "beendende_kontrolle_id"), + ("nachfuellung", "fehlbestand_id"), + ("nachfuellung", "objekt_id"), + ("nachfuellung", "material_id"), + ("nachfuellung", "benutzer_id"), + ("objekt", "objekttyp_id"), + ("objekt", "vorlage_id"), + ("objekt", "standort_id"), + ("objekt", "zustaendiger_server_id"), + ("objekt", "fahrzeug_id"), + ("objekt", "einheit_id"), + ("objektposition", "material_id"), + ("rolle_berechtigung", "rolle_id"), + ("rolle_berechtigung", "berechtigung_id"), + ("benutzer_rolle_zuordnung", "benutzer_id"), + ("benutzer_rolle_zuordnung", "rolle_id"), + ("einheit", "uebergeordnete_einheit_id"), + ("einheit", "standort_id"), + ("benutzer_qualifikation", "qualifikationstyp_id"), + ("objekttyp_qualifikationsanforderung", "objekttyp_id"), + ("objekttyp_qualifikationsanforderung", "qualifikationstyp_id"), + ("kategorie", "bereich_id"), + ("kategorie", "ueberkategorie_id"), + ("objekttyp", "bereich_id"), + ("objekttyp", "kategorie_id"), + ("fach", "objekttyp_id"), + ("material", "kategorie_id"), + ("vorlagenposition", "material_id"), + ("wartungsplan", "objekttyp_id"), + ("wartungsplan_position", "wartungsplan_id"), + ("wartungsintervall", "wartungsplan_position_id"), + ("wartungsauftrag", "objekt_id"), + ("wartungsauftrag", "geraet_instanz_id"), + ("wartungsauftrag", "wartungsplan_position_id"), + ("wartungsauftrag", "durchgefuehrt_von"), + ("zustaendigkeit", "benutzer_id"), + ("zustaendigkeit", "standort_id"), + ("zustaendigkeit", "objekt_id"), + ("kontrollverantwortung", "objekt_id"), + ("kontrollverantwortung", "benutzer_id"), +] + + +def upgrade() -> None: + for tabelle, spalte in _INDIZES: + op.execute(f"CREATE INDEX IF NOT EXISTS ix_{tabelle}_{spalte} ON {tabelle} ({spalte})") + + +def downgrade() -> None: + for tabelle, spalte in _INDIZES: + op.execute(f"DROP INDEX IF EXISTS ix_{tabelle}_{spalte}")