Svet databáz dlho dominovali relačné systémy riadenia databáz (DBMS), ktoré používajú jazyk SQL. A to až do takej miery, že vznikajúce varianty sa nazývajú NoSQL. Na tomto trhu si vydobyli svoje miesto, ale relačné systémy riadenia databáz nevykazujú žiadne známky zániku a naďalej sa aktívne používajú na svoje zamýšľané účely.
V tomto článku popíšem koncept funkčnej databázy. Pre lepšie pochopenie ho porovnám s klasickým relačným modelom. Použijem príklady z rôznych SQL testov nájdených online.
Úvod
Relačné databázy pracujú s tabuľkami a poľami. Vo funkčnej databáze sú tieto nahradené triedami a funkciami. 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 prepojovanej triedy. Namiesto JOIN-ov sa použije kompozícia funkcií.
Predtým, ako prejdem k samotným úlohám, popíšem logiku domény. Pre DDL budem používať syntax PostgreSQL. Pre funkcionálnu logiku použijem vlastnú syntax.
Tabuľky a polia
Jednoduchý objekt SKU s poľami pre názov a cenu:
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ú ako vstup jeden parameter Sku a vracajú primitívny typ.
Predpokladá sa, že vo funkčnom DBMS bude mať každý objekt nejaký interný kód, ktorý sa automaticky generuje a ku ktorému je možné v prípade potreby pristupovať.
Nastavme si cenu produktu/obchodu/dodávateľa. Môže sa časom meniť, preto do tabuľky pridajme časové pole. Pre skrátenie kódu preskočím deklaráciu tabuľky pre referenčné tabuľky v relačnej databáze:
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
V poslednom príklade vytvoríme index pre všetky kľúče a dátumy, aby sme mohli rýchlo nájsť cenu v konkrétnom čase.
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 relevantných na Habr.
Najprv deklarujme logiku domény (pre relačnú databázu sa to robí priamo v uvedenom článku).
CLASS Department;
name = DATA STRING[100] (Department);
Zamestnanec TRIEDA;
oddelenie = DATA Oddelenie(Zamestnanec);
vedúci = DATA Zamestnanec(Zamestnanec);
meno = DÁTOVÝ REŤAZEC[100] (Zamestnanec);
plat = DATA NUMERIC[14,2] (Zamestnanec);
1.1 Úloha
Uveďte zoznam zamestnancov, ktorí poberajú 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í vo svojom oddelení poberajú najvyšší 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));
// alebo ak je „vložený“
VYBERTE meno(Zamestnanec a) KDE
plat(a) = maxPlat(SKUPINA MAX plat(Zamestnanec e) AK oddelenie(e) = oddelenie(a));
Obe implementácie sú ekvivalentné. V prvom prípade môžete použiť CREATE VIEW v relačnej databáze, ktorá podobne najprv vypočíta maximálny plat pre konkrétne oddelenie. Pre prehľadnosť použijem nižšie prvý prípad, pretože lepšie odráža riešenie.
1.3 Úloha
Zobraziť zoznam ID oddelení s maximálne 3 zamestnancami.
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
Uveďte zamestnancov, ktorí nemajú prideleného manažéra pracujúceho v tom istom 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 najvyššími celkovými platmi zamestnancov.
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 problémom z iného Obsahuje podrobnú analýzu toho, ako implementovať túto úlohu v MS SQL.
2.1 Úloha
Ktorí predajcovia predali v roku 1997 viac ako 30 kusov produktu č. 1?
Doménová logika (rovnako ako predtým, v RDBMS deklaráciu vynechávame):
CLASS Employee 'Продавец';
lastName 'Фамилия' = DATA STRING[100] (Employee);
TRIEDA Produkt 'Produkt';
id = DATA INTEGER (Produkt);
názov = REŤAZEC DÁT[100] (Produkt);
TRIEDA Poriadok 'Poriadok';
dátum = DÁTUM_ÚDAJOV (Objednávka);
zamestnanec = DATA Zamestnanec(Objednávka);
Detail TRIEDA „Objednávkový riadok“;
objednávka = DATA Objednávka (Detail);
produkt = DATA Produkt(Detail);
množstvo = 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
Pre každého zákazníka (meno, priezvisko) nájdite dva produkty (meno), na ktoré zákazník v roku 1997 minul najviac peňazí.
Rozšírme logiku domény z predchádzajúceho príkladu:
CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);
zákazník = DATA Zákazník(Objednávka);
JednotkováCena = ČÍSELNÉ ÚDAJE[14,2] (Detail);
zľava = 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číta výraz zadaný za operátorom SUM (tu 1) v rámci zadaných skupín (tu Customer a Year, ale môže to byť ľubovoľný výraz) a zoradí v rámci skupín podľa výrazov zadaných v operátore ORDER (tu buyed, a ak je rovnaké, tak podľa interného kódu produktu).
2.3 Úloha
Koľko položiek je potrebné objednať od dodávateľov na splnenie aktuálnych objednávok.
Znovu rozviňme logiku domény:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
dodávateľ = DATA Dodávateľ(Produkt);
unitsInStock 'Stav zásob' = DATA NUMERIC[10,3] (Produkt);
reorderLevel 'Miera predaja' = DATA NUMERIC[10,3] (Produkt);
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 ešte jeden posledný príklad odo mňa osobne. Je to logika sociálnej siete. Ľudia môžu byť medzi sebou priateľmi a mať sa radi. Z hľadiska funkčnej databázy by to vyzeralo takto:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
Potrebujeme nájsť potenciálnych kandidátov na priateľstvo. Formálnejšie povedané, potrebujeme nájsť všetkých ľudí A, B, C takých, že A je priateľ s B, B je priateľ s C, A má rád C, ale A nie je priateľ s C.
Z hľadiska 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ľ má tento problém vyriešiť samostatne pomocou SQL. Predpokladá sa, že priateľov je oveľa menej ako lajkov, takže sú uložené v samostatných tabuľkách. Ak je riešenie úspešné, existuje aj problém s dvoma hviezdičkami. V tomto prípade 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));
AKTUALIZÁCIA: riešenie problému s prvou a druhou hviezdou 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
Stojí za zmienku, že prezentovaná syntax jazyka je len jednou z možných implementácií konceptu. Ako základ bol použitý jazyk SQL a cieľom bolo, aby bol čo najviac podobný. Samozrejme, niektorým sa môžu nepáčiť názvy kľúčových slov, veľkosť písmen atď. Kľúčom je tu samotný koncept. Podobná syntax by sa dala implementovať v jazykoch C++ a Python, ak by sa to chcelo.
Opísaný koncept databázy má podľa môjho názoru nasledujúce výhody:
- jednoduchosťToto je relatívne subjektívny ukazovateľ, ktorý nie je v jednoduchých prípadoch okamžite zrejmý. Ak však vezmeme do úvahy zložitejšie prípady (napríklad problémy s hviezdičkami), myslím si, že písanie takýchto dotazov je výrazne jednoduchšie.
- zapuzdrenieV niektorých príkladoch som deklaroval medziľahlé funkcie (napríklad predaných, kúpilo atď.), z ktorých sa zostavujú následné funkcie. To vám umožňuje v prípade potreby zmeniť logiku určitých funkcií bez zmeny logiky tých, ktoré od nich závisia. Napríklad môžete vykonávať predaje predaných vypočítané z úplne iných objektov, zatiaľ čo zvyšok logiky zostáva nezmenený. Áno, toto sa dá dosiahnuť v RDBMS pomocou CREATE VIEW. Ale zápis celej logiky týmto spôsobom ju znehodnotí.
- Absencia sémantickej medzeryTáto databáza operuje s funkciami a triedami (namiesto tabuliek a polí), rovnako ako v klasickom programovaní (ak metódu považujeme za funkciu, ktorej prvým parametrom je trieda, do ktorej patrí). Integrácia s univerzálnymi programovacími jazykmi by preto mala byť výrazne jednoduchšia. Okrem toho tento koncept umožňuje implementáciu oveľa zložitejších funkcií. Do databázy je možné vložiť napríklad operátory, ako sú tieto:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Dedičnosť a polymorfizmusVo funkčnej databáze môžete zaviesť viacnásobné dedenie pomocou konštruktu CLASS ClassP: Class1, Class2 a implementovať viacnásobný polymorfizmus. Pravdepodobne sa v budúcich článkoch budem venovať tomu, ako to presne funguje.
Aj keď je to len koncept, v Jave už máme implementáciu, ktorá prekladá všetku funkčnú logiku do relačnej logiky. Navyše je pekne doplnená o prezentačnú logiku a mnoho ďalších vecí, čím sa stáva celkom... V podstate používame RDBMS (momentálne iba PostgreSQL) ako „virtuálny stroj“. Tento preklad niekedy spôsobuje problémy, pretože optimalizátor dotazov RDBMS nepozná určité štatistiky, ktoré FSDBMS pozná. Teoreticky je možné implementovať systém správy databáz, ktorý používa štruktúru úložiska prispôsobenú špecificky funkčnej logike.
Zdroj: hab.com
