Svetu databáz už dlho dominujú relačné DBMS, ktoré využívajú jazyk SQL. Až tak, že vznikajúce varianty sa nazývajú NoSQL. Podarilo sa im vybojovať si určité miesto na tomto trhu, ale relačné DBMS nezomrú a naďalej sa aktívne využívajú na svoje účely.
V tomto článku chcem popísať koncept funkčnej databázy. Pre lepšie pochopenie to urobím porovnaním s klasickým relačným modelom. Ako príklad budú použité problémy z rôznych SQL testov nájdených na internete.
Úvod
Relačné databázy fungujú na tabuľkách a poliach. Vo funkčnej databáze budú namiesto nich použité triedy a funkcie, resp. Pole v tabuľke s N kľúčmi bude reprezentované ako funkcia N parametrov. Namiesto vzťahov medzi tabuľkami sa použijú funkcie, ktoré vracajú objekty triedy, ku ktorej sa pripája. Namiesto JOIN sa použije zloženie funkcie.
Skôr ako prejdem priamo k úlohám, popíšem úlohu doménovej logiky. Pre DDL použijem syntax PostgreSQL. Pre funkčnosť má svoju vlastnú syntax.
Tabuľky a polia
Jednoduchý objekt Sku s poľami názvu a ceny:
Relačný
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
funkčné
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
Oznamujeme dve функции, ktoré berú jeden parameter Sku ako vstup a vracajú primitívny typ.
Predpokladá sa, že vo funkčnom DBMS bude mať každý objekt nejaký interný kód, ktorý sa generuje automaticky a je možné k nemu v prípade potreby pristupovať.
Stanovme cenu za produkt/obchod/dodávateľ. Časom sa môže meniť, preto do tabuľky pridajte pole času. Preskočím deklarovanie tabuliek pre adresáre v relačnej databáze, aby som skrátil kód:
Relačný
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)
)
funkčné
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
Indexy
Pre posledný príklad vytvoríme index na všetkých kľúčoch a dátume, aby sme mohli rýchlo nájsť cenu pre konkrétny čas.
Relačný
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
funkčné
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
úlohy
Začnime s relatívne jednoduchými problémami prevzatými z príslušných
Najprv deklarujme doménovú logiku (v prípade relačnej databázy sa to robí priamo vo vyššie uvedenom článku).
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);
1.1 Úloha
Zobrazte zoznam zamestnancov, ktorí dostávajú vyšší plat ako ich priamy nadriadený.
Relačný
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
funkčné
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
1.2 Úloha
Uveďte zamestnancov, ktorí dostávajú na svojom oddelení maximálny plat
Relačný
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
funkčné
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));
Obe implementácie sú ekvivalentné. V prvom prípade v relačnej databáze môžete použiť CREATE VIEW, ktorý rovnakým spôsobom najskôr vypočíta maximálnu mzdu pre konkrétne oddelenie v nej. V nasledujúcom texte pre prehľadnosť použijem prvý prípad, pretože lepšie odráža riešenie.
1.3 Úloha
Zobrazte zoznam ID oddelení, ktorých počet zamestnancov nepresahuje 3 osoby.
Relačný
select department_id
from employee
group by department_id
having count(*) <= 3
funkčné
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
1.4 Úloha
Zobrazte zoznam zamestnancov, ktorí nemajú určeného manažéra pracujúceho v rovnakom oddelení.
Relačný
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
funkčné
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
1.5 Úloha
Nájdite zoznam ID oddelení s maximálnym celkovým platom zamestnanca.
Relačný
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 )
funkčné
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();
Prejdime k zložitejším úlohám od iného
2.1 Úloha
Ktorí predajcovia predali v roku 1997 viac ako 30 kusov produktu č. 1?
Doménová logika (ako predtým na RDBMS preskočíme deklaráciu):
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);
Relačný
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
funkčné
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;
2.2 Úloha
U každého kupujúceho (meno, priezvisko) nájdite dva tovary (meno), na ktoré kupujúci minul v roku 1997 najviac peňazí.
Rozšírime doménovú logiku z predchádzajúceho príkladu:
CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
Relačný
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
funkčné
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;
Operátor PARTITION funguje na nasledujúcom princípe: sčítava výraz zadaný po SUM (tu 1), v rámci zadaných skupín (tu Zákazník a Rok, ale môže to byť ľubovoľný výraz), triedenie v rámci skupín podľa výrazov zadaných v ORDER ( tu kúpili, a ak sú rovnaké, potom podľa interného kódu produktu).
2.3 Úloha
Koľko tovaru je potrebné objednať od dodávateľov na splnenie aktuálnych objednávok.
Opäť rozvinieme logiku domény:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
Relačný
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
funkčné
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;
Problém s hviezdičkou
A posledný príklad je odo mňa osobne. Existuje logika sociálnej siete. Ľudia môžu byť medzi sebou priateľmi a mať sa radi. Z pohľadu funkčnej databázy by to vyzeralo takto:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
Je potrebné nájsť možných kandidátov na priateľstvo. Formálnejšie musíte nájsť všetkých ľudí A, B, C tak, že A je kamarát s B a B je kamarát s C, A má rád C, ale A nie je kamarát s C.
Z pohľadu funkčnej databázy by dotaz vyzeral takto:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
Čitateľovi sa odporúča, aby tento problém v SQL vyriešil sám. Predpokladá sa, že priateľov je oveľa menej ako ľudí, ktorých máte radi. Preto sú v samostatných tabuľkách. V prípade úspechu je tu aj úloha s dvomi hviezdičkami. V ňom priateľstvo nie je symetrické. Vo funkčnej databáze by to vyzeralo takto:
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: riešenie problému s prvou a druhou hviezdičkou 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
Záver
Treba si uvedomiť, že daná syntax jazyka je len jednou z možností implementácie daného konceptu. Za základ sa bral SQL a cieľom bolo, aby sa mu čo najviac podobal. Samozrejme, niekomu sa nemusia páčiť názvy kľúčových slov, slovné registre atď. Hlavná vec je tu samotný koncept. Ak chcete, môžete vytvoriť podobnú syntax C++ aj Pythonu.
Opísaný koncept databázy má podľa môjho názoru tieto výhody:
- jednoduchosť. Ide o pomerne subjektívny ukazovateľ, ktorý v jednoduchých prípadoch nie je zrejmý. Ale ak sa pozriete na zložitejšie prípady (napríklad problémy s hviezdičkami), potom je podľa môjho názoru písanie takýchto otázok oveľa jednoduchšie.
- zapuzdrenie. V niektorých príkladoch som deklaroval medziľahlé funkcie (napr. predaných, kúpilo atď.), z ktorých boli vybudované následné funkcie. To vám umožňuje v prípade potreby zmeniť logiku určitých funkcií bez toho, aby ste zmenili logiku tých, ktoré od nich závisia. Môžete napríklad uskutočniť predaj predaných boli vypočítané z úplne iných objektov, pričom zvyšok logiky sa nezmení. Áno, toto je možné implementovať v RDBMS pomocou CREATE VIEW. Ale ak je všetka logika napísaná týmto spôsobom, nebude to vyzerať veľmi čitateľne.
- Žiadna sémantická medzera. Takáto databáza pracuje s funkciami a triedami (namiesto tabuliek a polí). Rovnako ako v klasickom programovaní (ak predpokladáme, že metóda je funkcia s prvým parametrom v tvare triedy, do ktorej patrí). Preto by malo byť oveľa jednoduchšie „spriateliť sa“ s univerzálnymi programovacími jazykmi. Okrem toho tento koncept umožňuje implementovať oveľa komplexnejšie funkcie. Môžete napríklad vložiť operátorov, ako sú:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Dedičnosť a polymorfizmus. Vo funkčnej databáze môžete zaviesť viacnásobné dedičstvo prostredníctvom konštruktov CLASS ClassP: Class1, Class2 a implementovať viacnásobný polymorfizmus. Asi napíšem ako presne v budúcich článkoch.
Aj keď je to len koncept, v Jave už máme nejakú implementáciu, ktorá prevádza všetku funkčnú logiku do relačnej logiky. Navyše sa k tomu krásne viaže logika reprezentácií a množstvo iných vecí, vďaka ktorým dostaneme celok
Zdroj: hab.com