Ś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
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
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
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ść
Źródło: www.habr.com