I.
Wprowadzenie do  języków zapytań (1)
II.
Wprowadzenie do  języków zapytań (2)
III.
Pojęcia obiektowości w bazach danych (1)
IV.
Pojęcia obiektowości w bazach danych (2)
  Wstęp
  1. Klasy
  2. Polimorfizm
  3. Role
  4. Kolekcje
  5. Trwałość
  6. Moduły
  Podsumowanie
  Zadania
V.
Podstawy semantyczne języków zapytań
VI.
Modele składu obiektów
VII.
Stos środowisk, rezultaty zapytań, funkcja nested
VIII.
Język SBQL (Stack-Based Query Language) (1)
IX.
X.
Dalsze własności SBQL
XI.
Operatory order by i group by
XII.
Przetwarzanie struktur nieregularnych
XIII.
Rozszerzenie języków zapytań o konstrukcje imperatywne
XIV.
Procedury, procedury funkcyjne, metody, reguły zakresu
XV.
Parametry procedur i metod, procedury rekurencyjne, optymalizacja poprzez modyfikację zapytań

 

4. Kolekcje

Kolekcje są nazwanymi zestawami danych o podobnej strukturze. Przyjmuje się, że rozmiaru kolekcji nie można przewidzieć ani ograniczyć. Do kolekcji zaliczane są zbiory, relacje, bagi, sekwencje, listy, drzewa itp.

Kolekcje nie występują w wielu popularnych językach programowania, np. w C. W języku Pascal są one zredukowane do jednego pojęcia pliku (file), dodatkowo obciążonego ograniczeniami implementacyjnymi. Brak kolekcji oznacza, że programista musi dość często używać pojęcia sterty (heap) do implementacji różnych zadań, co związane jest z uciążliwymi operacjami oraz znacznie zwiększoną skłonnością do błędów (m.in. wskutek operowania na pointerach, brakiem kontroli typologicznej i możliwością "wyciekania pamięci" (memory leak)). Popularne języki obiektowe (Smalltalk, C++, Java) nie wprowadzają pojęcia kolekcji lub wprowadzają ograniczenia na rodzaj, typ lub uniwersalność tego pojęcia. Najpoważniejsze niewygody związane z posługiwaniem się tym pojęciem dotyczą środków definiowania kolekcji (szczególnie zagnieżdżonych i powiązanych związkami referencyjnymi) środków dostępu do elementów kolekcji i ich użycia w programie. Kolekcje są wprowadzane implicite w popularnych metodykach i notacjach obiektowych takich jak OMT lub UML. Nie jest jednak jasne, czy oznaczenie klasy zawsze implikuje kolekcję jej obiektów, czy w tych notacjach można używać atrybutów będących zbiorami wartości, czy oznaczenia liczności asocjacji implikują kolekcje odpowiednich powiązań itd.

Z drugiej strony, bazy danych są przede wszystkim nastawione na przetwarzanie kolekcji. W szczególności, kolekcją jest relacja lub tablica w systemach relacyjnych. Występowanie kolekcji w bazach danych jest bezpośrednią przyczyną pojawienia się języków zapytań.

Brak (lub ograniczenia) kolekcji w językach programowania jest główną przyczyną niekorzystnego efektu określanego jako niezgodność impedancji (impedance mismatch). Obiektowe bazy danych, w założeniu, miały uniknąć tego efektu poprzez odpowiednie potraktowanie typów zarówno w bazach danych, jak i w językach programowania. Niestety, w obecnych systemach obiektowych baz danych (w szczególności bazujących na standardzie ODMG) to początkowe założenie pozostało w sferze pobożnych życzeń, głównie z powodu oparcia ich interfejsów na językach C++, Smalltalk i Java.


4.1. Rodzaje kolekcji

W standardzie ODMG wyróżniono następujące kolekcje:

  • Zbiory (sets): mogą zawierać elementy dowolnego, ustalonego typu, ale bez powtórzeń.

  • Bagi (multisets, bags): podobnie jak zbiory, ale elementy mogą się powtarzać.

  • Sekwencje (sequences): uporządkowane liniowo kolekcje elementów dowolnego, ustalonego typu; porządek ma tu znaczenie informacyjne, zaś elementy mogą się powtarzać. Dostęp do elementów jest również sekwencyjny. Możliwy jest jednak dostęp poprzez indeks (kolejny numer) elementu sekwencji, tak jak dla dynamicznych tablic.

  • Tablice dynamiczne (dynamic arrays): są podobne do sekwencji, ale z dostępem jak do tablic (poprzez indeks wiersza tablicy). Możliwe jest dostawianie i usuwanie elementów na końcu tablicy, ale w odróżnieniu od sekwencji tego rodzaju operacje są niedozwolone na elementach ze środka tablicy.

  • Słowniki, czyli zbiory par (t,v), gdzie t jest pewnym hasłem (ciągiem znaków), zaś v jest wartością dowolnego typu. Intencją wprowadzenia tego typu jest takie jego zorganizowanie (np. w oparciu o B-drzewa), aby był możliwy szybki dostęp do każdego elementu na podstawie znajomości t.


22
Rys.22. Przykład zagnieżdżania kolekcji

Zróżnicowanie kolekcji następuje nie tylko na podstawie różnic koncepcyjnych i implementacyjnych w strukturach danych, ale także na podstawie różnic w zestawie operatorów służących do ich obsługi. Przykładowo, słowniki są zbiorami wyposażonymi w kilka dalszych operatorów. M.in. z tego powodu powyższy zestaw kolekcji jest dość dowolny i kontrowersyjny ze względu na koncepcyjną redundancję. W szczególności, jeżeli wprowadzono bagi, to praktyczna przydatność zbiorów staje się niewielka. Zbiory implikują ponadto istotny problem semantyczny związany z tym, co to znaczy, że dwa elementy są identyczne. Implikuje także pracochłonne testowanie obecności duplikatów i ich usuwanie, które w większości przypadków jest niepotrzebne. Dodajmy, że język SQL w gruncie rzeczy nie zajmuje się zbiorami, lecz bagami, i nikomu to nie przeszkadza. Sekwencje i tablice dynamiczne są strukturami dość podobnymi, w związku z czym potrzeba ich różnicowania może być również przedmiotem kontrowersji. Słowniki z kolei nie są strukturami koncepcyjnymi; ich obecność jest podyktowana względami implementacyjnymi, które mogą być przesunięte na niższy, operacyjny poziom decyzji (np. do dyspozycji administratora bazy danych).

Kolekcje można zagnieżdżać, przy czym liczba poziomów zagnieżdżania i rodzaj zagnieżdżania nie powinny być ograniczane. Możliwe jest więc tworzenie np. zbiorów sekwencji obiektów, obiektów z atrybutami w postaci bagów itp. Rys.22 pokazuje taką kombinację struktur danych o typach masowych. Pracownicy jest strukturą typu masowego zawierającą obiekty Pracownik, które z kolei zawierają m.in. atrybut Zatrudnienia będący sekwencją miejsc jego wcześniejszego zatrudnienia oraz zbiór Dzieci zawierający obiekty Dziecko.

Powyższa elastyczność w zakresie zagnieżdżania kolekcji jest wygodna zarówno w modelowaniu pojęciowym, jak i w programowaniu. Ten pogląd stał się bardziej popularny w ostatnim okresie. Twórcy modelu relacyjnego nie zakładali i wręcz zwalczali taką możliwość jako szkodliwą dla prostoty systemu. W szczególności, w modelu relacyjnym nie był dopuszczalny atrybut będący zbiorem wartości; było to złamanie tzw. "pierwszej postaci normalnej" (1NF). Wydaje się jednak, że zwolennicy tego rodzaju doktrynalnych ograniczeń ostatecznie przegrali (przynajmniej w sensie ideologicznym), ponieważ okazały się one wadliwe zarówno z punktu widzenia modelowania pojęciowego, jak i niezbędnej elastyczności w zakresie struktur danych. Ich zalety koncepcyjne, implementacyjne oraz wspomaganie ze strony zmatematyzowanych teorii okazały się iluzją.


4.2. Operatory i konstrukcje do przetwarzania kolekcji

Kolekcje są związane ze specyficznymi operatorami oraz konstrukcjami językowymi służącymi do ich obsługi. Kolekcje mogą być zapamiętane, czyli stanowiące o stanie (bazy danych lub programu) oraz ulotne, czyli zwracane jako wynik wyrażeń, funkcji lub zapytań. W zależności od tego, pewne operatory lub konstrukcje językowe mogą mieć lub mogą nie mieć zastosowania do danej kolekcji. Operatory lub konstrukcje można pogrupować w dwie kategorie:

  • Operatory makroskopowe, których argumentem oraz wynikiem są kolekcje;

  • Operatory iteracyjne, których zadaniem jest kolejne przetwarzanie pojedynczych elementów kolekcji.


Inny podział dotyczy możliwości operowania na stanie: operatory mogą być wyszukiwawcze (nie zmieniające stanu) lub aktualizacyjne (zmieniające stan). Przykładowe operatory nie iteracyjne mogą być następujące:

  • Dla bagów:
    • suma bagów(bez usuwania duplikatów),
    • przecięcie bagów,
    • różnica bagów,
    • zawieranie się bagów,
    • usunięcie duplikatów z bagów,
    • zamiana bagu na sekwencję,
    • przynależność elementu do bagu,
    • rozmiar bagu,
    • usunięcie elementu z bagu,
    • dostawienie elementu do bagu.

  • Dla sekwencji (podobnie dla tablic):
    • konkatenacja sekwencji,
    • pierwszy, ostatni, n-ty element sekwencji,
    • rozmiar sekwencji,
    • zawieranie się sekwencji,
    • usunięcie pierwszego, ostatniego, n-tego elementu sekwencji
    • wstawienie nowego elementu przed pierwszym, za ostatnim, przed/za n-tym elementem sekwencji,
    • przynależność elementu do sekwencji,
    • zamiana sekwencji na bag,
    • usunięcie duplikatów z sekwencji (z zachowaniem porządku),sortowanie sekwencji według pewnego klucza.


Niektóre z wymienionych operatorów wymagają zdefiniowania dodatkowego operatora, który ustali, co to znaczy, że dwa elementy kolekcji są identyczne. Problem nie jest do końca trywialny. Jeżeli np. elementem kolekcji jest obiekt, wówczas należałoby ustalić kryterium identyczności dwóch obiektów. Wyróżnia się dwa przypadki:


Porównanie głębokie jest często rozważane w literaturze, ale prawie nigdy nie implementowane ze względu na trudności implementacyjne oraz semantyczne niejasności ,np. dotyczące tego, czy należy uwzględniać powiązania obiektu z innymi obiektami, czy też nie. Implementacja porównania płytkiego jest oczywiście banalna.

W tej sytuacji kolekcje obiektów właściwie są zbiorami, a nie bagami, gdyż każdy obiekt ma unikalny identyfikator. O bagach można więc mówić wyłącznie wtedy, gdy kolekcje składają się z wartości, przy czym mogą to być wartości atomowe, referencje (identyfikatory obiektów) lub wartości złożone (z uwzględnieniem nazw etykietujących wartości).

Jak widać, zestaw operatorów działających na kolekcjach może być dość duży, ponadto mogą istnieć operatory będące kombinacją tych lub innych operatorów. Praktyka dowiodła jednak, że niezależnie od tego, jak bogaty jest zestaw tego rodzaju operatorów, jest on zawsze niedostatecznie uniwersalny dla dowolnego przetwarzania kolekcji. Z tego powodu konieczne są konstrukcje iteracyjne, umożliwiające przetwarzanie kolekcji element po elemencie.

W praktyce języków programowania tego rodzaju konstrukcje są zwane "iteratorami". Iterator jest zestawem operatorów, takich jak: "daj pierwszy element", "daj następny element", "czy ostatni element?" itp., których celem jest odzyskanie kolejnych elementów z kolekcji. Iterator posiada swój własny stan przechowujący informację o bieżącym elemencie. W językach programowania dość często iterator jest przypisywany do kolekcji, co (wraz z faktem, że iterator posiada stan) prowadzi do problemów z zagnieżdżaniem iteratorów obiegających tę samą kolekcję. Zwrócimy także uwagę, że jeżeli kolekcja ma być aktualizowana poprzez iterator, wówczas iterator musi zwrócić referencję do elementu kolekcji. Iteratory są dość często obudowane własną składnią mającą na celu uproszczenie i zdyscyplinowanie ich użycia, ale w istocie zasada organizacji i działania tych iteratorów jest podobna do opisanej powyżej.

To przetwarzanie odbywa się na znacznie niższym poziomie niż poziom języków zapytań. W szczególności, nie jest optymalizowane, co powoduje, że przetwarzanie dużych kolekcji poprzez iteratory może mieć nieakceptowalne czasy wykonania. W językach zapytań iteratory są zamknięte wewnątrz operatorów takich jak selekcja, projekcja, złączenie, unia, co stwarza znacznie większe możliwości optymalizacyjne. Dla celów dowolnego przetwarzania stosuje się operator "for each", który obiega całą kolekcją element po elemencie i dla każdego elementu wykonuje pewne przetwarzanie, np.:

P4.16.

for each ( Pracownik where Stan = "analityk" ) as p do {
    print (p.Nazwisko);
    p.Zarobek := p.Zarobek + 100; };

W niektórych opracowaniach, np. w standardzie ODMG, operatory służące do przetwarzania kolekcji są zgrupowane w formie zestawu interfejsów, które niczym nie różnią się od interfejsów do normalnych obiektów, np.

P4.17.

interface Collection: Object{
    unsigned long cardinality();
    boolean is_empty();
    void insert_element( in Object element );
    boolean contains_element( in Object element );
...};


 P4.18.

interface Set : Collection{
    Set union_with( in Set other );
    Set difference_with( in Set other );
    boolean is_subset_of( in Set other );
... };

Tego rodzaju podejście do kolekcji jest obarczone błędem wynikającym z faktu, że kolekcje (i interfejsy do kolekcji) są parametryzowane typem elementu tych kolekcji. Stąd, podane wyżej interfejsy są koncepcyjnie różnymi bytami od normalnych interfejsów i bardziej odpowiadają temu, co w C++ nosi nazwę "szablonu" (template). Twórcy standardu ODMG próbują ten fakt zignorować, co w opinii niektórych specjalistów podważa sens tego standardu [Alag97].

Pojęcie kolekcji jest również nieco kontrowersyjne, gdyż prowadzi do sprzeczności z podstawowymi założeniami obiektowości. Jeżeli przyjmiemy następujące założenia:

  • dowolne dwie kolekcje nie posiadają jakichkolwiek wspólnych części;

  • ma być spełniona zasada zamienialności: każdy element klasy bardziej wyspecjalizowanej jest jednocześnie elementem klasy bardziej ogólnej;

  • ma być spełniona zasada otwarte-zamknięte: klasa może być zamknięta do modyfikacji, ale pozostaje otwarta dla specjalizacji.


Pierwsze założenie implikuje, że kolekcje Osoby i Studenci są rozłączne. Drugie założenie implikuje, że wewnątrz klasy Osoby muszą być przetwarzane obiekty Student pochodzące z kolekcji Studenci. Jeżeli w trakcie działania systemu dołożymy nową kolekcję Pracownicy, której obiekty są na mocy drugiego założenia także Osobami, wówczas jest konieczne poprawienie klasy Osoby, co łamie trzecie założenie. Podobny problem powstaje z heterogenicznymi kolekcjami, tj. takimi, które mogą posiadać obiekty różnych typów.

Jak się okazuje, pojęcie kolekcji nie jest niezbędne, a wobec podanych wyżej problemów lepiej go w ogóle nie wprowadzać. W XML pojęcie kolekcji nie występuje; zamiast tego można wprowadzić obiekty z tą samą nazwą na tym samym poziomie hierarchii. Przykładowo, dla obiektu Książka może być wiele atrybutów o tej samej nazwie Autor. Podobnie, na Rys.23 i Rys.24 atrybut Lokacja jest kolekcją, która w modelu składu została przedstawiona jako wiele atrybutów z tą samą nazwą Lokacja. Ten zabieg eliminuję potrzebę wprowadzania pojęcia kolekcji, przez co wyżej wymieniona niespójność nie występuje. W połączeniu z dynamicznymi rolami zabieg ten umożliwia również spójne potraktowanie heterogenicznych kolekcji.


4.3. Wartości zerowe a kolekcje

Wartości zerowe (null values) stanowią istotny temat w bazach danych, który zaowocował setkami artykułów oraz wieloma praktycznymi rozwiązaniami. Zwykle są oznaczane jako NULL lub NIL. Istnieje wiele przyczyn powstawania wartości zerowych, np.:

  • Pewien atrybut nie ma zastosowania dla konkretnego przypadku, np. atrybut NazwiskoPanieńskie dla mężczyzn i kobiet niezamężnych;

  • Informacja jest nieznana, np. miejsce, gdzie został pochowany Mozart;

  • Informacja o przyszłości, np. wynik przyszłego meczu piłkarskiego;

  • Informacja jeszcze nie zapełniona; np. po wprowadzeniu danych pracownika jego zarobek jest wartością zerową, gdyż wypełni ją później inny proces;

  • Wartość zerowa jest sygnałem wartości domniemanej, np. jeżeli prawie wszyscy pracownicy są urzędnikami, to pole Stanowisko wypełnia się tylko dla nieurzędników.


Większość przyczyn powstawania wartości zerowych można określić jako skutek nieregularnych w danych, które nie chcą się zmieścić w zadanym z góry regularnym formacie, który w przypadku relacyjnego modelu danych jest mocnym założeniem ideologicznym i technicznym. Przykładowo, jeżeli w relacyjnej bazie danych o pracownikach występuje atrybut NazwiskoPanieńskie, to dla mężczyzn i kobiet niezamężnych pojawienie się w tej kolumnie wartości NULL jest konsekwencją tego, że projektant bazy danych przyjął jednolity tablicowy format danych dla wszystkich pracowników, podczas gdy powinien w zasadzie stworzyć odrębną tabelę opisującą kobiety zamężne. Gdyby jednak  projektant dla każdego przypadku, kiedy spodziewa się wystąpienia wartości zerowej, tworzył odrębny format danych, skutkiem byłaby monstrualna eksplozja liczby formatów (np. tabel w relacyjnej bazie danych) i związane z tym ogromne problemy z utrzymaniem bazy danych oraz tworzeniem i pielęgnacją oprogramowania.

Szczególną uwagę poświęca się wartościom zerowym w modelu i systemach relacyjnych, które są oparte na sztywnych formatach danych. Wartości zerowe leżą u podstaw definicji wielu pojęć modelu relacyjnego, takich jak zewnętrzne złączenie (outer join). Okazuje się, że wartości zerowe zachowują się inaczej niż inne wartości, wobec czego wiele operacji w bazie danych musi je uwzględnić w postaci wydzielonej składni i/lub reguły semantycznej.

Wartości zerowe były przedmiotem licznych prac teoretycznych; nie znalazły one jednak istotnych zastosowań. Większość tych prac przyjmowała założenie, że wartość zerowa pojawia się wskutek niekompletnej informacji, co dla przeważającej ilości rzeczywistych przypadków było założeniem fałszywym. Przyczyn powstawania wartości zerowych jest więcej. Na domiar złego przyjmowano, że wartości zerowe będą traktowane wyłącznie przez bardzo ograniczony język zapytań (np. przez tzw. podzbiór SPJ (select-project-join) algebry relacji, co było niezgodne z rzeczywistym zakresem oddziaływania wartości zerowych. Wartości zerowe, o ile zostały wprowadzone jako pojęcie do bazy danych, muszą być uwzględnione we wszystkich zaawansowanych konstrukcjach języka zapytań (takich jak grupowanie, sortowanie, funkcje agregowane, kwantyfikatory itd.), we wszystkich konstrukcjach imperatywnych danego języka (create, update, insert, delete itd.), w interfejsach wiążących język zapytań z językiem programowania, abstrakcjach dotyczących danych (takich jak perspektywy, metody, procedury bazy danych), systemie kontroli typologicznej itd. Na dodatek, próby ustalenia formalnej semantyki dla wartości zerowych okazały się nieudane, gdyż w większości przypadków użytkownik lub programista jest jedynym autorytetem, który potrafi je poprawnie zinterpretować (w sensie ich zewnętrznej "biznesowej" ontologii). Z tego powodu wartości zerowe zyskały sobie sławę "diabełka", który o ile zostanie wprowadzony do systemu zarządzania bazą danych, potrafi rozprzestrzenić się na całe środowisko tworzenia aplikacji, psując przy tym skutecznie spójność, prostotę i efektywność wielu własności tego środowiska.

Doświadczenie pokazuje jednak, że wartości zerowych nie da się uniknąć w żadnym rzeczywistym systemie bazy danych, zaś zastępowanie ich wartościami domyślnymi (default values), jak proponuje Ch. Date, np. 0, spacje lub pusty ciąg,  zwiększa skłonność do błędów. Próby uniknięcia wartości zerowych poprzez specjalizację klas lub tabel (np. utworzenie specjalnej tabeli dla kobiet zamężnych, jeżeli chcemy uniknąć wartości zerowej NazwiskoPanieńskie), co jest postulowane przez niektórych ideologów obiektowości, prowadzą do nienaturalnego, nieczytelnego i niepotrzebnie skomplikowanego schematu bazy danych.

W języku SQL podczas definiowania tablic można ustalić, że pewne ich kolumny mogą zawierać wartości zerowe. Takie kolumny należy rozumieć jako kolumny podwójne: w pierwszej kolumnie są przechowywane normalne wartości, zaś w drugiej informacja (boolowska) wskazująca miejsca, w których są wartości zerowe. To rozdwojenie jest przenoszone dalej na wszystkie interfejsy programistyczne: jeżeli taka wartość ma być podstawiona na zmienną, to należy zadeklarować dwie zmienne, jedną dla wartości, drugą (zwaną indicator variable) dla informacji o wartości zerowej. Język SQL jest wyposażony w szereg cech umożliwiających przetwarzanie tak rozumianych wartości zerowych, m.in. w specjalny predykat (is_null) testujący wystąpienie wartości zerowej, specjalną funkcję (if_null) pozwalającą zastąpić wartość zerową poprzez dowolną inną wartość i specjalne traktowanie wartości zerowych w argumentach funkcji zagregowanych. SQL nie zakłada jakiejkolwiek formalnej semantyki wartości zerowych; są one raczej traktowane jako pewien trik techniczny, który projektant bazy danych lub programista może wykorzystać zgodnie z aktualną potrzebą i pomysłem.

Rozwiązania dotyczące wartości zerowych w SQL są przedmiotem ostrej krytyki (C.J. Date). Chodzi w niej o to, że jak dotąd nie udało się zintegrować wartości zerowych z całością interfejsu programowania baz danych w spójny sposób. Zasada korespondencji, wprowadzona przez autorytety języków programowania, żąda jasnej odpowiedzi na każde pytanie, jak nowe pojęcie wprowadzone do środowiska języka programowania współgra z każdym dotychczas istniejącym pojęciem tego środowiska. Zasada ta jest powszechnie łamana przez twórców systemów relacyjnych, co owocuje wieloma niekonsekwencjami i rafami semantycznymi wytworzonymi dla programistów.  SQL dostarcza wręcz kuriozalnych przykładów łamania zasady korespondencji,  niekonsekwencji i braku elementarnej logiki. Np. w SQL przyjmuje się, ze wszystkie wartości NULL są różne, zatem predykat X=Y, gdzie X oraz Y mają wartość NULL, zwraca UNKNOWN (nieznana wartość); ale operatory group by, distinct oraz order by traktują te wartości jako identyczne, zaś operatory sum, avg, min, max ignorują je tak, jakby były wyłącznie komentarzem. Predykat zdania select po słowie kluczowym where może wprawdzie zwrócić NULL (lub UNKNOWN), ale w takiej sytuacji jest on traktowany jako FALSE - czyli informacja nieznana jest traktowana jako znana. Uderzający jest przykład podany przez Date ([Date86]): jeżeli atrybuty AB relacji R mogą posiadać wartości zerowe, to zapytania:

P4.19.

select sum(A) + sum(B) from R

select sum(A+B) from R

mogą zwrócić różne wyniki. Można sobie wyobrazić, ilu programistów będzie w podobnych sytuacjach daremnie szukać błędu w swoich programach.

Ktoś mógłby powiedzieć, że jest to wyłącznie kwestia błędów projektowych popełnionych przez twórców SQL, które można będzie wyeliminować w następnej wersji tego języka. Okazuje się jednak, że to się dotąd nie udało, mimo kilku kolejnych wersji. Wręcz odwrotnie, w najnowszym standardzie SQL-99 wprowadzono więcej rodzajów wartości zerowych o różnej nieco semantyce oraz więcej konstrukcji semantycznych związanych m.in. z klauzulą group by, co w kombinacji z ogromną liczbą konstrukcji tego języka czyni z niego twór nieprzewidywalny w implementacji. Na niespójności poprzednich wersji nakładane są dalej idące cechy dotyczące wartości zerowych, co nieuchronnie będzie prowadzić do tworzenia dalszych niespójności. Przekonają się o tym ci, którzy będą ten język implementować i używać (co - zważywszy na wady specyfikacji SQL-99 -  miejmy nadzieję, nigdy nie nastąpi).

Wielu autorów próbowało uporządkować sprawę wartości zerowych. W końcowej konkluzji należy jednak przyjąć tezę Ch.Date, który twierdzi, że zło nie tkwi w nieadekwatnym projekcie środowiska tworzenia bazodanowych aplikacji, lecz jest ulokowane głębiej - w samym pojęciu wartości zerowej. Generalnie Ch.Date konkluduje "wartość zerowa wprowadza znacznie więcej problemów niż rozwiązuje", wobec czego z tego pojęcia należy całkowicie zrezygnować. Postuluje przy tym zastąpienie wartości zerowych wartościami domyślnymi (default values), ale to rozwiązanie w wielu sytuacjach również prowadzi do wad i jest przez to nieakceptowalne.

Temat wartości zerowych lub inaczej nieregularności w danych odrodził się ostatnio w nowym sformułowaniu w wersji tzw. danych półstrukturalnych (semi-structured data), koncepcji języka XML oraz związanych z nim technologii. Wydaje się, że nadal występuje zawężenie zakresu, w którym jest rozpatrywany problem nieregularności w danych, do dość ograniczonych języków zapytań. Niesposób sobie wyobrazić, że jakikolwiek system, w tym system z danymi półstrukturalnymi, byłby ograniczony wyłącznie do języka zapytań. Dane półstrukturalne muszą być przecież wprowadzane, pielęgnowane, aktualizowane,  przetwarzane, wyprowadzane; zatem muszą działać na nich aplikacje realizowane w językach z pełną mocą algorytmiczną. To stwarza przesłanki, aby twierdzić, że opisane problemy z wartościami zerowymi odrodzą się w przyszłych technologiach, językach i środowiskach tworzenia aplikacji i dotyczy to zarówno tematu danych półstrukturalnych, jak i technologii opartych na XML.

Istotą pomysłu w zakresie wartości zerowych jest wprowadzenie specjalnej kolekcji, która może przyjmować wyłącznie dwa stany: albo jest pusta, albo zawiera jeden element. Poza tym ograniczeniem kolekcja ta podlegać będzie wszelkim standardowym regułom przetwarzania kolekcji. W związku z tym, do przetwarzania opisanej wyżej specyficznej kolekcji można będzie użyć tych samych operatorów. Pomysł ten nie implikuje żadnych nowych operatorów lub własności środowiska programowania aplikacji, wobec czego jest semantycznie czysty.

Kolekcje, które mogą przyjmować dwa stany albo być puste, albo zawierać dokładnie jeden element, będziemy nazywać opcją.

Niech np. atrybut Stan obiektów Pracownik będzie zadeklarowany jako opcja. Jeżeli dla Kowalskiego stanowisko nie jest znane, wówczas ta kolekcja będzie pusta, natomiast jeżeli jest znany, wówczas będzie zawierać jeden element, np. string "projektant". Jeżeli opcja jest pusta, wówczas można ją zapełnić wartością używając np. operatora insert (znanego np. z SQL). Odpowiada to sytuacji, kiedy na wartość zerową podstawiamy wartość niezerową. Jeżeli opcja nie jest pusta, to możemy uczynić ją pustą używając np. operatora delete (również znanego z SQL). Opowiada to podstawieniu wartości zerowej na wartość niezerową; np. może być zastosowane w sytuacji, gdy Kowalski utracił uprawnienia projektanta i aktualnie nie posiada stanowiska.

Dalsze konsekwencje tego pomysłu również oznaczają uporządkowanie wielu koncepcji. Np. pusty zbiór oznaczymy {}, równość zbiorów oznaczymy =.  Przyjmiemy, że wszystkie puste zbiory są sobie równe. Wówczas zdanie (w rozszerzonym SQL):

P4.20.

select * from Pracownik where Stan = {}

oznacza wyszukanie wszystkich pracowników bez stanowiska. Zdanie

P4.21

select * from Pracownik where Stan = "projektant"

jest typologicznie niepoprawne, gdyż porównuje się zbiór ze stringiem. Natomiast poprawne są zdania:

P4.22.

select * from Pracownik where Stan = {"projektant"}

select * from Pracownik where $ Stan (z = "projektant")

select * from Pracownik where " z Stan (z = "projektant")

Z powyższych trzech zdań pierwsze jest równoważne drugiemu; oba wyszukują pracowników, którzy z pewnością są projektantami. Trzecie zdanie natomiast wyszukuje pracowników, którzy albo są projektantami, albo nie posiadają stanowiska.

Pomysł można obudować bardziej przyjazną składnią, aby użytkownik nie miał wrażenia, że jest "zbyt matematyczny". Jak dotąd, nie rozpatrywano w literaturze sytuacji, w której wartość złożona także może być zerowa. Opisany wyżej pomysł automatycznie obejmuje tę sytuację. Niech np. AdresPracownika będzie złożonym atrybutem. Jeżeli projektant liczy się z tym, że może być nieznany, może wprowadzić ten adres jako opcję.

Można pokazać na przykładach, że pomysł ten jest uniwersalny: jeżeli tylko pewne środowisko tworzenia aplikacji umożliwia przetwarzanie dowolnie zagnieżdżonych kolekcji, wówczas jest całkowicie przygotowane do przetwarzania tak rozumianych wartości zerowych. Dla uproszenia schematu bazy danych można także w każdej konkretnej sytuacji rozważyć, czy zastosować ten pomysł czy raczej skorzystać z wartości domyślnych; np. dla atrybutu Stan wartością domyślną (odpowiadającą wartości NULL ) mógłby być ciąg spacji. Istotą tych koncepcji jest to, że specjalna wartość NULL staje się niepotrzebna, przez co znikają wszystkie semantyczne problemy związane z tą wartością.

Copyrights © 2006 PJWSTK
Materiały zostały opracowane w PJWSTK w projekcie współfinansowanym ze środków EFS.