major
#29876
KnowledgeBase on H2: deleting objects scans the association tables (the reference indexes start with BRANCH, and H2 seeks an IN list only on the first index column)
Problem
Deleting a few hundred objects in one transaction takes 15–30 s on H2, even when the deleted objects have only a moderate number of referrers. The time does not depend on what is deleted but on how large the tables referencing the deleted type are. In the E-Home application (TopLogic 8.0.0-alpha10, H2), a test that deletes all data before importing a sample deletes about 400 root objects per run (projects, manufacturers with their catalogs, datapoint types, mask versions; the rest follows by composition):
| = Variant = | = roots = | = delete = | = commit = |
| tDelete() per object | 402 / 385 / 339 | 20.8 s / 15.0 s / 14.9 s | 6.4 s / 2.1 s / 2.1 s |
| one KnowledgeBase.deleteAll(items) | 402 / 385 / 339 | 21.1 s / 7.5 s / 8.8 s | 9.2 s / 2.3 s / 2.1 s |
Analysis
A Java Flight Recorder profile of the whole test suite (169 s) shows 45 % of all execution samples under
TLObject.tDelete
DBKnowledgeItem.delete
DefaultDBContext.removeObject
ReferenceDeletionHelper.collectChangesForDeleted
collectDeletions / processNewlyDeleted
ReferenceDeletionHelper.deleteReferenceSources
CompiledQuery (from DBKnowledgeBase.anyRefereesQuery(type, DeletionPolicy.DELETE_REFERER, TRUE, CURRENT_TARGETS, ...))
MonomorphicSearch.fetchData
org.h2.command.query.Select.isConditionMet
org.h2.expression.condition.ConditionInConstantSet.getValue
For every chunk of deleted items (getMaxSetSize) and every type that has a DELETE_REFERER reference to the deleted type, one query with ref IN (...) is run. H2 does not use an index for the IN list in the generated statement (the IN condition is combined with the branch/revision conditions inside AND/OR), so it reads every row of the referrer table (e.g. the composition link tables, which hold all links of all objects) and checks the IN set row by row. With the cascade (each newly deleted level runs the queries again for the next level) the referrer tables are scanned many times per transaction.
Expected
The referrer lookup during deletion uses an index on the reference columns (destination type/id plus branch and revision), so its cost grows with the number of referrers, not with the size of the table. Possibly the statement can be shaped so that H2 (and the other DBs) pick the index for the IN list, or the referrer columns need an index for this access path.
Reproduction
Any application with a composition-heavy model on H2: import a few thousand objects, then delete a few hundred containers in one transaction and profile. Profile and timings available from the reporter (JFR file from the E-Home test suite, test.de.haumacher.ehome.roundtrip.TestRoundTrip.clearData).
Lösung
Ursache (siehe Kommentare): H2 nutzt eine IN-Liste nur auf der ersten Spalte eines Index. In allen Indexen der Wissensbasis steht BRANCH vorne, die Spalte mit der IN-Liste (Identifier bzw. Referenz-ID) an zweiter Stelle. Das betrifft nicht nur die Referer-Suche beim Löschen, sondern jede IN-Liste über IDs: Bulk-Laden von Objekten, Navigation über Assoziationen (AssociationQueryImpl), Laden dynamischer Attribute (FLEX_DATA).
Das Layout mit BRANCH vorne bleibt für alle anderen Datenbanken erhalten: für Datenbanken mit Präfix-Kompression (Oracle) ist es das günstigste, und andere DBMS nutzen ein IN auch auf einer späteren Indexspalte.
Option `branch-last`
- Option branch-last am Typsystem (SchemaConfiguration, neben multiple-branches). Ohne Angabe bestimmt der SQL-Dialekt der Datenbank den Wert: DBHelper.seeksInListOnFirstIndexColumnOnly() ist true für H2 (H2Helper), sonst false. Ein gesetzter Wert überschreibt den Dialekt. Die Auflösung liegt an einer Stelle (SchemaSetup.isBranchLast(schema, dialect)).
- Das gespeicherte Schema (TL_PROPERTIES) enthält immer den aufgelösten Wert; ein gespeichertes Schema ohne das Attribut (alle bestehenden Datenbanken) gilt als false.
- Mit branch-last steht BRANCH direkt vor REV_MAX statt am Anfang (REV_MAX bleibt hinten, es wird nur als Bereich abgefragt); eine Hilfsklasse IndexColumnOrder ordnet die Spalten an allen Stellen gleich an:
- Primärschlüssel der Objekttabellen: IDENTIFIER, BRANCH, REV_MAX
- Referenz-Indexe (MOReferenceIndex): <REF>_ID, BRANCH, REV_MAX (bzw. <REF>_BRC bei Referenzen mit eigener Branch-Spalte)
- deklarierte, generierte Indexe (DefaultIndexColumnsStrategy): <Spalten>, BRANCH, REV_MAX; Indexe mit expliziter Spaltenliste (custom) behalten ihre Reihenfolge
- FLEX_DATA: IDENTIFIER, ATTR, BRANCH, REV_MAX
- Messung (TestBranchLastH2IndexUsage, vom KB erzeugtes SQL, EXPLAIN ANALYZE): Referer-Suche für 20 von 300 Objekten liest 301 Zeilen mit BRANCH vorne, 21 Zeilen mit branch-last; Bulk-Laden über IDENTIFIER IN (...) ebenso 301 → 21.
Schlüssel von FLEX_DATA
Unabhängig von der Option entfällt TYPE aus dem Primärschlüssel von FLEX_DATA (IDs sind global eindeutig; die Spalte bleibt in der Tabelle für Auswertungen), und ATTR folgt direkt auf IDENTIFIER: Standard-Layout BRANCH, IDENTIFIER, ATTR, REV_MAX. Die Journal-Tabellen behalten ihren Schlüssel. Eine bestehende FLEX_DATA-Tabelle behält ihren Schlüssel, bis eine Layout-Umstellung (s. u.) die Schlüssel neu aufbaut.
Single-Branch nur über das Flag
multiple-branches="false" allein bestimmt den Einzel-Branch-Betrieb: Beim Erzeugen der Typen aus der Schema-Konfiguration passt MOFactory.adaptAttribute (DefaultMOFactory) das Attribut _branch an das Flag an (ohne Spalte mit TrunkStorage bzw. Spalte BRANCH mit BranchStorage); mit Branch-Spalte gehört _branch zum Primärschlüssel (MORepositoryBuilder). Das gilt für alle Repositories, auch für gespeicherte Schemas bestehender Datenbanken. singleBranchMeta.xml ist leer und bleibt nur für Konfigurationen erhalten, die es noch deklarieren; top-logic.singleBranch.config.xml setzt nur noch das Flag.
Automatische Umstellung bestehender Datenbanken
Weichen branch-last oder multiple-branches der (aufgelösten) Konfiguration vom gespeicherten Schema ab, stellt AlignTableLayoutProcessor die Tabellen beim Start automatisch um (MigrationService.executeAutomaticSchemaMigrations, nach evtl. anstehenden Migrationsskripten):
- Tabellendefinitionen werden aus dem gespeicherten Schema und aus dem gespeicherten Schema mit den konfigurierten Layout-Optionen erzeugt und tabellenweise verglichen; nur Branch-Spalten, Primärschlüssel und Indexe werden angeglichen (DBSchemaUtils.alignTable, neue DBHelper-Methoden dropPrimaryKey, addPrimaryKey, renameTable, canChangePrimaryKey). Auf Oracle werden Tabellen mit Primärschlüssel-Speicherung (IOT) über eine Kopie neu aufgebaut.
- single → multiple: BRANCH und die <REF>_BRC-Spalten werden angelegt und mit dem Trunk gefüllt (_BRC nur, wo die Referenz gesetzt ist).
- multiple → single: die Branch-Spalten werden entfernt. Voraussetzung: es existiert nur der Trunk und keine Zeile liegt in einem anderen Branch; sonst bricht der Start mit einer Fehlermeldung ab (Tabellen, Spalten, Branch-IDs), ohne die Datenbank zu ändern.
- Danach wird das gespeicherte Schema mit den neuen Optionen gespeichert; andere Unterschiede zur Konfiguration bleiben den übrigen Mechanismen überlassen.
- Auf Oracle/MySQL ist DDL nicht transaktional: vor der Umstellung sichern.
Archetype
Der Archetype tl-archetype-app fragt mit der Eigenschaft useBranches nach, ob die neue Anwendung mehrere Branches braucht. Standard ist false: die generierte WEB-INF/conf/metaConf.txt enthält top-logic.singleBranch.config.xml.
Dokumentation: persistency/table-layout.md (neu), persistency/branches.md, persistency/database-notes.md, persistency/type-systems.md.
Migration
Keine Pflichtschritte.
- Anwendungen auf H2: die bestehende Datenbank wird beim nächsten Start automatisch auf branch-last umgestellt (Dialekt-Standard). Wer das alte Layout behalten will, setzt branch-last="false" am Typsystem Default.
- Eine Anwendung ohne eigene Branches kann top-logic.singleBranch.config.xml in ihre metaConf.txt aufnehmen; die Branch-Spalten werden beim nächsten Start entfernt (nur möglich, wenn die Datenbank nur den Trunk enthält).
- Eine eigene Deklaration von singleBranchMeta.xml in der Typsystem-Konfiguration ist nicht mehr nötig und kann entfernt werden.