Świat baz danych od dawna zdominowany jest przez relacyjne systemy zarządzania bazami danych, w których wykorzystywany jest język SQL. Tak bardzo, że pojawiające się rodzaje nazywane są NoSQL. Udało im się zdobyć pewne miejsce na tym rynku, ale relacyjne systemy zarządzania bazami danych nie zamierzają umierać i są nadal aktywnie wykorzystywane do swoich celów.
W tym artykule chcę opisać koncepcję funkcjonalnej bazy danych. Dla lepszego zrozumienia, uczynię to poprzez porównanie z klasycznym modelem relacyjnym. Jako przykłady zostaną wykorzystane zadania z różnych testów SQL znalezionych w internecie.
Wprowadzenie
Relacyjne bazy danych operują na tabelach i polach. W funkcjonalnej bazie danych zamiast nich będą wykorzystywane klasy i funkcje odpowiednio. Pole w tabeli z N kluczami będzie reprezentowane jako funkcja od N parametrów. Zamiast relacji między tabelami będą wykorzystywane funkcje, które zwracają obiekty klasy, z którą jest relacja. Zamiast JOIN wykorzystywana będzie kompozycja funkcji.
Zanim przejdziemy bezpośrednio do zadań, opiszę zadanie logiki domeny. Dla DDL będę używał składni PostgreSQL. Dla funkcyjnej swojej składni.
Tabele i pola
Prosty obiekt Sku z polami nazwa i cena:
Grafowy, dokumentowy
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
Funkcjonalna
KLASA Sku;
nazwa = DANY STRING[100] (Sku);
cena = DANE NUMERYCZNE[10,5] (Sku);
Deklarujemy dwie funkcje, które przyjmują jeden parametr Sku i zwracają typ prymitywny.
Zakłada się, że w funkcjonalnej bazie danych każdy obiekt będzie miał pewien wewnętrzny kod, który jest automatycznie generowany i do którego można się odwołać w razie potrzeby.
Ustalmy cenę dla towaru / sklepu / dostawcy. Może ona zmieniać się w czasie, więc dodamy do tabeli pole czas. Deklarację tabel dla słowników w relacyjnej bazie danych pominę, aby skrócić kod:
Grafowy, dokumentowy
CREATE TABLE prices
(
skuId bigint NOT NULL,
storeId bigint NOT NULL,
supplierId bigint NOT NULL,
dateTime timestamp without time zone,
price numeric(10,5),
CONSTRAINT prices_pkey PRIMARY KEY (skuId, storeId, supplierId)
)
Funkcjonalna
KLASA Sku;
KLASA Sklep;
KLASA Dostawca;
dataCzas = DANE DATETIME (Sku, Sklep, Dostawca);
cena = DANE NUMERYCZNE[10,5] (Sku, Sklep, Dostawca);
Indeksy
Dla ostatniego przykładu zbudujemy indeks na wszystkich kluczach i dacie, aby można było szybko znaleźć cenę na określony czas.
Grafowy, dokumentowy
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Funkcjonalna
INDEX Sku sk, Sklep st, Dostawca sp, dateTime(sk, st, sp);
Zadania
Zacznijmy od stosunkowo prostych zadań, wziętych z odpowiednich na Habrze.
Na początku ogłosimy logikę domenową (dla bazy relacyjnej jest to zrobione bezpośrednio w podanym artykule).
Klasa Departament;
nazwa = DANE CIĄG[100] (Departament);
KLASA Pracownik;
departament = DANE Departament (Pracownik);
szef = DANE Pracownik (Pracownik);
nazwa = DANE STRING[100] (Pracownik);
wynagrodzenie = DANE NUMERYCZNY[14,2] (Pracownik);
Zadanie 1.1
Wyświetl listę pracowników, których wynagrodzenie jest wyższe niż bezpośredniego przełożonego.
Grafowy, dokumentowy
select a.*
from pracownik a, pracownik b
where b.id = a.chief_id
and a.wynagrodzenie > b.wynagrodzenie
Funkcjonalna
WYBIERZ nazwę (Pracownik a) GDZIE wynagrodzenie(a) > wynagrodzenie(szef(a));
Zadanie 1.2
Wyświetl listę pracowników, którzy otrzymują maksymalne wynagrodzenie w swoim dziale.
Grafowy, dokumentowy
select a.*
from pracownik a
where a.wynagrodzenie = ( select max(wynagrodzenie) from pracownik b
where b.department_id = a.department_id )
Funkcjonalna
maxSalary 'Maksymalna pensja' (Działy) =
GROUP MAX pensja (Pracownik e) IF dział(e) = s;
SELECT name(Pracownik a) WHERE salary(a) = maxSalary(dział(a));
// или если "заинлайнить"
SELECT nazwa(Pracownik a) WHERE
wynagrodzenie(a) = maksWynagrodzenie(GRUPA MAX wynagrodzenie(Pracownik e) IF dział(e) = dział(a));
Obie realizacje są równoważne. W pierwszym przypadku w bazie relacyjnej można użyć CREATE VIEW, który w ten sposób najpierw obliczy maksymalne wynagrodzenie w danym dziale. W dalszej części dla przejrzystości będę się posługiwał pierwszym przypadkiem, ponieważ lepiej oddaje rozwiązanie.
Zadanie 1.3
Wyświetl listę ID działów, w których liczba pracowników nie przekracza 3 osób.
Grafowy, dokumentowy
select department_id
from pracownik
group by department_id
having count(*) <= 3
Funkcjonalna
countEmployees 'Liczba pracowników' (Dział d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Dział d WHERE countEmployees(d) <= 3;
Zadanie 1.4
Wyświetl listę pracowników, którzy nie mają przypisanego przełożonego, pracującego w tym samym dziale.
Grafowy, dokumentowy
select a.*
from pracownik a
left join pracownik b on (b.id = a.chief_id and b.department_id = a.department_id)
where b.id is null
Funkcjonalna
WYBIERZ nazwisko(Pracownik a) GDZIE NIE (departament(szef(a)) = departament(a));
Zadanie 1.5
Znajdź listę ID działów z maksymalną sumą wynagrodzeń pracowników.
Grafowy, dokumentowy
with suma_wynagrodzenia as
( select department_id, sum(wynagrodzenie) wynagrodzenie
from pracownik
group by department_id )
select department_id
from suma_wynagrodzenia a
where a.wynagrodzenie = ( select max(wynagrodzenie) from suma_wynagrodzenia )
Funkcjonalna
wynagrodzenieSum 'Maksymalne wynagrodzenie' (Departament d) =
GROUP SUM salary(Employee e) IF department(e) = d;
maxWynagrodzenieSum 'Maksymalne wynagrodzenie departamentów' () =
GRUPA MAX wynagrodzenieSum(Departament d);
WYBIERZ Departament d GDZIE wynagrodzenieSum(d) = maxWynagrodzenieSum();
Przejdźmy do bardziej złożonych zadań z innej . Zawiera szczegółową analizę tego, jak realizować to zadanie na MS SQL.
Zadanie 2.1
Jacy sprzedawcy sprzedali w 1997 roku więcej niż 30 sztuk towaru nr 1?
Logika domenowa (jak wcześniej na RDBMS pomijamy deklarację):
CLASS Employee 'Sprzedawca';
lastName 'Nazwisko' = DATA STRING[100] (Employee);
KLASA Produkt 'Produkt';
id = DANE CAŁKOWITY (Produkt);
nazwa = DANE STRING[100] (Produkt);
CLASS Order 'Zamówienie';
data = DANE DATA (Zamówienie);
pracownik = DANE Pracownik (Zamówienie);
KLASA Szczegół 'Wiersz zamówienia';
zamówienie = DANE Zamówienie (Szczegół);
produkt = DANE Produkt (Szczegół);
ilość = DANE NUMERYCZNY[10,5] (Szczegół);
Grafowy, dokumentowy
select Nazwisko
from Pracownicy as e
where (
select sum(od.Ilość)
from [Szczegóły zamówienia] as od
where od.ProduktID = 1 and od.ZamówienieID in (
select o.ZamówienieID
from Zamówienia as o
where year(o.DataZamówienia) = 1997 and e.PracownikID = o.PracownikID)
) > 30
Funkcjonalna
sprzedane (Pracownik e, INTEGER idProduktu, INTEGER rok) =
GRUPA SUMA ilość(SzczegółZamówienia d) JEŚLI
pracownik(zam(order(d))) = e ORAZ
id(produkt(d)) = idProduktu ORAZ
wyciągnijRok(data(zam(order(d)))) = rok;
WYBIERZ nazwisko(Pracownik e) GDY sprzedane(e, 1, 1997) > 30;
Zadanie 2.2
Dla każdego klienta (imię, nazwisko) znajdź dwa produkty (nazwa), na które klient wydał najwięcej pieniędzy w roku 1997.
Rozszerzamy logikę domenową z poprzedniego przykładu:
CLASS Customer 'Klient';
contactName 'Imię i nazwisko' = DATA STRING[100] (Customer);
klient = DATA Klient (Zamówienie);
cenaJednostkowa = DATA NUMERYCZNY[14,2] (Szczegół);
rabat = DATA NUMERYCZNY[6,2] (Szczegół);
Grafowy, dokumentowy
WYBIERZ NazwaKontaktu, NazwaProduktu Z (
WYBIERZ c.NazwaKontaktu, p.NazwaProduktu
, ROW_NUMBER() OVER (
PARTYCJA PRZEZ c.NazwaKontaktu
ZAMÓW SUM(od.Ilość * od.CenaJednostkowa * (1 - od.Rabat)) DESC
) AS OcenaWedługKwoty
Z Klienci c
JOIN Zamówienia o ON o.IDKlienta = c.IDKlienta
JOIN [Szczegóły Zamówienia] od ON od.IDZamówienia = o.IDZamówienia
JOIN Produkty p ON p.IDProduktu = od.IDProduktu
GDZIE ROK(o.DataZamówienia) = 1997
GRUPOWANE PRZEZ c.NazwaKontaktu, p.NazwaProduktu
) t
GDZIE OcenaWedługKwoty < 3
Funkcjonalna
suma (Szczegół d) = ilość(d) * cenaJednostkowa(d) * (1 - rabat(d));
kupiony 'Kupiony' (Klient c, Produkt p, LICZBA y) =
SUMA GRUPY sum(Szczegół d) IF
klient(zamówienie(d)) = c I
produkt(d) = p I
wyciągnijRok(data(zamówienie(d))) = y;
ocena 'Ocena' (Klient c, Produkt p, LICZBA y) =
PARTYCJA SUMA 1 PORZĄDEK DESC zakupiono(c, p, y), p GRUPUJ według c, y;
WYBIERZ contactName(Klient c), name(Produkt p) GDZIE ocena(c, p, 1997) < 3;
Operator PARTITION działa na zasadzie: sumuje wyrażenie podane po SUM (tutaj 1), wewnątrz określonych grup (tutaj Klient i Rok, ale może to być dowolne wyrażenie), sortując wewnątrz grup według wyrażeń podanych w ORDER (tutaj kupiony, a w przypadku równości według wewnętrznego kodu produktu).
Zadanie 2.3
Ile produktów należy zamówić u dostawców, aby zrealizować bieżące zamówienia.
Ponownie rozszerzamy logikę domenową:
CLASS Supplier 'Dostawca';
companyName = DATA STRING[100] (Dostawca);
dostawca = DATA Dostawca (Produkt);
jednostkiWMagazynie 'Stan magazynu' = DATA NUMERYCZNY[10,3] (Produkt);
poziomZamówienia 'Norma sprzedaży' = DATA NUMERYCZNY[10,3] (Produkt);
Grafowy, dokumentowy
wybierz s.NazwaFirmy, p.NazwaProduktu, SUM(od.Ilość) + p.PoziomZamówienia — p.JednostkiWMagazynie jako DoZamówienia
z Zamówienia o
join [Szczegóły Zamówienia] od on o.IDZamówienia = od.IDZamówienia
join Produkty p on od.IDProduktu = p.IDProduktu
join Dostawcy s on p.IDDostawcy = s.IDDostawcy
gdzie o.DataWysłania jest null
grupowanie według s.NazwaFirmy, p.NazwaProduktu, p.JednostkiWMagazynie, p.PoziomZamówienia
mając p.JednostkiWMagazynie < SUM(od.Ilość) + p.PoziomZamówienia
Funkcjonalna
zamówioneNieWysłane 'Zamówione, ale nie wysłane' (Produkt p) =
SUMA GRUPY ilość(DaneZamówienia d) JEŚLI produkt(d) = p;
doZamówienia 'Do zamówienia' (Produkt p) = zamówioneNieWysłane(p) + poziomZamówienia(p) - jednostkiWMagazynie(p);
SELECT nazwaFirmy(dostawca(Produkt p)), nazwa(p), doZamówienia(p) GDZIE doZamówienia(p) > 0;
Zadanie z gwiazdką
I ostatni przykład ode mnie. Jest logika sieci społecznościowej. Ludzie mogą przyjaźnić się oraz lubić nawzajem. Z punktu widzenia funkcjonalnej bazy danych wyglądałoby to następująco:
KLASA Osoba;
lubi = DANE BOOLEAN (Osoba, Osoba);
przyjaciele = DANE BOOLEAN (Osoba, Osoba);
Należy znaleźć potencjalnych kandydatów do przyjaźni. Bardziej formalnie należy znaleźć wszystkich ludzi A, B, C takich, że A przyjaźni się z B, a B przyjaźni się z C, A lubi C, ale A nie przyjaźni się z C.
Z punktu widzenia funkcjonalnej bazy danych zapytanie wyglądałoby następująco:
WYBIERZ Osobę a, Osobę b, Osobę c GDZIE
lubi(a, c) I NIE przyjaciele(a, c) I
przyjaciele(a, b) I przyjaciele(b, c);
Czytelnik jest zachęcany do samodzielnego rozwiązania tego zadania w SQL. Zakłada się, że przyjaciół jest znacznie mniej niż tych, którzy się lubią. Dlatego są one zawarte w osobnych tabelach. W przypadku pomyślnego rozwiązania jest także zadanie z dwoma gwiazdkami. W nim przyjaźń nie jest symetryczna. W funkcjonalnej bazie danych wyglądałoby to tak:
WYBIERZ Osobę a, Osobę b, Osobę c GDZIE
lubi(a, c) I NIE przyjaciele(a, c) I
(znajomi(a, b) LUB znajomi(b, a)) I
(znajomi(b, c) LUB znajomi(c, b));
UPD: rozwiązanie zadania z pierwszą i drugą gwiazdką od :
WYBIERZ
pl.PersonAID
,pf.PersonAID
,pff.PersonAID
Z Osób JAKO p
--Polubienia
DOŁĄCZ DO OsobyRelacji JAKO pl ON pl.PersonAID = p.PersonID
I pl.Relacja = 'Polubienie'
--Przyjaciele
DOŁĄCZ DO OsobyRelacji JAKO pf ON pf.PersonAID = p.PersonID
I pf.Relacja = 'Przyjaciel'
--Przyjaciele Przyjaciół
DOŁĄCZ DO OsobyRelacji JAKO pff ON pff.PersonAID = pf.PersonBID
I pff.PersonBID = pl.PersonBID
I pff.Relacja = 'Przyjaciel'
--Jeszcze nie są przyjaciółmi
LEWY DOŁĄCZ DO OsobyRelacji AS pnf ON pnf.PersonAID = p.PersonID
I pnf.PersonBID = pff.PersonBID
I pnf.Relacja = 'Przyjaciel'
GDZIE pnf.PersonAID JEST NULL
;Z WIDOKIEM OsobyRelacjiZłożonej JAKO (
WYBIERZ pl.PersonAID
,pl.PersonBID
,pl.Relacja
Z #OsobyRelacji JAKO pl
UNIA
WYBIERZ pl.PersonBID JAKO PersonAID
,pl.PersonAID JAKO PersonBID
,pl.Relacja
Z #OsobyRelacji JAKO pl
)
WYBIERZ
pl.PersonAID
,pf.PersonBID
,pff.PersonBID
Z #Osób JAKO p
--Polubienia
DOŁĄCZ DO OsobyRelacjiZłożonej JAKO pl ON pl.PersonAID = p.PersonID
I pl.Relacja = 'Polubienie'
--Przyjaciele
DOŁĄCZ DO OsobyRelacjiZłożonej JAKO pf ON pf.PersonAID = p.PersonID
I pf.Relacja = 'Przyjaciel'
--Przyjaciele Przyjaciół
DOŁĄCZ DO OsobyRelacjiZłożonej JAKO pff ON pff.PersonAID = pf.PersonBID
I pff.PersonBID = pl.PersonBID
I pff.Relacja = 'Przyjaciel'
--Jeszcze nie są przyjaciółmi
LEWY DOŁĄCZ DO OsobyRelacjiZłożonej AS pnf ON pnf.PersonAID = p.PersonID
I pnf.PersonBID = pff.PersonBID
I pnf.Relacja = 'Przyjaciel'
GDZIE pnf.[PersonAID] JEST NULL
Podsumowanie
Należy zauważyć, że podany składnia językowa to tylko jedna z możliwych realizacji przedstawionej koncepcji. Jako podstawę wzięto SQL, a celem było, aby był on jak najbardziej do niego podobny. Oczywiście, niektórzy mogą nie polubić nazw słów kluczowych, rejestrów słów itp. Tutaj głównym celem jest sama koncepcja. W razie potrzeby można stworzyć składnię podobną do C++ lub Pythona.
Opisany koncept bazy danych, moim zdaniem, ma następujące zalety:
- Prostota. Jest to względnie subiektywny wskaźnik, który nie jest oczywisty w prostych przypadkach. Ale jeśli spojrzymy na bardziej skomplikowane przypadki (na przykład zadania ze gwiazdkami), to uważam, że pisanie takich zapytań jest znacznie łatwiejsze.
- Enkapsulacja. W niektórych przykładach ogłaszałem funkcje pośrednie (na przykład, sprzedano, kupiono itd.), z których budowano kolejne funkcje. Pozwala to na zmianę logiki określonych funkcji bez zmiany logiki funkcji od nich zależnych. Na przykład, można by umożliwić, aby sprzedaże sprzedano były uważane za zupełnie inne obiekty, przy czym pozostała logika nie zmieni się. Tak, w RDBMS można to zrealizować za pomocą CREATE VIEW. Ale jeśli całą logikę pisać w ten sposób, będzie wyglądać dość nieczytelnie.
- Brak luk semantycznych. Taka baza danych operuje funkcjami i klasami (zamiast tabel i pól). Dokładnie tak samo jak w klasycznym programowaniu (jeśli uznać, że metoda to funkcja z pierwszym parametrem w postaci klasy, do której się odnosi). W związku z tym, „połączenie” z uniwersalnymi językami programowania powinno być znacznie prostsze. Ponadto koncepcja ta umożliwia wdrażanie znacznie bardziej złożonych funkcji. Na przykład można wprowadzać do bazy danych operatory w rodzaju:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Petya' MESSAGE 'Coś Petya sprzedaje zbyt wiele jednego towaru w 2019 roku';
- Dziedziczenie i polimorfizm. W funkcjonalnej bazie danych można wprowadzić wielokrotne dziedziczenie za pomocą konstrukcji CLASS ClassP: Class1, Class2 i wdrożyć wielokrotny polimorfizm. Jak dokładnie, być może napiszę w następnych artykułach.
Mimo że to tylko koncepcja, mamy już pewną realizację w Javie, która przekształca całą logikę funkcjonalną w logikę relacyjną. Dodatkowo pięknie zintegrowana jest logika prezentacji i wiele innych elementów, dzięki czemu powstaje cała . Zasadniczo używamy RDBMS (na razie tylko PostgreSQL) jako „wirtualnej maszyny”. Przy takiej transformacji czasami występują problemy, ponieważ optymalizator zapytań RDBMS nie zna pewnych statystyk, które zna FDBMS. Teoretycznie można wdrożyć system zarządzania bazą danych, który użyje jako magazynu jakiejś struktury, dostosowanej specjalnie do logiki funkcjonalnej.
Źródło: habr.com
