Pierwszy model kontraktów, który napisałem dla narzędzia BI w dużej firmie, miał jakieś osiemdziesiąt linii SQL-a i wydał mi się w porządku. Kontrakt, data początku, data końca, kilka warunków podwieszonych pod spodem. Czytało się czysto. Rok później ten sam model przepisałem ponad dziesięć razy, w tym raz zmieniając mu nazwę, bo nazwa kłamała.
To nie było złe planowanie. To było to, że dziedzinę widać dopiero, kiedy wchodzą prawdziwe dane.
Dlaczego pierwszy model zawsze wygląda poprawnie
Bo piszesz go przeciwko dziedzinie, o której ci opowiedziano, a nie tej, która istnieje. Mówią ci "kontrakt ma datę końca". Modelujesz to. Działa na dwudziestu wierszach, na które patrzysz.
Pierwsze ostrzeżenie przyszło z nazwą. Model nazywał się kpi_02_20_revenue_contracts, a zmieniłem go na kpi_02_20_rent_contracts. To nie było revenue, tylko rent: umowy najmu, w których firma płaci, a nie inkasuje. Przeciwny znak. Nazwa wzięła się z ticketu, a nie z tabeli. Miesiąc pracy z modelem, który nazywał się odwrotnie do tego, co zawierał.
Źle postawione nazwy to nie kosmetyka. Ktoś je czyta i coś zakłada.
Co pojawia się dopiero, gdy wchodzą prawdziwe dane
Data końca była studnią bez dna. Tabela miała dwie kolumny: DT_FIRST_CONTRACT_END i DT_CONTRACT_END_NEXT. Pierwsza wersja ujednoliconej daty wyglądała tak:
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
Wygląda wyczerpująco. Nie jest. Brakuje przypadku, w którym obie są nullem, i tego, w którym pierwsza jest w przeszłości, a druga nie istnieje. W danych oba przypadki znaczyły to samo: kontrakt bezterminowy. case wysyłał je do przypadkowego null, który potem liczył się jako "wygasły". Wersja finalna dodaje te dwie jawne gałęzie i when dt_end_merged is null then True we fladze is_unlimited, bo null jest tam informacją, a nie brakiem informacji.
A ta sama flaga niosła już inną perełkę z prawdziwego świata:
when year_end_merged in (8909, 8899, 9999) then True
Trzy różne lata-wartownicy na powiedzenie "bez końca". Żaden udokumentowany. Wszystkie trzy na produkcji.
Trzy razy, kiedy cofnęliśmy decyzję
Najbardziej wymowny jest rok 99. W źródłowych Excelach były daty końca, w których rok przychodził jako 99, i ktoś w zespole mówił, że to znaczy "bezterminowo". Dodałem obsługę:
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
I odpowiadający filtr, or ... like '%99%', żeby te wiersze nie wypadały przez okno dat. Usunąłem to trzy dni później. like '%99%' łapie 1999, 2099 i każdą datę z dziewiątką-dziewiątką w dniu albo miesiącu. Wrzucałem kontrakty zamknięte dwadzieścia lat wcześniej do wyliczenia aktywnego revenue. Reguła biznesowa może i była prawdziwa; implementacja przez like na tekstowej dacie już nie.
Dwa miesiące później ten sam problem wrócił innymi drzwiami, w modelu renegocjacji: dwucyfrowe lata. Tym razem zrobiłem to na już wyciągniętym roku, a nie na tekście:
case
when year(normalized_next_possible_termination_old_contract) < 100
then year(...) + 2000
else year(...)
end
To też nie przetrwało w nienaruszonej postaci: dwa tygodnie później zostało w pliku zakomentowane z -- ! z przodu. Trzecia runda wokół tej samej kolumny.
Drugim dużym wycofaniem był inner join między kontraktami a warunkami. Kontrakt bez warunku nie pojawiał się. Zmiana na left join zmieniła liczby na dashboardzie i to jest dokładnie ta rozmowa, którą trzeba odbyć z tym, kto zamawia KPI: czy kontrakt bez warunku liczy się, czy nie. Za pierwszym razem zdecydowałem sam, jednym inner, nie zdając sobie sprawy, że w ogóle o czymś decyduję.
Po czym poznaję, że model jest już stabilny
Po rodzaju commitów. Przez pierwszy rok commity były strukturalne: zmieniające się joiny, filtry wchodzące i wychodzące, zmieniane nazwy kolumn. Pod koniec projektu commity były innego rodzaju: usunąć mnożnik przez 100, scalić dwa case w jeden, napisać dokumentację kolumn. To jest model, który nie rusza się już od spodu, tylko z wierzchu.
Drugi sygnał to to, że plik .yml z dokumentacją przestaje mieć cztery mgliste punkty i zaczyna wymieniać dokładne kryteria wyboru: podmioty, typy kontraktów, walutę, co jest wyłączone i dlaczego. Kiedy potrafisz napisać tę listę bez patrzenia w SQL, model naprawdę istnieje. Wcześniej masz zapytanie.
Co robię dziś przed pierwszym CREATE TABLE
Kiedyś pisałem model, a potem patrzyłem na dane. Teraz jest odwrotnie i w tej kolejności:
- Policzyć nulle w każdej kolumnie, która wejdzie do warunku. Kolumna z 40% nulli nie jest datą, tylko dwoma zmieszanymi pojęciami.
- Poszukać wartości-wartowników.
select distinct year(...) order by 1 desc. Jeśli wyskoczą 9999, 8899 albo 2099, są tam ukryte nieudokumentowane reguły biznesowe. - Napisać filtr prozą, zanim napiszesz go w SQL-u. Jeśli nie umiem powiedzieć w jednym zdaniu, które kontrakty wchodzą,
whereskończy z sześcioma sprzecznymi warunkami i dwoma zakomentowanymi. - Normalizować na typach, nigdy na stringach. Każde
likena dacie to bomba z opóźnionym zapłonem.
Modelu dziedziny się nie projektuje: negocjuje się go z danymi, a dane nie ustępują. Za każdym razem, kiedy coś cofałem, było tak dlatego, że zdecydowałem o czymś, o co powinienem był zapytać.
