Skalierung: Was, wenn die Datenbanken anwachsen?

Am Morgen des 2026-10-21 war das Dashboard des CMS nicht mehr erreichbar, und kurz zuvor war die Startkarte mit einem Timeout abgestürzt. Beides hatte dieselbe Ursache: Mit den 120 neuen Werken aus Frankreich und Italien war die Datenbank gewachsen, und einige Berechnungen wurden mit jedem Eintrag teurer. Ich wollte deshalb wissen, was passiert, wenn die Datenbanken auf mehrere Tausend Einträge pro Art anwachsen, und solide Lösungen bauen, bevor es so weit ist.

Das Experiment

Statt zu raten, habe ich gemessen. Dafür wurde eine Testkopie der Datenbank mit rund zehnfacher Datenmenge erstellt (3500 Kunstwerke, 4200 Personen, 3200 Orte, 17'000 Provenienzschritte, 66'000 Quellenangaben) und jeder aufwendige Vorgang darauf gemessen.

Vorgang Damals Bei zehnfacher Menge
Qualitätsprüfung mit Duplikatprüfung 7 s über 2 min (Abbruch)
Mögliche Duplikat-Paare 6000 222'000 (27 s nur, um sie zu finden)
Übrige Qualitätsprüfungen ≈ 1 s je 9 s und 7 s für die zwei langsamsten
Ganze Liste der Kunstwerke, wie die Website sie lädt 1 s 15 s, 2,6 MB
Alle Listen zusammen ≈ 2 s ≈ 27 s, ≈ 10 MB
Startkarte mit allem ≈ 2 s Abbruch (über 5 s)
Wer war wann wo, in einem Zeitfenster < 1 s 4,4 s
Netzwerk < 1 s 4 s
Crossed Paths < 1 s 4,6 s
Seite eines Kunstwerks 0,2 s 1,9 s
Suchen, Orte, Museen, einzelne Karten schnell weiterhin schnell (deutlich unter 1 s)

Der Ausfall vom Morgen war also kein Einzelfall. Bei einigen Tausend Einträgen wären das Dashboard des CMS, die Startkarte und die Listen der Website ausgefallen. Dafür gab es drei Ursachen:

  1. Alles wurde von Grund auf neu berechnet. Nach jeder Änderung liefen alle Qualitätsprüfungen über alle Einträge. Manche Ansichten, etwa die aus der Provenienz abgeleiteten Verbindungen zwischen Einträgen, wurden bei jeder Anfrage neu berechnet. Der Aufwand wuchs also mit der ganzen Datenbank und nicht mit der Grösse der Änderung.
  2. Die Website lud jede Liste vollständig. Auswahlfelder, Listen und die Suche im Kopf der Seite filtern ganze Listen im Browser. Bei 300 Einträgen ist das gut, bei 3000 nicht mehr.
  3. Eine Seite fiel auf die teure Berechnung zurück. Waren die gespeicherten Resultate nicht aktuell, berechnete das Dashboard sie selbst, und genau das hatte es zum Absturz gebracht.

Die Umsetzung in sechs Schritten

Die Lösung habe ich zusammen mit Claude geplant und vom 2026-10-21 an in sechs Schritten umgesetzt, jeder als eigene Veröffentlichung mit Tests und Eintrag im Changelog des Servers.

  1. Nur neu berechnen, was eine Änderung betrifft. Trigger in der Datenbank schreiben jeden betroffenen Eintrag in eine Warteschlange, inklusive der abhängigen Einträge: Die Umbenennung einer Künstlerin aktualisiert auch ihre Werke, die Umbenennung eines Landes alles in seinen Orten ("Made in: Arles, France"). Ein Hintergrundprozess berechnet dann nur diese Einträge in kleinen Portionen neu: ihre Suchfelder, Qualitätsmeldungen und Duplikate. Für die Duplikatprüfung erhält jeder Eintrag einen Fingerabdruck seiner wichtigsten Angaben. Ein Eintrag wird nur dann neu verglichen, wenn sich sein Fingerabdruck ändert, und dann nur mit Einträgen, die ihm ähnlich sind. Die Seiten des CMS berechnen die Prüfungen nie mehr selbst, sondern zeigen "Updating — n entries waiting", wenn der Hintergrundprozess noch arbeitet. Jede Nacht wird zudem alles einmal von Grund auf geprüft. Was diese Prüfung korrigieren muss, haben die Prüfungen pro Änderung verpasst, und es erscheint auf der Seite "Quality", damit ich die Ursache beheben kann. Eine einzelne Änderung kostet bei zehnfacher Menge nun 1–4 Sekunden statt über zwei Minuten.
  2. Die Verbindungen zwischen Einträgen pro Änderung. Das Netz der Verbindungen, das unter anderem die Netzwerkfragen verwenden, wird nur noch für die betroffenen Einträge neu berechnet, und die API liest es, statt es bei jeder Anfrage zu berechnen. Eine Änderung erscheint auf der Website dadurch etwa eine Sekunde später, im CMS aber sofort.
  3. Gespeicherte Zeilen für die Listen. Jede Zeile der Listen wird einmal berechnet, wenn sich ihr Eintrag ändert. Ursprünglich hatte ich geplant, dass der Server sucht und die Listen seitenweise schickt. Die Messung zeigte aber, dass das Berechnen der Zeilen langsam war und nicht das Herunterladen: Komprimiert sind alle 3500 Kunstwerke bei zehnfacher Menge rund 1 MB gross. So bleiben das sofortige Filtern und Sortieren im Browser erhalten. Seitenweise Listen wären erst bei Zehntausenden Einträgen pro Art nötig.
  4. Zwischenspeicher und Leitplanken. Die Daten der Startkarte werden einmal pro Änderung berechnet statt bei jedem Besuch. Dazu kommen 22 Zeitbudgets, die auf der Testkopie geprüft werden, damit eine Verlangsamung auffällt, bevor sie die Website erreicht. Postgres protokolliert jede Abfrage über einer Sekunde, und ein Script fasst dieses Protokoll als Bericht zusammen. Der Bericht der ersten Woche zeigte genau, was die Schritte 1–3 ersetzt hatten: Die alte Prüfung über alle Einträge dauerte 1089 Mal länger als eine Sekunde, bis zu 29 Sekunden.
  5. Ein fehlerhafter Eintrag blockiert nichts mehr. Bisher wurde eine Portion, die wegen eines einzelnen Eintrags fehlschlug, immer wieder als erste versucht, und alles dahinter blieb stehen. Nun wird die Portion Eintrag für Eintrag wiederholt, der fehlerhafte Eintrag mit seiner Fehlermeldung beiseitegelegt und der Rest verarbeitet. Das Dashboard zeigt einen roten Hinweis, wenn Einträge beiseitegelegt wurden. Zudem wird die Zeitleiste der Stilepochen zwischengespeichert (2,1 s → 4 ms bei zehnfacher Menge).
  6. Das CMS bei zehnfacher Menge. Die Suche in den Listen des CMS ging für jeden Eintrag alle Suchfelder seines Typs durch, der Aufwand wuchs also mit dem Quadrat der Anzahl Einträge. Sie macht nun einen einzigen Durchgang. Die Abdeckung der Provenienz wird pro Werk gespeichert, und die Seite "Quality" zeigt höchstens 100 Meldungen pro Prüfung. Die Zeitbudgets umfassen nun auch 13 Seiten des CMS, insgesamt 35.

Das Ergebnis bei zehnfacher Datenmenge:

Vorgang Vorher Nachher
Qualitätsprüfung und Duplikate über 2 min, Abbruch eine Änderung in 1–4 s
Seite eines Kunstwerks 1,9 s 0,2 s
Netzwerk 4,1 s 0,03 s
Crossed Paths Abbruch 0,6 s
Alle Kunstwerke (Listen, Auswahlfelder, Suche) 15,4 s 0,8 s
Startkarte mit allem Abbruch 0,03 s (zwischengespeichert)
Suche in einer Liste des CMS 12,5 s 0,3 s
Provenienzlücken 1933–1945 im CMS 8,5 s 2,1 s

Bei den Tests wurden drei verpasste Abhängigkeiten gefunden und vor der Veröffentlichung behoben: ein umbenanntes Land, ein Glossarbegriff, der über seinen Namen im Medium eines Werks gefunden wird, und der Name einer Künstlerin in den Zeilen der Museen. Ein Fehler ist mir beim Messen passiert: Beim Zurücksetzen der Abfragestatistik auf der Testkopie wurde sie ohne Angabe der Datenbank für alle Datenbanken gelöscht, auch für die laufende. Andere Daten waren nicht betroffen, und die Statistik füllt sich seither wieder. Am selben Tag zeigte das Fehlerprotokoll, dass sich sehr lange Ansichten der Startkarte nicht mehr teilen liessen. Die Datenbank prüfte die Eindeutigkeit über den ganzen Text der Ansicht, ein Indexeintrag darf aber höchstens etwa 2,7 KB gross sein. Nun wird die Eindeutigkeit über einen Hash des Textes geprüft, und alle 118 bereits vergebenen kurzen Links blieben gültig.