Funkčný DBMS

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 Článok 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 ČlánokObsahuje 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 dss_kalika:

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

Kúpte si spoľahlivý hosting pre stránky s DDoS ochranou, VPS VDS servery 🔥 Kúpte si spoľahlivý webhosting s ochranou DDoS, VPS VDS servery | ProHoster