Svět databází je již dlouho dominován relačními databázovými systémy, které používají jazyk SQL. To je tak silné, že se objevující varianty nazývají NoSQL. Ty si dokázaly vybojovat určité místo na tomto trhu, ale relační databázové systémy nemají v úmyslu zmizet a nadále se aktivně používají pro své cíle.
V tomto článku bych chtěl popsat koncept funkční databáze. Pro lepší pochopení to udělám srovnáním s klasickým relačním modelem. Jako příklady budou použity úkoly z různých testů SQL, nalezených na internetu.
Úvod
Relační databáze operují s tabulkami a poli. Ve funkční databázi budou místo nich použity třídy a funkce. Pole v tabulce s N klíči bude představováno jako funkce s N parametry. Místo vazeb mezi tabulkami budou použity funkce, které vracejí objekty třídy, na kterou se vazba týká. Místo JOIN se použije kompozice funkcí.
Než přejdu přímo k úkolům, popíšu úkol doménové logiky. Pro DDL použiji syntaxi PostgreSQL. Pro funkční svůj vlastní syntaxi.
Tabulky a pole
Jednoduchý objekt Sku s poli název a cena:
Grafový, dokumentový
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
Funkční
TŘÍDA Sku;
název = DATOVÝ ŘETĚZEC[100] (Sku);
cena = DATOVÉ ČÍSLO[10,5] (Sku);
Vyhlašujeme dva funkce, které přijímají jeden parametr Sku a vrací primitivní typ.
Předpokládá se, že ve funkční databázi bude mít každý objekt nějaký vnitřní kód, který se automaticky generuje, a na který se dá v případě potřeby odkazovat.
Nastavíme cenu pro produkt / obchod / dodavatele. Ta se může v průběhu času měnit, proto přidáme do tabulky pole čas. Aniž bych sestavil tabulky pro referenční údaje v relační databázi, abych zkrátil kód:
Grafový, dokumentový
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í
TŘÍDA Sku;
TŘÍDA Obchod;
TŘÍDA Dodavatel;
datumČasu = DATA DATETIME (Sku, Obchod, Dodavatel);
cena = DATA NUMERIC[10,5] (Sku, Obchod, Dodavatel);
Indexy
Pro poslední příklad vytvoříme index podle všech klíčů a data, aby bylo možné rychle najít cenu v určitém čase.
Grafový, dokumentový
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Funkční
INDEX Sku sk, Obchod st, Dodavatel sp, dateTime(sk, st, sp);
Úkoly
Začneme s relativně jednoduchými úkoly, které byly převzaty z příslušné na Habr.
Nejdříve vyhlásíme doménovou logiku (pro relaci to bylo provedeno přímo v uvedeném článku).
TŘÍDA Oddělení;
název = DATA ŘETĚZEC[100] (Oddělení);
CLASS Employee;
department = DATA Department (Employee);
chief = DATA Employee (Employee);
name = DATA STRING[100] (Employee);
salary = DATA NUMERIC[14,2] (Employee);
Úkol 1.1
Vytvořit seznam zaměstnanců, kteří mají vyšší plat než jejich přímý nadřízený.
Grafový, dokumentový
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Funkční
VYBERTE jméno(Zaměstnanec a) KDE plat(a) > plat(šéf(a));
Úkol 1.2
Vytvořit seznam zaměstnanců, kteří mají nejvyšší plat ve svém oddělení
Grafový, dokumentový
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
Funkční
maxSalary 'Maximální plat' (oddělení s) =
GROUP MAX plat(části e) IF oddělení(e) = s;
SELECT jméno(části a) WHERE plat(a) = maxSalary(oddělení(a));
// или если "заинлайнить"
SELECT name(Employee a) WHERE
salary(a) = maxSalary(GROUP MAX salary(Employee e) IF department(e) = department(a));
Obě realizace jsou ekvivalentní. V prvním případě je v relaci možné použít CREATE VIEW, který stejným způsobem nejprve spočítá maximální plat pro dané oddělení. Pro přehlednost budu dále používat první případ, protože lépe odráží řešení.
Úkol 1.3
Vytvořit seznam ID oddělení, kde počet zaměstnanců nepřesahuje 3 osoby.
Grafový, dokumentový
select department_id
from employee
group by department_id
having count(*) <= 3
Funkční
countEmployees 'Počet zaměstnanců' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
Úkol 1.4
Vytvořit seznam zaměstnanců, kteří nemají přiděleného nadřízeného, ale pracují ve stejném oddělení.
Grafový, dokumentový
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í
VYBER jméno(Zaměstnanec a) KDE NE (oddělení(vedoucí(a)) = oddělení(a));
Úkol 1.5
Najít seznam ID oddělení s maximálním součtem platů zaměstnanců.
Grafový, dokumentový
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 'Maximální plat' (Department d) =
GROUP SUM salary(Employee e) IF department(e) = d;
maxSalarySum 'Maximální plat oddělení' () =
GROUP MAX salarySum(Department d);
SELECT Department d WHERE salarySum(d) = maxSalarySum();
Přejdeme k složitějším úkolům z jiného . Zde je podrobná analýza toho, jak tuto úlohu implementovat v MS SQL.
Úkol 2.1
Kteří prodejci prodali v roce 1997 více než 30 kusů zboží č. 1?
Doménová logika (jak již dříve v RDBMS vynecháváme vyhlášení):
TŘÍDA Zaměstnanec 'Prodávající';
lastName 'Příjmení' = DATA ŘETĚZEC[100] (Zaměstnanec);
CLASS Product 'Produkt';
id = DATA INTEGER (Product);
name = DATA STRING[100] (Product);
CLASS Order 'Objednávka';
date = DATA DATE (Order);
employee = DATA Employee (Order);
CLASS Detail 'Položka objednávky';
order = DATA Order (Detail);
product = DATA Product (Detail);
quantity = DATA NUMERIC[10,5] (Detail);
Grafový, dokumentový
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í
prodáno (Zaměstnanec e, INTEGER productId, INTEGER rok) =
SKUPINA SUMA množství (ObjednávkovýDetail d) POKUD
zaměstnanec(objednávka(d)) = e A
id(produkt(d)) = productId A
vyjmoutRok(datum(objednávka(d))) = rok;
VYBERTE příjmení (Zaměstnanec e) KDE prodáno(e, 1, 1997) > 30;
Úkol 2.2
Pro každého zákazníka (jméno, příjmení) najít dva produkty (název), na které zákazník vynaložil nejvíce peněz v roce 1997.
Rozšiřujeme doménovou logiku z předchozího příkladu:
CLASS Customer 'Klient';
contactName 'Jméno a příjmení' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
Grafový, dokumentový
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í
suma (Detail d) = množství(d) * jednotkováCena(d) * (1 - sleva(d));
zakoupeno 'Zakoupil' (Zákazník c, Produkt p, CELOČÍSELNÉ y) =
SKUPINOVÁ SUMA suma(Detail d) POKUD
zákazník(příkaz(d)) = c A
produkt(d) = p A
vyextrahujRok(datum(příkaz(d))) = y;
hodnocení 'Hodnocení' (Zákazník c, Produkt p, CELOČÍSELNÉ y) =
PARTICE SUMA 1 SESTUPNĚ zakoupeno(c, p, y), p PODLE c, y;
VYBERTE jménoKontaktu(Zákazník c), jméno(Produkt p) KDE hodnocení(c, p, 1997) < 3;
Operátor PARTITION funguje na následujícím principu: sčítá výrazy, které jsou uvedeny po SUM (zde 1), uvnitř určených skupin (zde Customer a Year, ale může to být jakýkoli výraz), a třídí v rámci skupin podle výrazů uvedených v ORDER (zde bought, a pokud jsou stejné, tak podle interního kódu produktu).
Úloha 2.3
Kolik zboží je potřeba objednat od dodavatelů pro splnění aktuálních zakázek.
Znovu rozšiřujeme doménovou logiku:
CLASS Supplier 'Dodavatel';
companyName = DATA STRING[100] (Dodavatel);
supplier = DATA Supplier (Product);
unitsInStock 'Zůstatek na skladě' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Norma prodeje' = DATA NUMERIC[10,3] (Product);
Grafový, dokumentový
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í
objednáno, ale neodesláno
GROUP SUM quantity(OrderDetail d) IF product(d) = p;
kObjednání 'K objednání' (Product p) = objednánoNeodesláno(p) + úroveňObjednání(p) - jednotkyNaSkladu(p);
SELECT companyName(dodavatel(Product p)), name(p), kObjednání(p) WHERE kObjednání(p) > 0;
Úloha se hvězdičkou
A poslední příklad osobně od mě. Existuje logika sociální sítě. Lidé si mohou navzájem přátelit a mít se rádi. Z pohledu funkční databáze to bude vypadat následovně:
TŘÍDA Osoba;
líbí se = DATUM BOOLEAN (Osoba, Osoba);
přátelé = DATUM BOOLEAN (Osoba, Osoba);
Je třeba najít možné kandidáty na přátelství. Formálně je třeba najít všechny lidi A, B, C tak, že A je přítel s B, B je přítel s C, A má rád C, ale A není přítel s C.
Z pohledu funkční databáze bude dotaz vypadat následovně:
VYBERTE osobu a, osobu b, osobu c KDE
má raději(a, c) A NE jsou přátelé(a, c) A
jsou přátelé(a, b) A jsou přátelé(b, c);
Čtenář je vyzván, aby tuto úlohu vyřešil na SQL. Předpokládá se, že přátel je mnohem méně než těch, kdo se mají rádi. Proto jsou uloženi ve zvláštních tabulkách. V případě úspěšného řešení je také úloha se dvěma hvězdičkami. V ní přátelství není symetrické. Z pohledu funkční databáze to bude vypadat takto:
VYBERTE osobu a, osobu b, osobu c KDE
má raději(a, c) A NE jsou přátelé(a, c) A
(přátelé(a, b) NEBO přátelé(b, a)) A
(přátelé(b, c) NEBO přátelé(c, b));
UPD: řešení úlohy s první a druhou hvězdičkou od :
VYBERTE
pl.PersonAID
,pf.PersonAID
,pff.PersonAID
Z Osob AS p
--Líbí se
PŘIPOJIT OsobníVztah AS pl ON pl.PersonAID = p.PersonID
A pl.Relace = 'Líbí'
--Přátelé
PŘIPOJIT OsobníVztah AS pf ON pf.PersonAID = p.PersonID
A pf.Relace = 'Přítel'
--Přátelé Přátel
PŘIPOJIT OsobníVztah AS pff ON pff.PersonAID = pf.PersonBID
A pff.PersonBID = pl.PersonBID
A pff.Relace = 'Přítel'
--Ještě nejsou přátelé
LEVÝ PŘIPOJIT OsobníVztah AS pnf ON pnf.PersonAID = p.PersonID
A pnf.PersonBID = pff.PersonBID
A pnf.Relace = 'Přítel'
KDE pnf.PersonAID IS NULL
;S VČETNĚ PersonRelationShipCollapsed AS (
VYBERTE pl.PersonAID
,pl.PersonBID
,pl.Relace
Z #OsobníVztah AS pl
SVAZEK
VYBERTE pl.PersonBID AS PersonAID
,pl.PersonAID AS PersonBID
,pl.Relace
Z #OsobníVztah AS pl
)
VYBERTE
pl.PersonAID
,pf.PersonBID
,pff.PersonBID
Z #Osoby AS p
--Líbí se
PŘIPOJIT PersonRelationShipCollapsed AS pl ON pl.PersonAID = p.PersonID
A pl.Relace = 'Líbí'
--Přátelé
PŘIPOJIT PersonRelationShipCollapsed AS pf ON pf.PersonAID = p.PersonID
A pf.Relace = 'Přítel'
--Přátelé Přátel
PŘIPOJIT PersonRelationShipCollapsed AS pff ON pff.PersonAID = pf.PersonBID
A pff.PersonBID = pl.PersonBID
A pff.Relace = 'Přítel'
--Ještě nejsou přátelé
LEVÝ PŘIPOJIT PersonRelationShipCollapsed AS pnf ON pnf.PersonAID = p.PersonID
A pnf.PersonBID = pff.PersonBID
A pnf.Relace = 'Přítel'
KDE pnf.[PersonAID] IS NULL
Závěr
Je třeba poznamenat, že uvedená syntaxe jazyka je pouze jednou z možností realizace uvedené koncepce. Bylo použito právě SQL a cílem bylo, aby se co nejvíce podobalo. Samozřejmě, někomu se nemusí líbit názvy klíčových slov, velikosti písmen a další. Zde je však důležitá právě samotná koncepce. Při přání lze udělat i C++ nebo Python podobnou syntaxi.
Opopisovaná koncepce databáze má podle mého názoru následující výhody:
- Jednoduchost. Je to relativně subjektivní ukazatel, který není zřejmý na jednoduchých případech. Ale pokud se podíváte na složitější případy (například úkoly se hvězdičkami), pak podle mého názoru psát takové dotazy je mnohem jednodušší.
- Zabalení. V některých případech jsem deklaroval mezifunkce (například, prodáno, zakoupeno atd.), z nichž se odvozovaly následné funkce. To umožňuje v případě potřeby měnit logiku určitých funkcí bez měnění logiky odvozených funkcí. Například lze udělat, aby prodeje prodáno byly považovány za zcela jiné objekty, přičemž ostatní logika zůstane nezměněná. Ano, v RDBMS je možné to realizovat pomocí CREATE VIEW. Avšak pokud byste psali celou logiku tímto způsobem, bude vypadat málo čitelně.
- Nedostatek sémantického rozdělení. Taková databáze operuje s funkcemi a třídami (namísto tabulek a polí). Přesně tak, jak je tomu v klasickém programování (pokud považujeme metodu za funkci s prvním parametrem jako třídu, k níž patří). Odpovídajícím způsobem by mělo být „zkamarádit“ se s univerzálními programovacími jazyky mnohem jednodušší. Kromě toho tato koncepce umožňuje realizaci mnohem složitějších funkcí. Například můžete do databáze integrovat operátory typu:
OMEZENÍ sold(Employee e, 1, 2019) > 100 IF name(e) = 'Petr' ZPRÁVA 'Něco Petr prodává příliš mnoho jednoho zboží v roce 2019';
- Dědičnost a polymorfismus. V funkční databázi lze zavést vícenásobnou dědičnost pomocí konstrukce CLASS ClassP: Class1, Class2 a tím realizovat vícenásobný polymorfismus. Jak přesně, možná napíšu v dalších článcích.
I když je to pouze koncept, máme již určitou realizaci v Java, která převádí veškerou funkční logiku do relační logiky. Navíc je k tomu elegantně přišroubována logika zobrazení a mnoho dalších funkcionalit, díky nimž vzniká celek . V podstatě používáme RDBMS (zatím pouze PostgreSQL) jako „virtuální stroj“. Při této transformaci někdy vznikají problémy, protože optimalizátor dotazů RDBMS nezná určité statistiky, které zná FDBMS. Teoreticky je možné realizovat systém správy databáze, který by jako úložiště používal nějakou strukturu, přizpůsobenou právě pro funkční logiku.
Zdroj: habr.com
