Funkčný DBMS

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 Článok na Habr.

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 Článok. Obsahuje podrobnú analýzu implementácie tejto úlohy v MS SQL.

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

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 platforma. V podstate používame RDBMS (zatiaľ iba PostgreSQL) ako „virtuálny stroj“. S týmto prekladom niekedy vznikajú problémy, pretože optimalizátor dotazov RDBMS nepozná určité štatistiky, ktoré pozná FDBMS. Teoreticky je možné implementovať systém správy databáz, ktorý bude využívať určitú štruktúru ako úložisko, prispôsobenú špeciálne pre funkčnú logiku.

Zdroj: hab.com

Pridať komentár