Funkční databázový systém

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 stránkách Maven Apache Project 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 na stránkách Maven Apache Project. 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 dss_kalika:

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 platforma. 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

Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery 🔥 Koupit spolehlivý hosting pro webové stránky s ochranou proti DDoS, VPS VDS servery | ProHoster