K3 hat gezeigt, was geschieht, wenn alle Termine eines Büros in einem Dokument liegen: Es wächst bis zur Grenze von 2 MB und ist dann tot. Die Reparatur war ein Element je Termin. Diese Lektion macht aus der Reparatur ein Verfahren, das Sie auf jede Beziehung der Kladde.Verlauf anwenden: erst die Zugriffsmuster, dann die Form.

Vorwissen. Die Rechnung zum Bürodokument und die Grenzen stehen in „K3, Lektion 3.6.4" und werden nicht wiederholt. Wie der Partitionsschlüssel die Antwort mitbestimmt, folgt in Lektion 1.4.

Was ausgeführt wurde. Das Rechenprogramm der Lektion wurde mit .NET 10 übersetzt und lief ohne Konto (es braucht nur System.Text.Json). Die Abfragen in SQL sind nicht gegen Azure ausgeführt. Zahlen sind Annahmen der Aufgabe, die Größen in den Ausgaben sind gemessen am Beispieltext. Die Ausgabe zeigt deutsche Zahlenformate (Punkt als Tausendertrenner); mit englischer Kultur stehen Kommas.

Zugriffsmuster zuerst

Ein relationales Modell fragt: Welche Dinge gibt es, wie hängen sie zusammen? Ein Dokumentmodell fragt zuerst: Was liest und schreibt die Anwendung, wie oft, mit welchem Schlüssel? Für die Kladde.Verlauf (F-03):

Nr. Zugriff Art Schlüsselwerte bekannt
Z1 Kalender eines Büros für einen Monat lesen, oft Büro, Monat
Z2 einen Termin öffnen lesen, oft Büro, Monat, id
Z3 Aktivitäten eines Büros an einem Tag lesen Büro, Tag
Z4 Verlauf eines Support-Gesprächs lesen, oft Büro, Person, Gespräch
Z5 letzte Nachrichten eines Gesprächs anzeigen lesen, sehr oft Büro, Person, Gespräch
Z6 neue Nachricht anhängen schreiben, sehr oft Büro, Person, Gespräch

Z1 bis Z6 kennen den Schlüssel; deshalb sind F-03 die Schlüssel. Ein Zugriff ohne Schlüsselwerte (zum Beispiel „alle Termine einer Kontaktperson in allen Büros") wäre eine Abfrage über alle Partitionen und gehört in eine Auswertung, nicht in den Alltag (Lektion 1.4).

Einbetten oder verweisen

Learn nennt die Regeln so, und sie decken sich mit den Zugriffsmustern:

  • Einbetten, wenn die Beziehung enthaltend ist, eins zu wenige, die Daten sich selten ändern, nicht unbegrenzt wachsen und meist zusammen gelesen werden. Ergebnis: ein Lesevorgang, ein Schreibvorgang.
  • Verweisen bei eins zu viele oder viele zu viele, bei häufig geänderten oder unbegrenzten Daten. Ergebnis: schnellere Schreibvorgänge, aber mehr Lesevorgänge, weil Cosmos DB nichts über Container hinweg verknüpft (JOIN wirkt nur innerhalb eines Elements).

Wo der Verweis liegt, entscheidet das Wachstum: Beim Verleger mit unbegrenzt vielen Büchern trägt jedes Buch die Verleger-Kennung, nicht der Verleger eine Liste. Für die Kladde heißt das: Ein Termin trägt officeId, das Büro trägt keine Terminliste (das ist der Werkstattbericht aus K3).

Cosmos DB kennt keine Fremdschlüssel. Ob eine contactId auf etwas zeigt, prüft die Anwendung (oder ein Trigger); der Verweis ist „schwach". Das ist einer der Gründe, warum Regeln der Kladde in SQL bleiben.

Kopieren und die Pflege der Kopie

Dazwischen liegt das Kopieren: Man hält einen Teil der Daten eines anderen Elements mit, damit eine Liste ohne zweiten Zugriff auskommt. Learns Beispiel: Das Buch trägt Name und Bild der Autoren, nicht nur deren id. Der Preis ist die Pflege: Ändert sich der Name, müssen alle Kopien aktualisiert werden, sonst weichen sie ab.

Im Element Appointment (F-03) steht contactName als Kopie des Namens aus der Kladde. Wie teuer wäre es, einen Namen überall zu ändern? Eine Annahme: 400 Kontakte, 7.360 Termine im Jahr (F-02, buero-nord), gleich verteilt, ein Schreibvorgang von etwa 6 RU (Annahme der Aufgabe aus Lektion 1.1). Das Programm zählte für einen Kontakt:

appointments of one contact in a year: 19; a rename of all copies = 114 RU

114 RU sind wenig. Die richtigere Frage ist deshalb nicht „was kostet es", sondern „soll man es tun?". Ein vergangener Termin ist eine Tatsache: Er fand mit dem Namen von damals statt. Eine Kopie, die einen Zeitpunkt festhält, pflegt man nicht. Eine Kopie, die immer aktuell sein soll, pflegt man mit dem Änderungsfeed (Lektion 2.5). Das legen Sie je Eigenschaft fest und schreiben es ins Modell.

Verwandt sind vorberechnete Werte (Learn: countOfBooks im Autor). Sie sparen Rechenzeit beim Lesen und kosten Rechenzeit beim Schreiben; das lohnt sich bei lesestarken Anwendungen. Weil ein Schreibvorgang mehrere Elemente einer Partition in einer Transaktion ändern kann (Lektion 2.3), muss man dafür nicht alles in ein Element zwängen.

Mehrere Elementtypen in einem Container

Elemente verschiedener Art dürfen in einem Container liegen, wenn sie zusammen gelesen oder zusammen geändert werden, vorausgesetzt, sie teilen den Partitionsschlüssel. Learn rät, ein Feld type mitzugeben. Für conversations (Schlüssel /officeId, /userId, /conversationId, F-03) heißt das: ein Element conversation als Kopf, dazu je Nachricht ein Element message, alle mit denselben drei Schlüsselwerten, also in einer logischen Partition. Dann liest eine Abfrage mit dem Schlüssel den ganzen Verlauf (Z4), Kopf und Nachrichten lassen sich gemeinsam ändern, und der Kopf trägt die letzten Nachrichten für Z5.

Warum nicht alle Nachrichten im Kopf? Das Programm baute ein Gespräch mit Nachrichten von etwa 230 Byte (Rolle, Zeitpunkt, ein Satz Text) einmal eingebettet:

C#
foreach (var count in new[] { 10, 100, 1_000, 5_000 })
{
    var doc = new ConversationEmbedded("c-001", "conversation", "buero-nord", "u-17", [.. Enumerable.Range(0, count).Select(MessageNo)]);
    Console.WriteLine($"  {count,5} messages: {SizeOf(doc),9:N0} bytes");
}
A. embedded messages
     10 messages:     2.414 bytes
    100 messages:    23.339 bytes
   1000 messages:   232.589 bytes
   5000 messages: 1.162.589 bytes
  the limit of 2.097.152 bytes is passed at message 9.020

Und als Kopf mit den letzten drei Nachrichten, daneben je Nachricht ein Element:

B. header plus message items
  header (5000 messages, 3 embedded): 813 bytes
  one message item:                   234 bytes

Der Kopf bleibt bei 813 Byte, egal wie lang das Gespräch wird; jede Nachricht kostet beim Schreiben nur ihre eigenen 234 Byte, nicht das ganze Gespräch. Das ist die hybride Lösung: eingebettet, was klein und begrenzt ist (die letzten Nachrichten für die Anzeige), als eigene Elemente, was wächst. Die Abfragen dazu (nicht gegen Azure ausgeführt), immer mit dem vollständigen Schlüssel:

SQL
SELECT * FROM c WHERE c.type = 'conversation'
SELECT * FROM c WHERE c.type = 'message' ORDER BY c.at DESC OFFSET 0 LIMIT 20

Grenzen dieses Musters: Alle Typen eines Containers teilen sich Durchsatz und Indexrichtlinie, und jede Abfrage muss type filtern oder den Schlüssel so eng setzen, dass sie nur das Gewollte trifft.

1:n und n:m ohne Fremdschlüssel

Beziehung Muster Beispiel Kladde.Verlauf
1:wenige, selten geändert einbetten Teilnehmende eines Termins (höchstens eine Handvoll) als Liste im Termin
1:viele, unbegrenzt Kind trägt die Kennung des Elternteils Termin trägt officeId und month
häufig geänderte Daten getrenntes Element, Verweis Zustand einer Aufgabe, der oft wechselt
n:m Kennungen in einer Liste auf einer oder beiden Seiten, statt einer Verknüpfungstabelle Termin und Teilnehmende

Bei n:m rät Learn, die Verknüpfungstabelle wegzulassen und die Listen der Kennungen beiderseits zu halten, solange die Listen klein bleiben. Eine Verknüpfungstabelle kostet immer zwei zusätzliche Abfragen.

Zahlen in JSON gehören an dieser Stelle dazu: Zahlen außerhalb von IEEE 754 (double) sollten als Text gespeichert werden, besonders in Partitionsschlüsseln, weil der Schlüssel später nicht mehr änderbar ist.

Übung 1

Teil A, ohne Konto. Legen Sie model.cs an (Message, ConversationEmbedded, ConversationHeader, SizeOf).

  1. Finden Sie die Nachrichtenzahl, bei der die eingebettete Fassung die 2 MB überschreitet (Schleife wie oben).
  2. Messen Sie den Kopf mit den letzten drei Nachrichten bei 5.000 Nachrichten und eine einzelne Nachricht.
  3. Entscheiden Sie für die vier Beziehungen einbetten, verweisen oder kopieren und schreiben Sie die Pflegeregel dazu: Büro und Termine; Termin und Kontaktname; Gespräch und Nachrichten; Aktivität (events) und Termin (Titel).

Teil B, mit einem Testkonto (nicht gegen Azure ausgeführt): Legen Sie Kopf und drei Nachrichten in conversations an und lesen Sie den Verlauf mit einer Abfrage mit vollem Schlüssel. Aufräumen: Elemente löschen oder die Datenbank des Tests.

So prüfen Sie es: Teil A zeigt die Zahlen der Lösung (bei anderem Beispieltext andere Werte; das Verfahren zählt).

Lösung 1

  1. Mit dem Beispieltext liegt die Grenze bei Nachricht 9.020 (2.097.152 Byte), also bei etwa 232 Byte je Nachricht. Ein lebhaftes Gespräch erreicht das nie, ein Gedächtnis über Jahre (Lektion 4.3) schon.
  2. 813 Byte für den Kopf, 234 Byte je Nachricht.
  3. Büro und Termine: verweisen (der Termin trägt officeId), unbegrenzt. Termin und Kontaktname: kopieren als Schnappschuss; vergangene Termine behalten den Namen von damals (Tatsache), künftige holen den neuen Namen nur, wenn Fachbereich es verlangt (Änderungsfeed, Lektion 2.5); die Kosten wären 114 RU für 19 Termine. Gespräch und Nachrichten: Kopf mit den letzten drei Nachrichten (eingebettet, begrenzt) und je Nachricht ein Element in derselben Partition. Aktivität und Termin: der Titel wird kopiert und nie gepflegt, denn eine Aktivität beschreibt, was zu einem Zeitpunkt geschah; die 90 Tage Ablaufzeit (F-03) räumen sie weg.

Teil B bleibt ungeprüft. Offen bleibt, ob die Kladde Umbenennungen überhaupt an Kladde.Verlauf meldet; das legt die Outbox fest (K4, Lektion 4.6.2).

Was Sie jetzt können

  • Sie leiten ein Modell aus Zugriffsmustern mit bekannten Schlüsselwerten ab.
  • Sie entscheiden einbetten, verweisen oder kopieren nach Wachstum und Änderungshäufigkeit.
  • Sie schätzen die Pflege einer Kopie in RU ab und fragen zuerst, ob sie nötig ist.
  • Sie halten Kopf und Kinder in einer Partition und trennen die Typen mit type.
  • Sie bilden n:m mit Listen von Kennungen ab und wissen, dass die Anwendung die Verweise prüft.

Weiterlesen