Funkcjonalny system zarządzania bazą danych

Świat baz danych od dawna jest zdominowany przez relacyjne systemy DBMS, które korzystają z języka SQL. Do tego stopnia, że ​​pojawiające się warianty nazywane są NoSQL. Udało im się wywalczyć dla siebie pewne miejsce na tym rynku, ale relacyjne systemy DBMS nie umrą i nadal będą aktywnie wykorzystywane do swoich celów.

W tym artykule chcę opisać koncepcję funkcjonalnej bazy danych. Dla lepszego zrozumienia zrobię to porównując to z klasycznym modelem relacyjnym. Jako przykłady zostaną wykorzystane problemy z różnych testów SQL znalezionych w Internecie.

Wprowadzenie

Relacyjne bazy danych działają na tabelach i polach. W funkcjonalnej bazie danych zamiast tego będą używane odpowiednio klasy i funkcje. Pole w tabeli z N kluczami będzie reprezentowane jako funkcja N parametrów. Zamiast relacji między tabelami zostaną użyte funkcje zwracające obiekty klasy, z którą następuje połączenie. Zamiast JOIN zostanie użyta kompozycja funkcji.

Zanim przejdę bezpośrednio do zadań opiszę zadanie logiki dziedzinowej. W przypadku DDL użyję składni PostgreSQL. W przypadku funkcjonalności ma swoją własną składnię.

Tabele i pola

Prosty obiekt Sku z polami nazwy i ceny:

Relacyjny

CREATE TABLE Sku
(
    id bigint NOT NULL,
    name character varying(100),
    price numeric(10,5),
    CONSTRAINT id_pkey PRIMARY KEY (id)
)

Funkcjonalne

CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);

Ogłaszamy dwa funkcje, które przyjmują jeden parametr Sku jako dane wejściowe i zwracają typ pierwotny.

Zakłada się, że w funkcjonalnym systemie DBMS każdy obiekt będzie miał wewnętrzny kod, który jest generowany automatycznie i będzie można uzyskać do niego dostęp w razie potrzeby.

Ustalmy cenę dla produktu/sklepu/dostawcy. Z biegiem czasu może się to zmieniać, dlatego dodajmy do tabeli pole czasu. Pominę deklarowanie tabel dla katalogów w relacyjnej bazie danych, aby skrócić kod:

Relacyjny

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)
)

Funkcjonalne

CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);

wskaźniki

W ostatnim przykładzie zbudujemy indeks na wszystkich kluczach i dacie, abyśmy mogli szybko znaleźć cenę na konkretny czas.

Relacyjny

CREATE INDEX prices_date
    ON prices
    (skuId, storeId, supplierId, dateTime)

Funkcjonalne

INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);

zadania

Zacznijmy od stosunkowo prostych problemów wziętych z odpowiednich Artykuł na Habr.

Najpierw zadeklarujmy logikę domeny (w przypadku relacyjnej bazy danych robi się to bezpośrednio w powyższym artykule).

CLASS Department;
name = DATA STRING[100] (Department);

CLASS Employee;
department = DATA Department (Employee);
chief = DATA Employee (Employee);
name = DATA STRING[100] (Employee);
salary = DATA NUMERIC[14,2] (Employee);

Zadanie 1.1

Wyświetl listę pracowników, którzy otrzymują wynagrodzenie wyższe niż wynagrodzenie ich bezpośredniego przełożonego.

Relacyjny

select a.*
from   employee a, employee b
where  b.id = a.chief_id
and    a.salary > b.salary

Funkcjonalne

SELECT name(Employee a) WHERE salary(a) > salary(chief(a));

Zadanie 1.2

Wymień pracowników, którzy otrzymują maksymalne wynagrodzenie w swoim dziale

Relacyjny

select a.*
from   employee a
where  a.salary = ( select max(salary) from employee b
                    where  b.department_id = a.department_id )

Funkcjonalne

maxSalary 'Максимальная зарплата' (Department s) = 
    GROUP MAX salary(Employee e) IF department(e) = s;
SELECT name(Employee a) WHERE salary(a) = maxSalary(department(a));

// или если "заинлайнить"
SELECT name(Employee a) WHERE 
    salary(a) = maxSalary(GROUP MAX salary(Employee e) IF department(e) = department(a));

Obie implementacje są równoważne. W pierwszym przypadku w relacyjnej bazie danych można skorzystać z opcji CREATE VIEW, która w ten sam sposób najpierw obliczy maksymalne wynagrodzenie dla konkretnego znajdującego się w niej działu. W dalszej części dla przejrzystości posłużę się pierwszym przypadkiem, ponieważ lepiej odzwierciedla on rozwiązanie.

Zadanie 1.3

Wyświetl listę identyfikatorów działów, w których liczba pracowników nie przekracza 3 osób.

Relacyjny

select department_id
from   employee
group  by department_id
having count(*) <= 3

Funkcjonalne

countEmployees 'Количество сотрудников' (Department d) = 
    GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;

Zadanie 1.4

Wyświetl listę pracowników, którzy nie mają wyznaczonego menedżera pracującego w tym samym dziale.

Relacyjny

select a.*
from   employee a
left   join employee b on (b.id = a.chief_id and b.department_id = a.department_id)
where  b.id is null

Funkcjonalne

SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));

Zadanie 1.5

Znajdź listę identyfikatorów działów z maksymalną łączną pensją pracownika.

Relacyjny

with sum_salary as
  ( select department_id, sum(salary) salary
    from   employee
    group  by department_id )
select department_id
from   sum_salary a       
where  a.salary = ( select max(salary) from sum_salary )

Funkcjonalne

salarySum 'Максимальная зарплата' (Department d) = 
    GROUP SUM salary(Employee e) IF department(e) = d;
maxSalarySum 'Максимальная зарплата отделов' () = 
    GROUP MAX salarySum(Department d);
SELECT Department d WHERE salarySum(d) = maxSalarySum();

Przejdźmy do bardziej złożonych zadań z innego Artykuł. Zawiera szczegółową analizę sposobu realizacji tego zadania w MS SQL.

Zadanie 2.1

Którzy sprzedawcy sprzedali w 1997 roku więcej niż 30 sztuk produktu nr 1?

Logika domeny (tak jak poprzednio w RDBMS pomijamy deklarację):

CLASS Employee 'Продавец';
lastName 'Фамилия' = DATA STRING[100] (Employee);

CLASS Product 'Продукт';
id = DATA INTEGER (Product);
name = DATA STRING[100] (Product);

CLASS Order 'Заказ';
date = DATA DATE (Order);
employee = DATA Employee (Order);

CLASS Detail 'Строка заказа';

order = DATA Order (Detail);
product = DATA Product (Detail);
quantity = DATA NUMERIC[10,5] (Detail);

Relacyjny

select LastName
from Employees as e
where (
  select sum(od.Quantity)
  from [Order Details] as od
  where od.ProductID = 1 and od.OrderID in (
    select o.OrderID
    from Orders as o
    where year(o.OrderDate) = 1997 and e.EmployeeID = o.EmployeeID)
) > 30

Funkcjonalne

sold (Employee e, INTEGER productId, INTEGER year) = 
    GROUP SUM quantity(OrderDetail d) IF 
        employee(order(d)) = e AND 
        id(product(d)) = productId AND 
        extractYear(date(order(d))) = year;
SELECT lastName(Employee e) WHERE sold(e, 1, 1997) > 30;

Zadanie 2.2

Dla każdego kupującego (imię, nazwisko) znajdź dwa dobra (imię), na które kupujący wydał najwięcej pieniędzy w 1997 roku.

Rozszerzamy logikę domeny z poprzedniego przykładu:

CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);

customer = DATA Customer (Order);

unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);

Relacyjny

SELECT ContactName, ProductName FROM (
SELECT c.ContactName, p.ProductName
, ROW_NUMBER() OVER (
    PARTITION BY c.ContactName
    ORDER BY SUM(od.Quantity * od.UnitPrice * (1 - od.Discount)) DESC
) AS RatingByAmt
FROM Customers c
JOIN Orders o ON o.CustomerID = c.CustomerID
JOIN [Order Details] od ON od.OrderID = o.OrderID
JOIN Products p ON p.ProductID = od.ProductID
WHERE YEAR(o.OrderDate) = 1997
GROUP BY c.ContactName, p.ProductName
) t
WHERE RatingByAmt < 3

Funkcjonalne

sum (Detail d) = quantity(d) * unitPrice(d) * (1 - discount(d));
bought 'Купил' (Customer c, Product p, INTEGER y) = 
    GROUP SUM sum(Detail d) IF 
        customer(order(d)) = c AND 
        product(d) = p AND 
        extractYear(date(order(d))) = y;
rating 'Рейтинг' (Customer c, Product p, INTEGER y) = 
    PARTITION SUM 1 ORDER DESC bought(c, p, y), p BY c, y;
SELECT contactName(Customer c), name(Product p) WHERE rating(c, p, 1997) < 3;

Operator PARTITION działa na następującej zasadzie: sumuje wyrażenie określone po SUM (tutaj 1), w obrębie określonych grup (tu Klient i Rok, ale może to być dowolne wyrażenie), sortując w ramach grup według wyrażeń określonych w ZAMÓWIENIU ( tutaj kupiony, a jeśli jest taki sam, to zgodnie z wewnętrznym kodem produktu).

Zadanie 2.3

Ile towarów należy zamówić u dostawców, aby zrealizować bieżące zamówienia.

Rozwińmy ponownie logikę domeny:

CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);

supplier = DATA Supplier (Product);

unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);

Relacyjny

select s.CompanyName, p.ProductName, sum(od.Quantity) + p.ReorderLevel — p.UnitsInStock as ToOrder
from Orders o
join [Order Details] od on o.OrderID = od.OrderID
join Products p on od.ProductID = p.ProductID
join Suppliers s on p.SupplierID = s.SupplierID
where o.ShippedDate is null
group by s.CompanyName, p.ProductName, p.UnitsInStock, p.ReorderLevel
having p.UnitsInStock < sum(od.Quantity) + p.ReorderLevel

Funkcjonalne

orderedNotShipped 'Заказано, но не отгружено' (Product p) = 
    GROUP SUM quantity(OrderDetail d) IF product(d) = p;
toOrder 'К заказу' (Product p) = orderedNotShipped(p) + reorderLevel(p) - unitsInStock(p);
SELECT companyName(supplier(Product p)), name(p), toOrder(p) WHERE toOrder(p) > 0;

Problem z gwiazdką

I ostatni przykład pochodzi ode mnie osobiście. Istnieje logika sieci społecznościowej. Ludzie mogą się przyjaźnić i lubić. Z punktu widzenia funkcjonalnej bazy danych wyglądałoby to tak:

CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);

Konieczne jest znalezienie potencjalnych kandydatów do przyjaźni. Mówiąc bardziej formalnie, musisz znaleźć wszystkie osoby A, B, C takie, ż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:

SELECT Person a, Person b, Person c WHERE 
    likes(a, c) AND NOT friends(a, c) AND 
    friends(a, b) AND friends(b, c);

Czytelnika zachęcamy do samodzielnego rozwiązania tego problemu w języku SQL. Zakłada się, że przyjaciół jest znacznie mniej niż osób, które lubisz. Dlatego znajdują się one w oddzielnych tabelach. Jeśli się powiedzie, pojawi się również zadanie z dwiema gwiazdkami. W nim przyjaźń nie jest symetryczna. W funkcjonalnej bazie danych wyglądałoby to tak:

SELECT Person a, Person b, Person c WHERE 
    likes(a, c) AND NOT friends(a, c) AND 
    (friends(a, b) OR friends(b, a)) AND 
    (friends(b, c) OR friends(c, b));

UPD: rozwiązanie problemu z pierwszą i drugą gwiazdką z dss_kalika:

SELECT 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
FROM Persons                 AS p
--Лайки                      
JOIN PersonRelationShip      AS pl ON pl.PersonAID = p.PersonID
                                  AND pl.Relation  = 'Like'
--Друзья                     
JOIN PersonRelationShip      AS pf ON pf.PersonAID = p.PersonID 
                                  AND pf.Relation = 'Friend'
--Друзья Друзей              
JOIN PersonRelationShip      AS pff ON pff.PersonAID = pf.PersonBID
                                   AND pff.PersonBID = pl.PersonBID
                                   AND pff.Relation = 'Friend'
--Ещё не дружат         
LEFT JOIN PersonRelationShip AS pnf ON pnf.PersonAID = p.PersonID
                                   AND pnf.PersonBID = pff.PersonBID
                                   AND pnf.Relation = 'Friend'
WHERE pnf.PersonAID IS NULL 

;WITH PersonRelationShipCollapsed AS (
  SELECT pl.PersonAID
        ,pl.PersonBID
        ,pl.Relation 
  FROM #PersonRelationShip      AS pl 
  
  UNION 

  SELECT pl.PersonBID AS PersonAID
        ,pl.PersonAID AS PersonBID
        ,pl.Relation
  FROM #PersonRelationShip      AS pl 
)
SELECT 
   pl.PersonAID
  ,pf.PersonBID
  ,pff.PersonBID
FROM #Persons                      AS p
--Лайки                      
JOIN PersonRelationShipCollapsed  AS pl ON pl.PersonAID = p.PersonID
                                 AND pl.Relation  = 'Like'                                  
--Друзья                          
JOIN PersonRelationShipCollapsed  AS pf ON pf.PersonAID = p.PersonID 
                                 AND pf.Relation = 'Friend'
--Друзья Друзей                   
JOIN PersonRelationShipCollapsed  AS pff ON pff.PersonAID = pf.PersonBID
                                 AND pff.PersonBID = pl.PersonBID
                                 AND pff.Relation = 'Friend'
--Ещё не дружат                   
LEFT JOIN PersonRelationShipCollapsed AS pnf ON pnf.PersonAID = p.PersonID
                                   AND pnf.PersonBID = pff.PersonBID
                                   AND pnf.Relation = 'Friend'
WHERE pnf.[PersonAID] IS NULL 

wniosek

Należy zaznaczyć, że podana składnia języka jest tylko jedną z możliwości realizacji danej koncepcji. Za podstawę przyjęto SQL, a celem było, aby był do niego jak najbardziej podobny. Oczywiście niektórym mogą nie podobać się nazwy słów kluczowych, rejestrów słów itp. Najważniejsza jest tutaj sama koncepcja. Jeśli chcesz, możesz stworzyć podobną składnię zarówno w C++, jak i w Pythonie.

Opisana koncepcja bazy danych ma moim zdaniem następujące zalety:

  • Łatwość. Jest to wskaźnik stosunkowo subiektywny, który w prostych przypadkach nie jest oczywisty. Ale jeśli spojrzysz na bardziej złożone przypadki (na przykład problemy z gwiazdkami), to moim zdaniem pisanie takich zapytań jest znacznie łatwiejsze.
  • Enkapsulacja. W niektórych przykładach zadeklarowałem funkcje pośrednie (np. sprzedany, kupiony itp.), z których zbudowano kolejne funkcje. Pozwala to w razie potrzeby zmienić logikę niektórych funkcji, bez zmiany logiki tych, które są od nich zależne. Możesz na przykład dokonać sprzedaży sprzedany zostały obliczone z zupełnie innych obiektów, reszta logiki się nie zmieni. Tak, można to zaimplementować w RDBMS za pomocą CREATE VIEW. Ale jeśli cała logika zostanie napisana w ten sposób, nie będzie wyglądać zbyt czytelnie.
  • Żadnej luki semantycznej. Taka baza danych operuje na funkcjach i klasach (a nie na tabelach i polach). Podobnie jak w programowaniu klasycznym (jeśli założymy, że metoda jest funkcją, której pierwszy parametr ma postać klasy, do której należy). W związku z tym znacznie łatwiej powinno być „zaprzyjaźnić się” z uniwersalnymi językami programowania. Dodatkowo koncepcja ta pozwala na realizację znacznie bardziej złożonych funkcjonalności. Można na przykład osadzić operatory takie jak:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE  'Что-то Петя продает слишком много одного товара в 2019 году';

  • Dziedziczenie i polimorfizm. W funkcjonalnej bazie danych można wprowadzić wielokrotne dziedziczenie poprzez konstrukcje CLASS ClassP: Class1, Class2 i zaimplementować wielokrotny polimorfizm. Pewnie napiszę jak dokładnie w przyszłych artykułach.

Chociaż jest to tylko koncepcja, mamy już pewną implementację w Javie, która przekłada całą logikę funkcjonalną na logikę relacyjną. Do tego pięknie dołączona jest do tego logika przedstawień i mnóstwo innych rzeczy, dzięki czemu otrzymujemy całość platforma. Zasadniczo używamy RDBMS (na razie tylko PostgreSQL) jako „maszyny wirtualnej”. Czasami pojawiają się problemy z tym tłumaczeniem, ponieważ optymalizator zapytań RDBMS nie zna pewnych statystyk, które zna FDBMS. Teoretycznie możliwe jest zaimplementowanie systemu zarządzania bazą danych, który będzie wykorzystywał określoną strukturę jako pamięć, dostosowaną specjalnie do logiki funkcjonalnej.

Źródło: www.habr.com

Dodaj komentarz