Funkční DBMS

Svět databází již dávno převzaly relační DBMS, které používají jazyk SQL. Natolik, že vznikající odrůdy se nazývají NoSQL. Podařilo se jim získat určité místo na tomto trhu, ale relační DBMS nezemřou a nadále jsou aktivně využívány pro své vlastní účely.

V tomto článku chci popsat koncept funkční databáze. Pro lepší pochopení to udělám porovnáním s klasickým relačním modelem. Jako příklady budou použity úlohy z různých SQL testů nalezených na internetu.

úvod

Relační databáze fungují na tabulkách a polích. Ve funkční databázi budou místo toho použity třídy a funkce. Pole v tabulce s N klíči bude reprezentováno jako funkce N parametrů. Místo odkazů mezi tabulkami budou použity funkce, které vracejí objekty třídy, na kterou odkaz směřuje. Místo JOIN bude použito složení funkcí.

Než přistoupím přímo k úlohám, popíšu úlohu doménové logiky. Pro DDL použiji syntaxi PostgreSQL. Pro funkční vlastní syntaxi.

Tabulky a pole

Jednoduchý objekt Sku s poli názvu a ceny:

vztahový

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

Vyhlašujeme dva funkce, které berou jeden parametr Sku jako vstup a vracejí primitivní typ.

Předpokládá se, že ve funkčním DBMS bude mít každý objekt nějaký interní kód, který se automaticky generuje a je k němu v případě potřeby přístup.

Stanovme cenu za produkt / obchod / dodavatele. Časem se může měnit, proto do tabulky přidáme pole času. Přeskočím deklaraci tabulek pro adresáře v relační databázi, abych zkrátil kód:

vztahový

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

Pro poslední příklad sestavme index na všech klíčích a datu, abychom mohli rychle najít cenu za určitý čas.

vztahový

CREATE INDEX prices_date
    ON prices
    (skuId, storeId, supplierId, dateTime)

Funkční

INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);

úkoly

Začněme relativně jednoduchými problémy převzatými z odpovídajících články na Habr.

Nejprve deklarujme doménovou logiku (u relační databáze je to provedeno přímo ve výše uvedeném č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 výzva

Zobrazte seznam zaměstnanců, kteří dostávají vyšší mzdy než přímý nadřízený.

vztahový

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 výzva

Zobrazte seznam zaměstnanců s nejvyšší mzdou ve svém oddělení

vztahový

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

Obě implementace jsou ekvivalentní. Pro první případ v relační databázi můžete použít CREATE VIEW, který stejným způsobem nejprve spočítá maximální mzdu pro konkrétní oddělení v ní. V budoucnu budu pro přehlednost používat první případ, protože lépe odráží řešení.

1.3 výzva

Zobrazte seznam ID oddělení, počet zaměstnanců nepřesahuje 3 osoby.

vztahový

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 výzva

Zobrazit seznam zaměstnanců, kteří nemají přiděleného vedoucího pracujícího ve stejném oddělení.

vztahový

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 výzva

Najděte seznam ID oddělení s maximálním celkovým platem zaměstnance.

vztahový

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();

Přejděme ke složitějším úkolům od jiného články. Obsahuje podrobnou analýzu, jak implementovat tento úkol v MS SQL.

2.1 výzva

Kteří prodejci prodali v roce 1997 více než 30 kusů položky č. 1?

Doménová logika (stejně jako dříve přeskočíme deklaraci na RDBMS):

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

vztahový

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 výzva

U každého zákazníka (jméno, příjmení) najděte dvě položky (jméno), za které zákazník v roce 1997 utratil nejvíce peněz.

Rozšíření doménové logiky z předchozího příkladu:

CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);

customer = DATA Customer (Order);

unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);

vztahový

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 následujícím principu: sečte výraz zadaný za SUM (zde 1) v rámci zadaných skupin (zde Zákazník a Rok, ale může to být libovolný výraz), seřadí v rámci skupin podle výrazů zadaných v ORDER ( zde koupil, a pokud jsou stejné, pak podle interního kódu produktu).

2.3 výzva

Kolik zboží je třeba objednat od dodavatelů pro splnění aktuálních objednávek.

Pojďme znovu rozšířit doménovou logiku:

CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);

supplier = DATA Supplier (Product);

unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);

vztahový

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;

Úkol s hvězdičkou

A poslední příklad je ode mě osobně. Existuje logika sociální sítě. Lidé se mohou navzájem přátelit a mít se rádi. Z pohledu funkční databáze by to vypadalo takto:

CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);

Je potřeba najít možné kandidáty na přátelství. Formálněji musíte najít všechny lidi A, B, C tak, že A se přátelí s B a B se přátelí s C, A má rád C, ale A není kamarád s C.
Z pohledu funkční databáze by dotaz vypadal 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);

Čtenář je vyzván k samostatnému řešení tohoto problému v SQL. Předpokládá se, že přátel je mnohem méně než těch, kteří mají rádi. Proto jsou v samostatných tabulkách. V případě úspěšného řešení je také problém se dvěma hvězdičkami. Její přátelství není symetrické. Na funkční databázi by to vypadalo 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: řešení problému s první a druhou hvězdič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ávěr

Je třeba poznamenat, že výše uvedená syntaxe jazyka je pouze jednou z možností implementace výše uvedeného konceptu. Právě SQL byl vzat jako základ a cílem bylo, aby se mu co nejvíce podobalo. Někomu se samozřejmě nemusí líbit názvy klíčových slov, velká a malá písmena slov a podobně. Hlavní je zde samotný koncept. Pokud chcete, můžete vytvořit podobnou syntaxi C++ i Pythonu.

Popsaný koncept databáze má podle mého názoru následující výhody:

  • Jednoduchost. Jedná se o poměrně subjektivní ukazatel, který není v jednoduchých případech zřejmý. Pokud se ale podíváte na složitější případy (například úkoly s hvězdičkami), pak je podle mého názoru psaní takových dotazů mnohem jednodušší.
  • Zapouzdření. V některých příkladech jsem deklaroval mezilehlé funkce (např. prodáno, koupilo atd.), ze kterých byly vybudovány následné funkce. To vám umožňuje v případě potřeby změnit logiku určitých funkcí, aniž byste změnili logiku těch, které na nich závisí. Můžete například provádět prodeje prodáno byly vypočítány ze zcela jiných objektů, zatímco zbytek logiky se nezmění. Ano, v RDBMS to lze provést pomocí CREATE VIEW. Ale když takto napíšete veškerou logiku, tak to nebude vypadat moc čtivě.
  • Žádná sémantická mezera. Taková databáze pracuje s funkcemi a třídami (místo tabulek a polí). Stejně jako v klasickém programování (za předpokladu, že metoda je funkce s prvním parametrem ve tvaru třídy, do které patří). V souladu s tím by mělo být mnohem snazší „spřátelit se“ s univerzálními programovacími jazyky. Tento koncept navíc umožňuje implementovat mnohem složitější funkce. Do databáze můžete například vložit následující příkazy:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE  'Что-то Петя продает слишком много одного товара в 2019 году';

  • Dědičnost a polymorfismus. Ve funkční databázi můžete zavést vícenásobnou dědičnost prostřednictvím konstrukcí CLASS ClassP: Class1, Class2 a implementovat vícenásobný polymorfismus. Jak přesně, snad napíšu v následujících článcích.

I když je to jen koncept, v Javě již máme nějakou implementaci, která převádí veškerou funkční logiku do relační logiky. Navíc je k tomu krásně přišroubovaná logika reprezentací a spousta dalších věcí, díky kterým dostaneme celek platforma. V podstatě používáme RDBMS (zatím pouze PostgreSQL) jako "virtuální stroj". Tento překlad někdy způsobuje problémy, protože optimalizátor dotazů RDBMS nezná určité statistiky, které zná FDBMS. Teoreticky je možné implementovat systém správy databáze, který bude využívat určitou strukturu jako úložiště, přizpůsobenou speciálně pro funkční logiku.

Zdroj: www.habr.com

Přidat komentář