Tony Hoare, którego najczęściej określa się mianem wynalazcy referencji NULL, teraz nazywa to błędem o wartości miliarda dolarów, na który „cierpią” prawie wszystkie języki, w tym SQL.
Cytując Tony'ego (z jego artykułu w Wikipedii):
Nazywam to moim miliardowym błędem. Było to wynalezienie zerowej referencji w 1965 roku. W tym czasie projektowałem pierwszy kompleksowy system typów dla referencji w języku obiektowym (ALGOL W). Moim celem było zapewnienie, że wszelkie użycie referencji powinno być całkowicie bezpieczne, ze sprawdzaniem wykonywanym automatycznie przez kompilator. Ale nie mogłem oprzeć się pokusie umieszczenia odwołania null, po prostu dlatego, że było to tak łatwe do zaimplementowania. Doprowadziło to do niezliczonych błędów, luk w zabezpieczeniach i awarii systemu, które prawdopodobnie spowodowały miliard dolarów bólu i szkód w ciągu ostatnich czterdziestu lat.Interesujące jest to, że Tony miał ochotę zaimplementować to odniesienie, ponieważ było to łatwe. Ale dlaczego w ogóle potrzebował takiego odniesienia?
Różne znaczenia NULL
W idealnym świecie nie potrzebowalibyśmy wartości NULL. Każda osoba ma imię i nazwisko. Każda osoba ma datę urodzenia, pracę itp. A może?
Niestety nie.
Nie wszystkie kraje stosują koncepcję imion i nazwisk.
Nie wszyscy ludzie mają pracę. A czasami nie znamy ich pracy. Albo nas to nie obchodzi.
W tym miejscu NULL jest niezwykle przydatny. NULL może modelować wszystkie te stany, których tak naprawdę nie chcemy modelować. NULL może być:
- Wartość „nieokreślona” , tj. wartość, która nie została jeszcze zdefiniowana (prawdopodobnie z powodów technicznych), ale może być zdefiniowana później. Pomyśl o osobie, którą chcemy dodać do bazy, aby używać jej w innych tabelach. Na późniejszym etapie dodamy pracę tej osoby.
- Wartość „nieznana” , czyli wartość, której nie znamy (i możemy nigdy nie znać). Być może nie możemy już dłużej pytać tej osoby lub jej bliskich o datę urodzenia – informacje zostaną na zawsze utracone. Ale nadal chcemy modelować osobę, więc używamy NULL w znaczeniu UNKNOWN (co jest jego prawdziwym znaczeniem w SQL, jak zobaczymy później).
- Wartość „opcjonalna” , czyli wartość, której nie trzeba definiować. Zwróć uwagę, że wartość „opcjonalna” pojawia się również w przypadku OUTER JOIN, gdy sprzężenie zewnętrzne nie daje żadnych wartości po jednej stronie relacji. Lub także podczas korzystania z ZESTAWÓW GRUPOWANIA, gdzie różne kombinacje kolumn GRUPUJ WEDŁUG są łączone (lub pozostawiane puste).
- Wartość „usunięta” lub „uniknięto” , czyli wartość, której nie chcemy określać. Być może zwykle rejestrujemy stan cywilny osoby, jak to ma miejsce w niektórych jurysdykcjach, ale nie w innych, w których rejestracja jakichkolwiek danych osobowych tego typu jest nielegalna. Dlatego w niektórych przypadkach nie chcemy znać tej wartości.
- Wartość „specjalna” w danym kontekście , czyli wartość, której nie możemy inaczej zamodelować w zakresie możliwych wartości. Często dzieje się tak podczas pracy z zakresami dat. Załóżmy, że praca danej osoby jest ograniczona dwoma datami, a jeśli dana osoba obecnie pracuje na tym stanowisku, użyjemy NULL, aby powiedzieć, że okres jest nieograniczony na końcu zakresu dat.
- Przypadkowy NULL , tj. wartość NULL, która jest po prostu NULL, ponieważ programiści nie zwracali na to uwagi. W przypadku braku jawnego ograniczenia NOT NULL, większość baz danych zakłada, że kolumny mogą mieć wartość NULL. A gdy kolumny są wartościami null, programiści mogą po prostu „przypadkowo” umieścić wartości NULL w swoich wierszach, gdzie nawet nie zamierzali.
Jak widzieliśmy powyżej, to tylko kilka wybranych z 50 odcieni NULL .
Poniższy przykład pokazuje różne znaczenia NULL w konkretnym przykładzie SQL:
CREATE TABLE company ( id int NOT NULL, name text NOT NULL, CONSTRAINT company_pk PRIMARY KEY (id) ); CREATE TABLE job ( person_id int NOT NULL, start_date date NOT NULL, -- If end_date IS NULL, the “special value” of an unbounded -- interval is encoded end_date date NULL, description text NOT NULL, -- A job doesn’t have to be done at a company. It is “optional”. company_id int NULL, CONSTRAINT job_pk PRIMARY KEY (person_id,start_date), CONSTRAINT job_company FOREIGN KEY (company_id) REFERENCES company (id) ); CREATE TABLE person ( id int NOT NULL, first_name text NOT NULL, -- Some people need to be created in the database before we -- know their last_names. It is “undefined” last_name text NULL, -- We may not know the date_of_birth. It is “unknown” date_of_birth date NULL, -- In some situations, we must not define any marital_status. -- It is “deleted” marital_status int NULL, CONSTRAINT person_pk PRIMARY KEY (id), CONSTRAINT job_person FOREIGN KEY (person_id) REFERENCES person (id) );
Ludzie zawsze spierali się o brak wartości
Skoro NULL jest tak użyteczną wartością, dlaczego ludzie wciąż ją krytykują?
Wszystkie te poprzednie przypadki użycia NULL (i innych) są pokazane w tej interesującej, niedawnej rozmowie C.J. Date na temat „Problemu brakujących informacji” (obejrzyj film na YouTube).
Współczesny SQL może zrobić wiele niesamowitych rzeczy, o których nie wie niewielu programistów języków ogólnego przeznaczenia, takich jak Java, C#, PHP. Poniżej pokażę przykład.
W pewnym sensie C.J. Date zgadza się z Tonym Hoare, że (nad)używanie wartości NULL dla wszystkich tych różnych rodzajów „brakujących informacji” jest bardzo złym wyborem.
Na przykład w elektronice podobne techniki są stosowane do modelowania rzeczy takich jak 1, 0, „konflikt”, „nieprzypisany”, „nieznany”, „nie obchodzi mnie”, „wysoka impedancja”. Zauważ jednak, jak w elektronice różne wartości specjalne są używane do tych rzeczy, zamiast pojedynczej specjalnej wartości NULL . Czy to naprawdę lepsze? Co programiści JavaScript myślą o rozróżnieniu między różnymi „fałszywymi” wartościami, takimi jak „null”, „undefined”, „0”, „NaN”, pusty ciąg „”? Czy tak jest naprawdę lepiej?
A propos zera:gdy na chwilę opuścimy przestrzeń SQL i przejdziemy do matematyki, zobaczymy, że starożytne kultury, takie jak Rzymianie czy Grecy, miały te same problemy z liczbą zero. W rzeczywistości nie mieli nawet żadnego sposobu na przedstawienie zera, w przeciwieństwie do innych kultur, co można zobaczyć w artykule Wikipedii o liczbie zero. Cytując z artykułu:
Z zapisów wynika, że starożytni Grecy nie byli pewni statusu zera jako liczby. Zadawali sobie pytanie:„Jak nic nie może być czymś?”, prowadząc do filozoficznych, aw średniowieczu religijnych argumentów o naturze i istnieniu zera i próżni.Jak widzimy, „argumenty religijne” wyraźnie rozciągają się na informatykę i oprogramowanie, gdzie nadal nie wiemy na pewno, co zrobić z brakiem wartości.
Powrót do rzeczywistości:NULL w SQL
Choć ludzie (w tym naukowcy) nadal nie zgadzają się co do tego, czy potrzebujemy kodowania dla „nieokreślonego”, „nieznanego”, „opcjonalnego”, „usuniętego”, „specjalnego”, wróćmy do rzeczywistości i złych części dotyczących NULL dla SQL.
Jedną z rzeczy, o których często zapomina się, gdy mamy do czynienia z NULL SQL, jest to, że formalnie implementuje on przypadek UNKNOWN, który jest specjalną wartością, która jest częścią tzw. w przypadku operacji UNION lub INTERSECT.
Jeśli wrócimy do naszego modelu:
Jeśli na przykład chcemy intuicyjnie znaleźć wszystkie osoby, które nie są zarejestrowane jako małżeństwa, chcielibyśmy napisać następujące oświadczenie:
SELECT * FROM person WHERE marital_status != 'married'
Niestety, z powodu trójwartościowej logiki i NULL SQL, powyższe zapytanie nie zwróci tych wartości, które nie mają wyraźnego statusu małżeństwa. Dlatego musimy napisać dodatkowy, wyraźny predykat:
SELECT * FROM person WHERE marital_status != 'married' OR marital_status IS NULL
Lub, przed porównaniem zmieniamy wartość na wartość NOT NULL
SELECT * FROM person WHERE COALESCE(marital_status, 'null') != 'married'
Trzy wartości logiczne są trudne. I to nie jedyny problem z NULL w SQL. Oto więcej wad używania NULL:
- Istnieje tylko jedna wartość NULL, gdy naprawdę chcieliśmy zakodować kilka różnych „nieobecnych” lub „specjalnych” wartości. Zakres użytecznych wartości specjalnych w dużym stopniu zależy od domeny i używanych typów danych. Jednak wiedza o domenie jest zawsze wymagana do prawidłowej interpretacji znaczenia kolumny dopuszczającej wartość null, a zapytania muszą być starannie zaprojektowane, aby zapobiec zwracaniu błędnych wyników, jak widzieliśmy powyżej.
- Ponownie, trójwartościowa logika jest bardzo trudna do poprawnego. Chociaż powyższy przykład jest nadal dość prosty, jak myślisz, co przyniesie następujące zapytanie?
SELECT * FROM person WHERE marital_status NOT IN ('married', NULL)
Dokładnie. Jak wyjaśniono w tym artykule, to w ogóle niczego nie przyniesie. W skrócie, powyższe zapytanie jest takie samo jak poniższe:
SELECT * FROM person WHERE marital_status != 'married' AND marital_status != NULL -- This is always NULL / UNKNOWN
-
Baza danych Oracle traktuje NULL i pusty ciąg „” jako to samo. Jest to bardzo trudne, ponieważ nie od razu zauważysz, dlaczego następujące zapytanie zawsze zwraca pusty wynik:
SELECT * FROM person WHERE marital_status NOT IN ('married', '')
-
Oracle (ponownie) nie umieszcza wartości NULL w indeksach. Jest to źródło wielu nieprzyjemnych problemów z wydajnością, np. gdy używasz kolumny dopuszczającej wartość null w predykacie NOT IN jako takim:
SELECT * FROM person WHERE marital_status NOT IN ( SELECT some_nullable_column FROM some_table )
W przypadku Oracle powyższe zabezpieczenie przed sprzężeniem spowoduje pełne skanowanie tabeli, niezależnie od tego, czy masz indeks na some_nullable_column. Ze względu na logikę trójwartościową i ponieważ Oracle nie umieszcza wartości NULL w indeksach, silnik będzie musiał trafić w tabelę i sprawdzić każdą wartość, aby upewnić się, że w zestawie nie ma co najmniej jednej wartości NULL, co spowodowałoby cały predykat NIEZNANY.
Wniosek
Nie rozwiązaliśmy jeszcze problemu NULL w większości języków i platform. Chociaż twierdzę, że NULL NIE jest miliardowym błędem, za który Tony Hoare próbuje przepraszać, NULL z pewnością też jest daleki od ideału.
Jeśli chcesz pozostać po bezpiecznej stronie projektu bazy danych, unikaj za wszelką cenę wartości NULL, chyba że absolutnie potrzebujesz jednej z tych specjalnych wartości do zakodowania przy użyciu wartości NULL. Pamiętaj, że te wartości to:„nieokreślony”, „nieznany”, „opcjonalny”, „usunięty” i „specjalny” i nie tylko:50 odcieni wartości NULL . Jeśli nie znajdujesz się w takiej sytuacji, zawsze domyślnie dodaj ograniczenie NOT NULL do każdej kolumny w Twojej bazie danych. Twój projekt będzie znacznie czystszy, a wydajność znacznie lepsza.
Gdyby tylko NOT NULL było wartością domyślną w DDL, a NULLABLE słowo kluczowe, które musiało być ustawione jawnie…
Jakie są twoje odczucia i doświadczenia z NULL? Jak Twoim zdaniem mógłby działać lepszy SQL?
Lukas Eder jest założycielem i dyrektorem generalnym Data Geekery GmbH z siedzibą w Zurychu w Szwajcarii. Data Geekery sprzedaje produkty i usługi baz danych dotyczące Javy i SQL od 2013 roku.
Od czasu studiów magisterskich na EPFL w 2006 roku fascynuje go interakcja Java i SQL. Większość tego doświadczenia zdobył w szwajcarskiej bankowości elektronicznej poprzez różne warianty (JDBC, Hibernate, głównie z Oracle). Chętnie dzieli się tą wiedzą na różnych konferencjach, JUG-ach, prezentacjach wewnętrznych i na swoim firmowym blogu.