Funkcjonalna DBMS

Ś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 artykułu 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 artykułu. 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 dss_kalika:

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 platforma. 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

Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS 🔥 Kup solidny hosting stron z ochroną przed DDoS, serwery VPS VDS | ProHoster