V1 Grid-Performance-Analyse — Variable Grids bei großen Datenbeständen (OpenShift)¶
Zweck: Vollständige Architektur- und Hotspot-Analyse der V1-Grids (Yii 1.1,
httpdocs/), damit spätere Sessions/PRs direkt auf dieser Referenz aufsetzen können, ohne den Code erneut explorieren zu müssen. Alle Aussagen sind mitDatei:Zeilebelegt (Stand: 2026-07,develop).Anlass: Auf OpenShift ist der Aufbau großer Grids (z. B. Inventar) und das Öffnen von Detailmasken extrem langsam. Referenz-Datenbestand (Kunde Heilkunde): ~50.000 Inventare, ~2 Mio. Kalibrierungen. Auf einer VDI mit Docker direkt auf schneller Hardware tritt das Problem nicht auf. Gesucht: Beschleunigung der Datenanzeige ohne Caching-Mechanismen.
Verwandte Dokumente:
- analysis-v1-datatable.md — Struktur-Referenz des DataTable-Systems
- v1-grid-fieldmanagement-analysis.md — Feldmanagement
- GRID_OVERVIEW.md (Repo-Root) — Katalog aller ~161 Grids inkl. v1/v2-Status
- OPENSHIFT_DEBUG_SUMMARY.md — OpenShift-Stabilisierung 2026-03
1. Kernaussage (TL;DR)¶
Die Grid-Datenquery selbst ist nicht der dominante Kostenfaktor. Die Zeit geht in
viele kleine, serielle DB-Roundtrips pro Request (Übersetzungen, N+1-Lazy-Loads pro
Zeile, Konfigurations-Lookups, Infrastruktur-Proben) sowie — bei Sortierung/Filterung über
Kalibrierfelder — in eine ungünstige Join-Form auf die 2-Mio-Tabelle calibration.
Warum OpenShift besonders leidet: Die Query-Anzahl wird mit der Netzwerk-Latenz zur DB
multipliziert. Beispielrechnung: 400 Queries/Request × 0,05 ms (Docker, DB auf demselben Host)
= 20 ms — unsichtbar. Dieselben 400 Queries × 2–5 ms (OpenShift, DB als Service über das
Pod-Netz) = 0,8–2 s pro Request, zzgl. File-I/O auf NFS/PVC. Der Code ist also nicht
„auf OpenShift kaputt", sondern latenz-empfindlich — und große Datenbestände erhöhen
zusätzlich sowohl die Query-Anzahl (Übersetzungsmodus „Version 2", siehe 4.1) als auch die
Kosten der Einzelqueries (Joins auf calibration).
Die fünf größten Hebel (ohne Caching), Details in Abschnitt 6:
| # | Maßnahme | Wirkung | Aufwand |
|---|---|---|---|
| 1 | tt()-Übersetzungen: Bulk-Load pro Kategorie statt 1 Query pro String (Version-2-Modus) |
eliminiert hunderte Queries/Request auf jeder Seite | klein |
| 2 | N+1 im Grid-Rendering beseitigen (Kategorie-Farbe, duedate/lastcal, last_certificate, lastUser → Batch-Load pro Seite) | eliminiert ~1–4 Queries pro Zeile | mittel |
| 3 | CacheHelper::flushAll() aus KTbExtendedGridView::beginCache/endCache entfernen |
stoppt die Selbstzerstörung von Schema-/Query-Cache bei jedem Grid-AJAX | klein |
| 4 | Fällige/letzte Kalibrierung denormalisieren (Spalten auf inventory, per Save-Hook gepflegt) |
eliminiert Join/GROUP BY auf 2-Mio-Tabelle in Grid, Header-Countern und Dashboard; macht duedate-Sort/Filter indexfähig | mittel–groß |
| 5 | Index-Lücken schließen: calibration.C2303, inventory.I4201; 2026er-Index-Migrationen auf Kundensystemen verifizieren |
Sort/Filter „Fällig am" ohne Filesort; Hierarchie-Joins indexiert | klein |
2. Architektur: Request-Fluss der variablen Grids¶
2.1 Beteiligte Bausteine¶
| Baustein | Datei | Rolle |
|---|---|---|
| Controller-Action | httpdocs/protected/modules/frontend/controllers/FrontendInventoryController.php:531 (actionAdmin) |
Filter-/Sort-Parameter → Model, wählt View (v1 admin / v2 admin_v2 per Flag features/grid/use_legacy_inventory, Z. 754–765, 784–795) |
| Haupt-View | .../views/frontendInventory/admin_v2.php |
Seite: Tabs, Action-Menü, Modals; inkludiert _columns_v2.php + _admin_v2.php |
| Spalten-Aufbau | .../views/frontendInventory/_columns_v2.php |
Action-Buttons + FrontendProperty::getGridViewShownColumns() (Z. 202) → nur sichtbare Spalten; der volle Spaltenkatalog lädt lazy per loadColumns-AJAX (Z. 175–195) |
| Spalten-Fabrik | httpdocs/protected/modules/frontend/components/GridColumn.php:382 (getColumn, 10.862 Zeilen, 558 cases) |
erzeugt pro sichtbarer Spalte die Widget-Config inkl. value-Expressions, Filter, Editable-Setup |
| Feldkonfiguration | field_configuration-Tabelle via InventoryHelper::getTableFields/getFieldsFaster (httpdocs/protected/helpers/InventoryHelper.php:445–731) |
definiert verfügbare/sichtbare/editierbare Felder je Tabelle; nur per-Request statisch memoisiert |
| Sichtbare Spalten | FrontendProperty::getVisibleColumns (FrontendProperty.php:1131) → FrontendTableViews::getStandardView (FrontendTableViews.php:135) |
Benutzer-/Standard-Tabellenansicht (1–2 Queries, läuft doppelt: in search() und in _columns) |
| DataProvider | FrontendInventory::search() (FrontendInventory.php:2912–3873) |
baut KCDbCriteria (Filter aus ~70 compare()-Aufrufen), Relationen, Sort-Attribute; separater Count |
| Provider-Klasse | httpdocs/protected/components/KCActiveDataProvider.php |
pageSize-Handling ('all' → PHP_INT_MAX, Z. 83–85!), Query-Cache-Hook (cache_duration = 3600, Z. 47) |
| Grid-Widget | httpdocs/protected/components/KTbExtendedGridView.php (+ KGridViewTrait.php, GridViewHelper.php) |
HTML-Rendering, Fragment-Cache, Bulk-Actions, Summary/Pager |
| Zellen | KTbDataColumn, KTbEditableColumn/KTbEditableField, TButtonColumn, KCCheckBoxColumn (httpdocs/protected/components/) |
pro Zelle evaluateExpression; Editable = volles Widget pro Zelle |
| Übersetzung | tt() (helpers/helperFunctions.php:822) → AdminMessage::tt (modules/adminpanel/models/AdminMessage.php:66–326) |
DB-basiert, ohne Cross-Request-Cache (Kernbefund 4.1) |
2.2 Ablauf¶
Erstaufruf (Nicht-AJAX): Layout (httpdocs/themes/calhelp/views/layouts/main.php) mit
~175 tt()-Aufrufen und 2 Fällig-Zählern (FrontendInventory::getTotalNotification('due'/'overdue'),
Layout Z. 170–171 → FrontendInventory.php:5486) → admin_v2.php → _columns_v2.php
(Spalten) → _admin_v2.php mit $dataProvider = $model->search() (Z. 99) → Widget rendert
Filter-Zeile, Datenzeilen, Summary, Pager. Das Grid wird beim Erstaufruf mit Daten
gerendert (kein Deferred-Load; anders als das Kalibrier-Grid, s. 2.3).
AJAX-Update (jede Filter-Eingabe, jeder Sort-Klick, jeder Seitenwechsel via
$.fn.yiiGridView.update): gleiche Action mit $_GET['ajax'] = 'inventory-grid' →
renderPartial('admin_v2') (Controller Z. 754–765) → kompletter Neuaufbau von Spalten,
search(), Count und HTML. Es gibt kein JSON-/Delta-Protokoll — jede Interaktion ist ein
voller Server-Render des Grid-Fragments.
Fragment-Cache: KTbExtendedGridView::run() (Z. 115–138) umschließt die Ausgabe mit
COutputCache — Key User_<id>_Language_<lang>_<controller>_<gridId>,
varyByExpression = http_build_query($_GET) (Z. 79–80), Dauer Controller::getCacheDuration()
= Property settings/cache/duration, Default 120 s (Controller.php:18,693–708). Da der Key
über alle GET-Parameter variiert, trifft er bei interaktiver Filterei praktisch nie — und
wird durch den flushAll-Bug (4.3) ohnehin sofort wieder gelöscht.
2.3 Detailmasken¶
actionView(FrontendInventoryController.php:843): lädt das Inventar ohnewith()(Z. 863);view.php(1.689 Zeilen) rendert ~11 Tab-Grids (Kalibrierungen, Repair, Location, Notepad, Orders, Child-Inventory, DMS, Tickets, Audit-Historie, Mailer-Actions, Specifications). Relationen (customer,lastUser,lastRental,type) werden lazy im View nachgeladen. Workaround im Code:@set_time_limit(300); @ini_set('memory_limit','512M')(Z. 854–855) — der Kommentar (Z. 845–853) beschreibt das eigentliche Problem: pro Kalibrier-Zeile wird einFrontendCalibration-AR inkl. inventory/customer/type/standards/ results/lastRental/DMS-Relationen hydriert.- Das Kalibrier-Grid im Detail lädt deferred:
FrontendCalibration::search()liefert bei Nicht-AJAX einen leeren Provider (FrontendCalibration.php:3287–3292); die Daten kommen per Grid-AJAX nach. SortierungC2301 DESC, C2333 DESC, C2339 DESC(Z. 3794–3796) + Filtert.MTAG = <id>(Z. 3379) ist seit Jan-2026 durchindex_MTAG_C2301_C2333_C2339gedeckt. - Teure Per-Zeilen-Pfade im Kalibrier-Grid: Dokument-/Zertifikat-Spalten →
getDocumentUrl()→Model::loadMergedRelations()(Model.php:327–341) = 1 Query pro Zeile, wenn die cert-Relationen nicht vorgeladen sind.
3. Query-Ebene: Anatomie von FrontendInventory::search()¶
FrontendInventory.php:2912–3873. Positiv: SELECT enthält nur sichtbare Felder (Z. 2986),
der Count läuft mit vereinfachtem Criteria (Bedingungen ja, Anzeige-Joins nein,
Z. 3810–3828), totalItemCount wird genau einmal berechnet und via
setTotalItemCount() gesetzt (Z. 3855–3858).
3.1 Join-Form bei Kalibrier-Filter/-Sortierung (duedate, lastcal, Benachrichtigungs-Links)¶
Relation (FrontendInventory.php:342–347):
'activeCalibrationGrid' => array(
self::HAS_ONE, 'FrontendCalibration', 'MTAG',
'on' => '(activeCalibrationGrid.C2339 = 1 OR activeCalibrationGrid.C2339 IS NULL)',
'joinType' => 'LEFT JOIN',
'together' => true
),
Aktiviert bei: duedate-Filter (Z. 3259–3308, u. a. Dashboard-Links ?notification=due|overdue),
lastcal (Z. 3310–3346), last_order_number (Z. 3348–3353), Sortierung auf diese Spalten
(Z. 3682–3713), Advanced-Filter mit Kalibrier-Variablen (Z. 3178–3181). Resultierende Form:
SELECT t.<felder> FROM inventory t
LEFT JOIN calibration acg ON acg.MTAG = t.MTAG AND (acg.C2339 = 1 OR acg.C2339 IS NULL)
[JOIN map_user / status / customers / types ...]
WHERE ... AND acg.C2303 < :heute ...
GROUP BY t.MTAG -- Dedup, s. 3.2
ORDER BY acg.C2303 ASC -- kein Index auf C2303!
LIMIT 10;
-- plus identisch gefilterte COUNT-Query (Z. 3855)
Probleme:
1. OR C2339 IS NULL verhindert einen sauberen Index-Zugriff auf
(MTAG, C2339) und lässt bei Altdaten (NULL-Flag) alle Kalibrierungen eines Geräts in
den Join laufen (Fan-out Richtung 2 Mio. Zeilen).
2. GROUP BY t.MTAG über das Join-Ergebnis + ORDER BY auf der Join-Spalte C2303
→ volle Materialisierung + Filesort bei jedem Request; LIMIT 10 hilft nicht.
3. C2303 (Fällig-Datum) hat in keiner Migration einen Index (geprüft: alle
add_missing_indexes*-Migrationen).
4. Die COUNT-Query wiederholt denselben Join (relationsCount, Z. 3816–3823).
3.2 Dauerhaftes GROUP BY t.MTAG¶
shouldDeduplicateInventoryGrid() (Z. 2798–2810) ist per Default an (abschaltbar nur via
ENV ENABLE_INVENTORY_GRID_DEDUP=0) → die Datenquery hat immer GROUP BY t.MTAG
(Z. 2989–2994), auch ohne jeden Join. Nötig ist das nur, wenn HAS_MANY-Relationen
(mapusers, Dokument-Relationen) together gejoint werden und Zeilen duplizieren.
3.3 Sichtbarkeits-Join für Nicht-Global-User¶
Ohne inventory_global-Recht wird mapusers (HAS_MANY!) together gejoint + auf
mapusers.user_uID = <user> gefiltert (Z. 3076–3081) → Zeilen-Fan-out (daher das GROUP BY)
statt eines EXISTS-Subselects. map_user selbst ist gut indiziert
(m130220_194523_install.php:320–327).
3.4 Filter-Semantik¶
- Text-Filter →
LIKE '%wert%'(prepareSearchValue,helperFunctions.php:8483–8501) — Contains-Suche, kein Index nutzbar (bei 50k Inventarzeilen allein akzeptabel, in Kombination mit Joins teuer). - „Exakt"-Modus →
BINARY spalte = wert(KCDbCriteria.php:186) — derBINARY-Cast auf der Spalte verhindert ebenfalls Indexnutzung. - Status-/Zustands-Startfilter joinen
status(LEFT JOIN + OR-Kette,addSomeFilters, Z. 7853–7861) — unkritisch klein, aber Teil jeder Query.
3.5 Indizes (Migrationsstand im Repo)¶
| Tabelle | Vorhanden (Auszug) | Fehlt |
|---|---|---|
calibration |
PK CTAG; UNIQUE (C2301,C2333,MTAG) (m161114...); (C2339,MTAG) (m210407...:8); (MTAG,C2301↓,C2333↓,C2339↓) + (C2301↓,C2333↓,C2339↓) (m260117_074317_add_missing_indexes_v4.php:9,15); MTAG solo (m260528_000001_add_mtag_index_to_calibration.php:16) |
C2303 (due date) — nirgends |
inventory |
PK MTAG; ktag (+FK); I4217 (m190304...:7); path (m210811...:8); I4299, (I4299,MTAG) (m260117...:21,27) |
I4201 (Asset-Nr.; Join-Ziel von childItems, barcode, get_path()) |
location |
(MTAG,L2815,customer_KTAG) + (MTAG,L2815,delivery_customer_KTAG) (m260117...:53,58) |
— |
messages |
(language_code,category_id,message_from) (m260117...:33) |
Nutzen durch BINARY-Vergleich beschnitten (4.1) |
⚠️ Auf Kundensystemen verifizieren, ob m260117_074317 und m260528_000001 bereits
gelaufen sind (SHOW INDEX FROM calibration; bzw. tbl_migration). Vor diesen Migrationen
gab es keinen führenden MTAG-Index auf calibration — exakt das Szenario „Detailmaske
öffnet sich minutenlang" bei 2 Mio. Kalibrierungen.
4. Query-Anzahl pro Request (der OpenShift-Killer)¶
4.1 Übersetzungssystem tt() — größter Einzelposten¶
AdminMessage::tt (AdminMessage.php:66–326), nutzt ausschließlich statische
Per-Request-Maps, keinen Yii-Cache:
- Erster
tt()-Aufruf je Request:BaseAdminMessageCategory::model()->findAll()(Z. 92–97). translationVersionwird bei > 10.000 Zeilen inmessagesautomatisch auf 2 gestellt (Z. 132–147). Genau das ist bei großen/alten Installationen (Heilkunde) der Fall.- Version 2 (Z. 177–207): pro einzelnem String ein
findByAttributesmitBINARY message_from = :message_from(Z. 186) — derBINARY-Cast neutralisiert denmessage_from-Teil des Index. Eine Grid-Seite (Layout ~175tt()+ Spalten-Header + Buttons + Modals) erzeugt so 200–500 serielle Einzelqueries pro Request. - Fallback-Pfad (Z. 234–285): weitere Query + ggf.
addNewMessage()= INSERT während des Renderns (Z. 269–279) — Schreiblast auf dem Lesepfad. - Version 1 (≤ 10.000 Messages) lädt dagegen eine Query pro Kategorie (Z. 149–176) — der performante Modus ist paradoxerweise der für kleine Systeme.
→ Latenz-Mathematik siehe TL;DR. Dieser Pfad erklärt, warum alle Masken (nicht nur Grids) auf OpenShift zäh sind, und warum es mit wachsendem Datenbestand schlimmer wurde (10.000er-Schwelle kippt den Modus).
4.2 N+1 im Grid-Rendering (pro Zeile)¶
| Quelle | Ort | Kosten |
|---|---|---|
Zeilen-CSS Kategorie-Farbe: rowCssClassExpression => '$data->getClassNameForRow($row)' |
_admin_v2.php:141 → FrontendInventory.php:1991–2018, Query Z. 2010–2012 |
FrontendCategory ... find() pro Zeile, unbedingt, unmemoisiert — 50 Zeilen = 50 Queries, auch ohne aktivierte Kategorie-Farben |
| duedate / lastcal / last_order_number (Anzeige) | GridColumn.php:7402,7412,7422 → getValueFromCalibrationField (FrontendInventory.php:1617–1628) |
parametrisierter Lazy-Call $this->activeCalibrationGrid([...]) = 1 Query pro Zeile (per MTAG memoisiert, Spalten teilen sie sich) |
| last_certificate | GridColumn.php:7596 → getLastCertificate (FrontendInventory.php:2371–2379) |
lastCalibration(['with'=>['certificate1','certificate2']]) pro Zeile, unmemoisiert |
| last_location_customer / _delivery / L2801/L2802 | GridColumn.php:7431–7458 |
lastUser/lastDeliveryUser/lastRental lazy = 1 Query pro Zeile (per MTAG memoisiert) |
| category_name-Spalte | GridColumn.php:7505 |
Kategorie-Lookup pro Zeile |
| Kalibrier-Grid: Dokument-Spalten | GridColumn.php:1208–1255 → Model.php:327–341 |
findByPk mit DMS-Relationen pro Zeile |
Entschärft (Memoisierung pro Fremdschlüssel, nicht pro Zeile): status (pro Status-Code),
type (pro Typ-ID), customer (Bulk getMyCustomers).
Wichtig: Die Anzeige-Relationen sind im DataProvider nicht eager geladen — $criteria->with
wird nur für Filter/Sort-Relationen gesetzt (Z. 3793–3808). Und selbst Eager-Loading würde die
parametrisierten Relationsaufrufe ($this->activeCalibrationGrid(['select'=>…])) nicht
bedienen — die erzwingen immer eine frische Query.
4.3 Cache-Selbstzerstörung bei jedem Grid-AJAX¶
KTbExtendedGridView::beginCache()/endCache() (Z. 70–108): bei jedem Request mit
ajax-Parameter wird CacheHelper::flushAll(['Columns:*']) aufgerufen — zweimal pro
Request. CacheHelper::flushAll (CacheHelper.php:122–162) macht $cache->flush() auf den
gesamten Anwendungs-Cache (nur Columns:* + 3 Except-Keys werden gerettet). Damit werden
bei jeder Filter-Eingabe u. a. gelöscht:
- der DB-Schema-Cache (
schemaCachingDuration = 3600,config/components.php:9) → der nächste Request re-introspektiert Tabellen-Schemata (SHOW FULL COLUMNSje AR-Tabelle), - der Query-Cache (u. a.
KCActiveDataProvidercache_duration = 3600), - alle
COutputCache-Fragmente undCacheHelper::remember-Einträge (z. B.user_model).
Auf OpenShift läuft der Cache zudem als CFileCache auf dem NFS-PVC (s. 5) — flush()
iteriert dort ein Verzeichnis über NFS. Der Effekt: das System verhält sich, als gäbe es
keinen Cache, bezahlt aber zusätzlich die Flush-Kosten. (Das erklärt die Beobachtung
„Caching bringt nichts".)
4.4 Sonstiger Per-Request-Overhead¶
| Posten | Ort | Kosten/Request |
|---|---|---|
| Header-Zähler fällig/überfällig | themes/calhelp/views/layouts/main.php:170–171 → getTotalNotification (FrontendInventory.php:5486–5518) + fn_due (Z. 4855–4907) |
2 COUNT-Queries inventory JOIN calibration (C2339=1) JOIN customers mit Filter auf unindiziertem C2303 — auf jeder Vollseite |
SchemaCheck (Pre-Bootstrap) |
httpdocs/index.php → SchemaCheck.php:45–95 |
eigene PDO-Verbindung + 5–6 INFORMATION_SCHEMA-Queries, ungecacht, jeder Request |
| DB-Log-Route | config/components.php:153–159 (KCDbLogRoute, autoCreateLogTable=true, Levels error, warning, info) |
zusätzliche dbLog-Verbindung + Tabellen-Probe je Request; INSERT je Log-Eintrag (inkl. info) |
| DB-Verbindungen | local.php:34–64 — keine persistenten Verbindungen |
bis zu 3 Connect-Handshakes/Request (db, dbLog, SchemaCheck-PDO) + SET NAMES |
| Konfig-Queries Grid | getGridViewShownColumns/getGridViewVisibleColumns/getStandardView/property()->get |
~5–15 Queries; getStandardView läuft doppelt (in search() Z. 2926 und in _columns) |
| Editierbare Zellen | KTbExtendedGridView::renderTableRow Z. 521–542 |
pro Zelle $data->checkAccess('edit') (für inventory früh true, FrontendInventory.php:1356–1358) + volles KTbEditableField-Widget pro Zelle (KTbEditableColumn.php:59–155) — CPU, kein DB |
pageSize all |
KCActiveDataProvider.php:83–85 (PHP_INT_MAX) |
rendert den kompletten Bestand serverseitig (50k Zeilen × N+1!) — OOM-/Timeout-Risiko |
5. Infrastruktur-Faktoren OpenShift vs. VDI/Docker¶
Aus local.php, config/components.php, configs/php.ini, k8s/:
| Thema | Befund | Wirkung auf OpenShift |
|---|---|---|
| Cache-Backend | local.php:80–109: KCMemCache nur wenn MEMCACHE_HOST gesetzt + Extension da; sonst CFileCache. k8s/values-oc.yml:51–52: memcached.enabled: false |
Schema-/Query-/App-Cache = Dateien in protected/runtime/cache/ auf dem PVC/NFS (statefulset.yaml:254–256) → jeder Cache-Hit ist NFS-I/O; flushAll (4.3) iteriert NFS-Verzeichnisse |
| PVC-Mounts | statefulset.yaml:236–286: runtime, assets, filemanager, uploads, Logs per subPath auf einem RWO-PVC |
Log-Schreiben, Asset-stat()s, Cache-I/O über NFS; PHP-Quellcode bleibt im Image (Overlay, schnell) |
| OPcache | configs/php.ini:1765–1891 — komplett auskommentiert, kein Tuning (validate_timestamps, memory_consumption, max_accelerated_files) |
ggf. ungetunter Distro-Default; große Codebase (Yii + vendor) |
| realpath-Cache | php.ini:341,347 auskommentiert → 16 KB Default |
viele stat()-Syscalls pro Request |
| DB-Verbindung | CDbConnection (kein class-Override in local.php → KCDbConnection/KCMysqlSchema sind inaktiv), kein PDO::ATTR_PERSISTENT |
Verbindungsaufbau pro Request × 2–3 |
| Sessions | CHttpSession files, /var/lib/php/sessions (Container-FS, nicht PVC), GC aus (php.ini:1413) |
ok; aber PHP-File-Sessions serialisieren parallele AJAX-Requests desselben Users (Session-Lock) — mehrere Grids/Notification-Calls laden nacheinander |
| Query-Cache-Konfig | components.php:10–11: queryCachingCount = 1, queryCachingDuration = 5000 |
global wird nur die erste Query je Request gecacht — praktisch wirkungslos |
| Ressourcen | values-oc.yml:36–40: requests 500Mi/300m, keine Limits |
CPU-Throttling möglich; PHP-Rendering (Editable-Widgets, AR-Hydration) reagiert direkt darauf |
Abgrenzung: Auf der VDI (Docker, DB im selben Compose-Netz auf lokaler Hardware) sind RTT ≈ 0,05 ms und File-I/O lokal — dieselbe Query-/IO-Anzahl kostet dort fast nichts. Der Unterschied ist also erklärbar, ohne dass OpenShift „defekt" ist.
6. Maßnahmenkatalog (ohne Caching), priorisiert¶
P0 — Quick Wins (Tage, geringes Risiko)¶
tt()Version-2-Modus bulk-laden (AdminMessage.php:177–207): beim ersten Miss einer Kategorie die ganze Kategorie laden (wie Version 1, Z. 149–176), statt pro String zu queryen;BINARY-Vergleich entfernen (Kollationutf8_unicode_cireicht praktisch; sonst Spalte binär kollationieren statt casten).addNewMessage()-INSERTs (Z. 126, 278) hinter einen expliziten „Capture"-Schalter legen. Erwartung: −200…−500 Queries pro Seite, wirkt auf alle Masken.CacheHelper::flushAllaus dem Grid-Widget entfernen (KTbExtendedGridView.php:72–77, 102–107): gezielt nur die betroffenenColumns:*-/Fragment-Keys invalidieren (flushExcept-Logik umkehren bzw.deleteder konkreten Keys). Schema-/Query-Cache bleiben dann überhaupt erst wirksam — auch mit CFileCache.- N+1 Zeilen-CSS:
getClassNameForRow(FrontendInventory.php:2010) — Kategorie-Farben einmal pro Request als Map (item_uID → color) für die Seiten-MTAGs laden (eineIN (...)-Query) oder ganz überspringen, wennshow_category_coloraus ist (_admin_v2.php:102–105kennt das Flag bereits). −1 Query pro Zeile. - Header-Zähler entkoppeln (
main.php:170–171): die beidengetTotalNotification-COUNTs per AJAX nachladen (Endpoints existieren:frontendInventory/getNotification) oder nur auf der Dashboard-Seite berechnen. −2 schwere COUNTs auf jeder Vollseite. - Indizes: (a) auf Kundensystemen prüfen, ob
m260117_074317+m260528_000001angewendet sind; (b) neue Yii-Migration:calibration (C2303)— oder besser(MTAG, C2339, C2303)— undinventory (I4201). Macht duedate-Filter/Sort und Hierarchie-/Barcode-Joins indexfähig. Muss als Yii-Migration kommen (V1-Kundensysteme!). SchemaCheckdrosseln (index.php→SchemaCheck.php): Ergebnis in lokaler Datei (z. B.runtime/schema-check.ok, Container-FS) merken und nur bei Fehlschlag/Alter erneut prüfen. −1 Verbindung, −6 Queries pro Request.KCDbLogRoutezähmen (components.php:153–159):autoCreateLogTable => false(Tabelle existiert), Level auferror, warningreduzieren. −1 Verbindung/Probe-Query, weniger INSERTs.
P1 — Query-Form (der strukturelle Fix, 1–2 Wochen)¶
- Letzte/aktive Kalibrierung denormalisieren: Spalten auf
inventory(z. B.last_cal_date,next_due_date,last_order_number,active_CTAG), gepflegt in den Save-/Delete-Pfaden vonFrontendCalibration(dort existiert bereits dieC2339-Aktiv-Logik) + einmaliger Backfill-Command. Danach: - Grid-Filter/Sort
duedate/lastcalauf lokale, indizierte Spalten (search()Z. 3259–3346, 3682–3713 vereinfachen; JoinactiveCalibrationGridentfällt), - Header-Zähler (4.4) und Dashboard-Widgets werden zu Index-Scans auf
inventory, - Anzeige-N+1 (4.2, duedate/lastcal-Spalten) entfällt komplett.
⚠️ Schema-Änderung an einer V1-Tabelle → Yii-Migration Pflicht; V2/Laravel-Migration
nur für V2-Installationen nachziehen. Kein
thermo_-Präfix. - Übergangsvariante ohne Schema-Änderung (falls 8 zu groß): Join-Bedingung auf
C2339 = 1verengen (Altdaten-Backfill:UPDATE calibration SET C2339 = 0 WHERE C2339 IS NULLfür Nicht-Aktive),GROUP BYnur setzen, wenn tatsächlich eine HAS_MANY-Relation gejoint ist (shouldDeduplicateInventoryGriddatengetrieben statt Default-an),mapusersalsEXISTS (SELECT 1 FROM map_user ...)statt Join (Z. 3076–3081). - Anzeige-Relationen batchen statt lazy: nach
fetchData()für die Seiten-MTAGs eine Query je Relation (calibration,location/lastUser,dms-Zertifikate) und die statischen Memo-Arrays (self::$calibrationList,self::$locationCustomerList, …) vorbefüllen — dievalue-Expressions greifen dann ohne Einzelqueries. −1…4 Queries pro Zeile bei unveränderten Spalten-Definitionen. - COUNT entkoppeln: bei unveränderten Filtern (Hash der Filter-GET-Parameter) den
letzten
totalItemCountaus dem Request mitgeben (Hidden-Field<gridId>_item_totalexistiert schon) statt bei jedem Seitenwechsel neu zu zählen.
P2 — Rendering (parallel machbar)¶
- Editable-ACL pro Grid statt pro Zelle (
KTbExtendedGridView.php:521–542): Ergebnis von$data->checkAccess('edit')pro Request memoisieren (für Tabellen ≠inventoryreal). pageSize=allkappen (KCActiveDataProvider.php:83–85): Obergrenze (z. B. 1.000) stattPHP_INT_MAX; Export-Wege existieren separat.- Optional:
debug_backtrace()ausWebUser::checkAccess(Z. 202–208) entfernen — wird hunderte Male pro Request aufgerufen.
P3 — Betrieb/OpenShift (kein Code, große Wirkung)¶
runtime/cachevom NFS-PVC aufemptyDir/Container-FS legen (Cache-Dateien müssen nicht persistent sein) — macht CFileCache brauchbar, solange kein Memcache läuft.- Memcached im OC-Values aktivieren (
values-oc.yml:51) — streng genommen Caching, aber nur Wiederherstellung des vorgesehenen Betriebszustands. - OPcache explizit konfigurieren (
opcache.enable=1,memory_consumption≥256,max_accelerated_files≥50000,validate_timestamps=0bei Image-Deploys) +realpath_cache_size=4096K,realpath_cache_ttl=600(configs/php.ini). - DB-Nähe prüfen (gleicher Node/AZ, kein DNS pro Connect), ggf. persistente Verbindungen
(
PDO::ATTR_PERSISTENT) testen;queryCachingCount=1(components.php:10) entfernen oder sinnvoll setzen.
7. Mess- und Verifikationsplan¶
Vorhandene Instrumente (alle ohne Code-Änderung nutzbar):
INVENTORY_SEARCH_PROFILE=1(+ optionalINVENTORY_SEARCH_PROFILE_THRESHOLD=<sek>, Default 5.0): loggt Count-/Search-Dauer inkl. Kontext (Joins, Dedup, Total) als WARNING, Kategorieinventory.search(FrontendInventory.php:2817–2882).ENABLE_INVENTORY_GRID_DEDUP=0: A/B-Test GROUP-BY-Kosten (Z. 2798–2810).?_grid_version=v1|v2(nur YII_DEBUG): View-Vergleich (Controller Z. 755–758) — beide Views teilensearch()/Widget, unterscheiden sich also kaum in der Datenpfad-Performance.- MySQL-Seite:
SET GLOBAL slow_query_log,performance_schema.events_statements_summary_by_digest(Query-Anzahl pro Request-Typ!),EXPLAINder 3.1-Query mit/ohneC2303-Index. - Vorher/Nachher-Kennzahlen je Maßnahme: (a) Queries pro Request (general log zählen),
(b) TTFB
actionAdminohne AJAX, (c) TTFB Grid-AJAX mit Filter, (d) TTFBactionView.
Erwartungswerte für den Referenzbestand (50k/2M) nach P0+P1: Grid-AJAX von mehreren Sekunden auf < 500 ms (Queries: von 300–600 auf < 30), Detailmaske ohne 512M-Memory-Workaround.
8. Schlüsselstellen-Index (für spätere Sessions)¶
| Thema | Datei:Zeile |
|---|---|
| Grid-Einstieg Inventar | FrontendInventoryController.php:531 (actionAdmin), 754–765 (v1/v2-Switch) |
| search() Inventar | FrontendInventory.php:2912 (Criteria), 3810 (Count), 3848 (Provider) |
| activeCalibrationGrid-Relation | FrontendInventory.php:342–347 |
| Dedup-GROUP-BY | FrontendInventory.php:2798,2989–2994 |
| Sichtbarkeits-Join mapusers | FrontendInventory.php:3076–3081 |
| N+1 Zeilen-CSS | FrontendInventory.php:1991–2018 (Query: 2010) |
| N+1 Kalibrier-Anzeige | FrontendInventory.php:1617–1628, GridColumn.php:7402–7422 |
| N+1 last_certificate | FrontendInventory.php:2371–2379, GridColumn.php:7596 |
| Header-Zähler | themes/calhelp/views/layouts/main.php:170–171, FrontendInventory.php:5486, fn_due 4855 |
| tt()/Übersetzung | helperFunctions.php:822, AdminMessage.php:66–326 (V2-Einzelqueries: 177–207) |
| flushAll-Bug | KTbExtendedGridView.php:70–108, CacheHelper.php:122–162 |
| Fragment-Cache | KTbExtendedGridView.php:115–138, Controller.php:18,693 |
| Provider/pageSize | KCActiveDataProvider.php:81–121 ('all'→PHP_INT_MAX: 83–85) |
| Spalten-Fabrik | GridColumn.php:382 (Inventar-Block: 6987–7622) |
| Feld-Konfig-Cache (nur per Request) | InventoryHelper.php:445–545 (getFieldsFaster), 557–731 |
| Editable pro Zelle | KTbExtendedGridView.php:521–542, KTbEditableColumn.php:59–155 |
| Detail-Ansicht | FrontendInventoryController.php:843 (actionView, Memory-Workaround 854), view.php (~11 Grids) |
| Kalibrier-Grid Detail | FrontendCalibration.php:3285–3804 (leerer Provider bei Nicht-AJAX: 3287) |
| Index-Migrationen | m260117_074317_add_missing_indexes_v4.php, m260528_000001_add_mtag_index_to_calibration.php, m210407_024347_add_missing_indexes_v3.php |
| Cache-/DB-Konfig | config/components.php:8–11,129,145–189, config/local.php:34–109 |
| OpenShift-Deployment | k8s/values-oc.yml:36–52, k8s/charts/calserver/templates/statefulset.yaml:236–286, configs/php.ini:341,1322,1765–1891 |
| Profiling-Schalter | FrontendInventory.php:2817–2882 (ENV INVENTORY_SEARCH_PROFILE), DevProfiler (nur YII_DEBUG) |