Indizes
Status: Entwurf · Spec-Kandidat: ja
Pflicht-Indizes (Bestandteil des initialen DDL)
-- Eindeutigkeit nur unter aktiven (nicht soft-gelöschten) Datensätzen,-- jeweils pro Tenant.CREATE UNIQUE INDEX uq_entity_code_active ON mdm.DDM_ENTITY (tenant_id, entity_type_id, code) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX uq_entity_relation_active ON mdm.DDM_ENTITY_RELATION (tenant_id, relation_type_id, from_entity_id, to_entity_id) WHERE deleted_at IS NULL;
-- Lookup-Indizes auf Metadaten-KeysCREATE INDEX ix_entity_type_key ON mdm.DDM_ENTITY_TYPE (key);CREATE INDEX ix_enum_set_key ON mdm.DDM_ENUM_SET (key);CREATE INDEX ix_relation_type_key ON mdm.DDM_RELATION_TYPE (key);
-- Attribut-Lookups je Entitätstyp (sortierbar)CREATE INDEX ix_entity_type_attribute_entity_type ON mdm.DDM_ENTITY_TYPE_ATTRIBUTE (entity_type_id, sort_order, key);
-- Häufigste Filter auf DDM_ENTITY (Tenant + Typ + Status, nur aktiv)CREATE INDEX ix_entity_entity_type_status ON mdm.DDM_ENTITY (tenant_id, entity_type_id, status) WHERE deleted_at IS NULL;
CREATE INDEX ix_entity_deleted_at ON mdm.DDM_ENTITY (deleted_at);
-- Volltext-Index (search_vector wird trigger-gepflegt, siehe Trigger-Spec)CREATE INDEX ix_entity_search_vector_gin ON mdm.DDM_ENTITY USING GIN (search_vector);
-- Hinweis: Ein generischer GIN auf `attributes` ist NICHT pauschal Teil des-- Initial-DDL. Er wird selektiv pro DDM_ENTITY_TYPE aktiviert, abhängig vom-- Flag DDM_ENTITY_TYPE.is_external_source_target. Siehe Abschnitt-- "Search-Index-Strategie für JSONB-Attribute (OP-17)" weiter unten.
-- Beziehungs-LookupsCREATE INDEX ix_entity_relation_from ON mdm.DDM_ENTITY_RELATION (from_entity_id) WHERE deleted_at IS NULL;
CREATE INDEX ix_entity_relation_to ON mdm.DDM_ENTITY_RELATION (to_entity_id) WHERE deleted_at IS NULL;
-- Audit-LogCREATE INDEX ix_audit_log_lookup ON mdm.AA_AUDIT_LOG (entity_name, entity_id, changed_at DESC);
CREATE INDEX ix_audit_log_actor ON mdm.AA_AUDIT_LOG (actor_principal_id, changed_at DESC);
CREATE INDEX ix_audit_log_tenant ON mdm.AA_AUDIT_LOG (tenant_id, changed_at DESC) WHERE tenant_id IS NOT NULL;
-- =====================================================================-- Tenant-- =====================================================================
CREATE INDEX ix_tenant_key ON mdm.AA_TENANT (key) WHERE deleted_at IS NULL;
-- =====================================================================-- Outbox---- Dispatcher liest pending-Events strikt FIFO je aggregate (Reihenfolge--- Garantie pro Entität), daher (status, available_at, id) als Worker-Index.-- =====================================================================
CREATE INDEX ix_outbox_event_pending ON mdm.AA_OUTBOX_EVENT (status, available_at, id) WHERE status = 'pending';
CREATE INDEX ix_outbox_event_aggregate ON mdm.AA_OUTBOX_EVENT (aggregate_type, aggregate_id, id);
CREATE INDEX ix_outbox_event_correlation ON mdm.AA_OUTBOX_EVENT (correlation_id) WHERE correlation_id IS NOT NULL;
-- =====================================================================-- DDM_RELATION_TYPE_ATTRIBUTE-- =====================================================================
CREATE INDEX ix_relation_type_attribute_relation_type ON mdm.DDM_RELATION_TYPE_ATTRIBUTE (relation_type_id, sort_order, key);
-- =====================================================================-- RBAC + ACL-- =====================================================================
-- Identity LookupsCREATE INDEX ix_app_user_email ON mdm.AA_APP_USER (lower(email)) WHERE deleted_at IS NULL AND email IS NOT NULL;
CREATE INDEX ix_service_account_key ON mdm.AA_SERVICE_ACCOUNT (key) WHERE deleted_at IS NULL;
CREATE INDEX ix_principal_group_key ON mdm.AA_PRINCIPAL_GROUP (key) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX uq_principal_group_member_active ON mdm.AA_PRINCIPAL_GROUP_MEMBER (principal_group_id, member_principal_type, member_principal_id) WHERE deleted_at IS NULL;
CREATE INDEX ix_principal_group_member_lookup ON mdm.AA_PRINCIPAL_GROUP_MEMBER (member_principal_type, member_principal_id) WHERE deleted_at IS NULL;
-- RBACCREATE INDEX ix_role_key ON mdm.AA_ROLE (key) WHERE deleted_at IS NULL;
CREATE UNIQUE INDEX uq_role_permission_active ON mdm.AA_ROLE_PERMISSION (role_id, permission_id, coalesce(scope_entity_type_id, '00000000-0000-0000-0000-000000000000'::uuid), coalesce(scope_entity_id, '00000000-0000-0000-0000-000000000000'::uuid), coalesce(scope_relation_type_id, '00000000-0000-0000-0000-000000000000'::uuid)) WHERE deleted_at IS NULL;
CREATE INDEX ix_role_permission_role ON mdm.AA_ROLE_PERMISSION (role_id) WHERE deleted_at IS NULL;
-- Principal-Role-Auflösung (Hot Path bei jedem Request)CREATE UNIQUE INDEX uq_principal_role_active ON mdm.AA_PRINCIPAL_ROLE (principal_type, principal_id, role_id, scope, coalesce(scope_entity_type_id, '00000000-0000-0000-0000-000000000000'::uuid), coalesce(scope_entity_id, '00000000-0000-0000-0000-000000000000'::uuid), coalesce(scope_relation_type_id, '00000000-0000-0000-0000-000000000000'::uuid)) WHERE deleted_at IS NULL;
CREATE INDEX ix_principal_role_principal ON mdm.AA_PRINCIPAL_ROLE (principal_type, principal_id) WHERE deleted_at IS NULL;
CREATE INDEX ix_principal_role_role ON mdm.AA_PRINCIPAL_ROLE (role_id) WHERE deleted_at IS NULL;
-- ACL-Lookups (per principal + scope)CREATE INDEX ix_acl_entry_principal ON mdm.AA_ACL_ENTRY (principal_type, principal_id) WHERE deleted_at IS NULL;
CREATE INDEX ix_acl_entry_entity ON mdm.AA_ACL_ENTRY (scope_entity_id) WHERE deleted_at IS NULL AND scope_entity_id IS NOT NULL;
CREATE INDEX ix_acl_entry_entity_type ON mdm.AA_ACL_ENTRY (scope_entity_type_id) WHERE deleted_at IS NULL AND scope_entity_type_id IS NOT NULL;
CREATE INDEX ix_acl_entry_relation_type ON mdm.AA_ACL_ENTRY (scope_relation_type_id) WHERE deleted_at IS NULL AND scope_relation_type_id IS NOT NULL;Search-Index-Strategie für JSONB-Attribute (OP-17)
V1-Entscheidung (2026-04-29): zustandsgeführte Expression-Indizes über Async-Job, durchgesetzt über UI-Status-Modell. GIN-Catch-all wird selektiv nur für External-Source-Target-Typen aktiviert. Internal-Only-Typen verzichten auf den Catch-all und werden über UI-Sperren plus Engineering-Disziplin abgesichert.
Managed Expression-Indizes (alle Typen)
Expression-Indizes auf searchable- und sortable-Attributen sind nicht Teil des Initial-DDL. Sie werden bei Konfigurationsänderung von DDM_ENTITY_TYPE_ATTRIBUTE.searchable / .sortable über AA_JOB.job_kind='index_sync' aufgebaut bzw. abgebaut. Build läuft CONCURRENTLY. Naming-Konvention: ix_entity_<type_key>_<attr_key> (truncated SHA bei Längenüberschreitung des Postgres-Limits).
Jedes Attribut führt zwei zusätzliche Spalten in DDM_ENTITY_TYPE_ATTRIBUTE:
index_status text— eines auspending | building | active | failedindex_built_at timestamptz
Beispiele für die generierten Indizes:
-- searchable=true (Text)CREATE INDEX CONCURRENTLY ix_entity_kunde_customer_number ON mdm.DDM_ENTITY ((attributes ->> 'customer_number')) WHERE entity_type_id = '<kunde-type-id>' AND deleted_at IS NULL;
-- sortable=true mit numerischer CoercionCREATE INDEX CONCURRENTLY ix_entity_kunde_annual_revenue ON mdm.DDM_ENTITY (((attributes ->> 'annual_revenue')::numeric)) WHERE entity_type_id = '<kunde-type-id>' AND deleted_at IS NULL;Durchsetzung:
- UI und REST-API sperren Filter und Sort gegen ein Attribut, solange
index_status != 'active'. Filter-Picker und Quick-Search listen ausschließlich Attribute mitsearchable=true. REST-API rejectet?filter[<key>]=...für nicht-searchable-Pfade mit409 attribute_not_filterable. - Saved Searches mit
pending/failed-Attributen zeigen Warnung und sind nicht ausführbar, bisactive. - Drop bei
searchable=false/sortable=false: Service prüft Referenzen insaved_search/report_viewund blockt mit Liste der Abhängigkeiten. Force-Drop ist möglich, wird aber auditiert (AA_AUDIT_LOG-Aktionattribute_index_force_drop). - Type-Coercion-Schutz: vor Build eines numerischen / Datum-Index läuft Drift-Check gegen
mdm.v_entity_attribute_drift. Build startet erst bei Drift = 0; Steward-Override ist möglich, wird auditiert. - Fehlertoleranz: gescheiterter Build setzt
index_status='failed', schreibtAA_OUTBOX_EVENTindex_build_failedund erscheint inmdm.v_index_drift(Soll/Ist je Attribut+Typ). Steward-Inbox.
GIN-Catch-all (nur External-Source-Target-Typen)
DDM_ENTITY_TYPE.is_external_source_target boolean DEFAULT false markiert Typen, deren Daten aus Quellsystemen oder Bulk-Imports stammen. Auto-Setzung sobald erster DDM_ENTITY_EXTERNAL_ID-Eintrag oder DDM_SOURCE_SYSTEM-Mapping für den Typ existiert; Steward kann das Flag manuell vorab setzen.
Für solche Typen wird ein partieller GIN-Catch-all-Index pro Typ angelegt:
CREATE INDEX CONCURRENTLY ix_entity_attributes_gin_<type_key> ON mdm.DDM_ENTITY USING GIN ((attributes) jsonb_path_ops) WHERE entity_type_id = '<uuid>' AND deleted_at IS NULL;Build erfolgt automatisch beim Setzen des Flags über AA_JOB.job_kind='gin_backfill'. Promotion (intern → external) ist regulärer Workflow; Demotion (external → intern) ist Admin-Migration und droppt den Index nicht automatisch (Sicherheitsnetz bleibt, solange historische Daten vorhanden).
Der Catch-all dient als Sicherheitsnetz für:
- Schema-Drift aus Quellsystemen, die unbekannte Keys liefern (in Verbindung mit Lazy-Validate, OP-19).
- Bulk-Import-Drift (OP-41).
- Generische JSONB-Path-Queries für Match/Merge (OP-29) und External-ID-Heuristiken (OP-30).
- Steward-Reports „Diff zwischen MDM und Quellsystem”.
Internal-Only-Typen (is_external_source_target=false) bekommen keinen Catch-all. Filter/Sort-Pfade laufen ausschließlich über die managed Expression-Indizes oben. UI-Sperren und die folgende Engineering-Regel ersetzen das Sicherheitsnetz.
Engineering-Regel: keine Service-Path-Query ohne deklarierten Index
Service-Code darf JSONB-Path-Queries (->, ->>, @>, ?, jsonb_path_*) ausschließlich gegen explizit deklarierte Indizes ausführen. Jede Match/Merge-Regel und jede External-ID-Heuristik bringt ihre Index-Anforderung als Migration mit:
-- Beispiel: Match-Regel "tax_id-Exact-Match" auf kundeCREATE INDEX ix_entity_kunde_tax_id_path ON mdm.DDM_ENTITY USING GIN ((attributes -> 'tax_id') jsonb_path_ops) WHERE entity_type_id = '<kunde-type-id>' AND deleted_at IS NULL;Diese Regel gilt strikt für Internal-Only-Typen und weicher (Catch-all als Fallback) für External-Source-Target-Typen. Code-Review erzwingt die Deklaration.
Trigram-Indizes (Fuzzy-Suche, OP-43)
pg_trgm-Indizes folgen der gleichen Async-Job-Mechanik. Auslöser ist DDM_ENTITY_TYPE_ATTRIBUTE.fuzzy=true; der Job legt einen GIN-trgm-Index auf lower(attributes ->> '<attr_key>') an, gefiltert nach entity_type_id und deleted_at IS NULL.
Strategie
- Partial Indexes auf
deleted_at IS NULLsind Standard. - Expression-Indizes für JSONB-Filter/Sort werden async und zustandsgeführt verwaltet — Plattform ist runtime-konfigurierbar, daher kein DDL-Hardcoding.
- GIN-Catch-all kommt selektiv für External-Source-Target-Typen, weil dort Schema-Drift, Bulk-Import-Drift und generische Path-Queries echte Treiber sind.
- UI- und Engineering-Disziplin ersetzen den allgemeinen Catch-all für rein internal gepflegte Typen.
Offen
- Auto-Setzung von
is_external_source_target=truebeim erstenDDM_ENTITY_EXTERNAL_ID- bzw.DDM_SOURCE_SYSTEM-Mapping wird als Trigger / Service-Layer-Regel in04-triggers.mdbzw.../50-behavior/01-validation.mdausgearbeitet. - Worker-Implementierung für
index_sync/gin_backfill(Coercion-Mappingdata_type→::numeric/::date/::timestamptz, Naming-Truncation bei langen Type-/Attribut-Keys, Force-Drop-Audit) wird mit Backend-Pick (OP-08) festgelegt. - Drift-Detektion für GIN-Catch-all (existiert der Index für jeden
is_external_source_target=true-Typ?) läuft Aufgabe voncleanup_jobs, konkrete Frequenz und Alarm-Schwelle bleiben offen.