02Datenbanken5 min

Ich habe dasselbe Vertragsmodell mehr als zehnmal neu geschrieben

Ein kompliziertes Domänenmodell gelingt nicht beim ersten Versuch. Man nähert sich ihm an, indem man es mit den echten Daten vor Augen neu schreibt, und dazu gehört, Entscheidungen zurückzunehmen, die richtig aussahen.

Von Salva Castells

Titelbild: Ernst Alban, Gemeinfrei.
In diesem Beitrag

Das erste Vertragsmodell, das ich für das BI-Tool eines großen Unternehmens geschrieben habe, hatte rund achtzig Zeilen SQL und kam mir gut vor. Ein Vertrag, ein Startdatum, ein Enddatum, ein paar Konditionen daran. Es las sich sauber. Ein Jahr später hatte ich genau dieses Modell mehr als zehnmal neu geschrieben, einmal davon mit einer Umbenennung, weil der Name log.

Das war keine schlechte Planung. Es lag daran, dass man die Domäne erst sieht, wenn die echten Daten hereinkommen.

Warum das erste Modell immer richtig aussieht

Weil du es gegen die Domäne schreibst, die man dir erzählt hat, nicht gegen die, die existiert. Man sagt dir "ein Vertrag hat ein Enddatum". Du modellierst das. Es funktioniert mit den zwanzig Zeilen, die du dir anschaust.

Die erste Warnung kam über den Namen. Das Modell hieß kpi_02_20_revenue_contracts und ich habe es in kpi_02_20_rent_contracts umbenannt. Es war kein revenue, es war rent: Mietverträge, bei denen das Unternehmen zahlt, nicht kassiert. Umgekehrtes Vorzeichen. Der Name kam aus dem Ticket, nicht aus der Tabelle. Einen Monat lang mit einem Modell gearbeitet, dessen Name das Gegenteil dessen sagte, was drinsteckte.

Falsch gewählte Namen sind keine Kosmetik. Jemand liest sie und nimmt sie für bare Münze.

Was erst auftaucht, wenn die echten Daten hereinkommen

Das Enddatum war der Abgrund. Die Tabelle hatte zwei Spalten: DT_FIRST_CONTRACT_END und DT_CONTRACT_END_NEXT. Die erste Version des vereinheitlichten Datums war das hier:

case
    when c.DT_FIRST_CONTRACT_END is null then c.DT_CONTRACT_END_NEXT
    when c.DT_CONTRACT_END_NEXT is null then c.DT_FIRST_CONTRACT_END
    when c.DT_FIRST_CONTRACT_END < CURRENT_DATE() then c.DT_CONTRACT_END_NEXT
    else c.DT_FIRST_CONTRACT_END
end as dt_end_merged

Sieht vollständig aus. Ist es nicht. Es fehlt der Fall, dass beide null sind, und der, dass das erste in der Vergangenheit liegt und das zweite gar nicht existiert. In den Daten bedeuteten beide Fälle dasselbe: unbefristeter Vertrag. Das case schickte sie in ein versehentliches null, das danach als "abgelaufen" gezählt wurde. Die finale Version ergänzt diese zwei expliziten Zweige und ein when dt_end_merged is null then True im Flag is_unlimited, denn null ist an dieser Stelle eine Information, nicht das Fehlen von Information.

Und dasselbe Flag schleppte schon ein anderes Juwel aus der echten Welt mit:

when year_end_merged in (8909, 8899, 9999) then True

Drei verschiedene Sentinel-Jahre für "kein Ende". Keines dokumentiert. Alle drei in Produktion.

Die drei Male, die wir eine Entscheidung zurückgenommen haben

Das aufschlussreichste ist das Jahr 99. In den Ausgangs-Excels gab es Enddaten, deren Jahr als 99 ankam, und jemand im Team sagte, das bedeute "unbefristet". Ich habe die Behandlung ergänzt:

case
    when {{ normalize_date('ew.WARUNEK_WA_NY_DO') }} like '%99%' then '2099-12-31'::date
    else {{ normalize_date('ew.WARUNEK_WA_NY_DO') }}::date
end

Und den passenden Filter, ein or ... like '%99%', damit diese Zeilen nicht durch das Datumsfenster fielen. Drei Tage später habe ich es wieder entfernt. Das like '%99%' passt auf 1999, auf 2099 und auf jedes Datum, das eine 99 im Tag oder im Monat trägt. Ich schleppte vor zwanzig Jahren geschlossene Verträge in die Berechnung des aktiven Revenue. Die Geschäftsregel mochte stimmen; die Implementierung per like über einen Datums-String stimmte nicht.

Zwei Monate später kam dasselbe Problem durch eine andere Tür zurück, im Modell für Neuverhandlungen: zweistellige Jahre. Diesmal habe ich es auf dem bereits extrahierten Jahr gemacht, nicht auf dem Text:

case
  when year(normalized_next_possible_termination_old_contract) < 100
    then year(...) + 2000
  else year(...)
end

Auch das hat nicht unversehrt überlebt: Zwei Wochen später stand es auskommentiert in der Datei, mit einem -- ! davor. Dritte Runde über derselben Spalte.

Das andere große Rollback war der inner join zwischen Verträgen und Konditionen. Ein Vertrag ohne Kondition tauchte nicht auf. Der Wechsel auf left join veränderte die Zahlen im Dashboard, und genau das ist das Gespräch, das man mit der Person führen muss, die den KPI bestellt: ob ein Vertrag ohne Kondition zählt oder nicht. Beim ersten Mal habe ich das allein entschieden, mit einem inner, ohne zu merken, dass ich überhaupt etwas entschied.

Woran ich erkenne, dass ein Modell stabil ist

An der Art der Commits. Im ersten Jahr waren die Commits strukturell: Joins, die sich ändern, Filter, die kommen und gehen, Spalten, die umbenannt werden. Am Ende des Projekts waren die Commits von anderer Sorte: einen Multiplikator mal 100 entfernen, zwei case zu einem verschmelzen, die Spaltendokumentation schreiben. Das ist ein Modell, das sich nicht mehr von unten bewegt, nur noch von oben.

Das andere Signal ist, dass die .yml-Dokumentation von vier vagen Stichpunkten dazu übergeht, die genauen Auswahlkriterien aufzulisten: Entitäten, Vertragsarten, Währung, was ausgeschlossen wird und warum. Wenn du diese Liste schreiben kannst, ohne ins SQL zu schauen, existiert das Modell wirklich. Vorher hast du eine Abfrage.

Was ich heute vor dem ersten CREATE TABLE mache

Früher schrieb ich das Modell und schaute dann in die Daten. Jetzt ist es umgekehrt, und zwar in dieser Reihenfolge:

  • Die Nulls jeder Spalte zählen, die in eine Bedingung eingehen soll. Eine Spalte mit 40 % Nulls ist kein Datum, sondern zwei vermischte Konzepte.
  • Nach Sentinel-Werten suchen. select distinct year(...) order by 1 desc. Wenn 9999, 8899 oder 2099 auftauchen, verstecken sich dort undokumentierte Geschäftsregeln.
  • Den Filter in Prosa schreiben, bevor ich ihn in SQL schreibe. Wenn ich nicht in einem Satz sagen kann, welche Verträge eingehen, endet das where mit sechs widersprüchlichen Bedingungen, zwei davon auskommentiert.
  • Über Typen normalisieren, nie über Strings. Jedes like auf einem Datum ist eine Zeitbombe.

Ein Domänenmodell wird nicht entworfen: Es wird mit den Daten verhandelt, und die Daten geben nicht nach. Jedes Mal, wenn ich etwas zurückgenommen habe, lag es daran, dass ich selbst entschieden hatte, was ich hätte fragen müssen.