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
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
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
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
Zdroj: www.habr.com