V svetu baz podatkov že dolgo prevladujejo relacijski DBMS-ji, ki uporabljajo jezik SQL. Tako zelo, da se nastajajoče različice imenujejo NoSQL. Uspeli so si izboriti določeno mesto na tem trgu, vendar relacijski DBMS ne bodo umrli in se bodo še naprej aktivno uporabljali za svoje namene.
V tem članku želim opisati koncept funkcionalne baze podatkov. Za boljše razumevanje bom to naredil tako, da ga bom primerjal s klasičnim relacijskim modelom. Kot primeri bodo uporabljeni problemi iz različnih testov SQL, ki jih najdemo na internetu.
Predstavitev
Relacijske baze podatkov delujejo na tabelah in poljih. V funkcionalni bazi podatkov bodo namesto tega uporabljeni razredi oziroma funkcije. Polje v tabeli z N ključi bo predstavljeno kot funkcija N parametrov. Namesto relacij med tabelami bodo uporabljene funkcije, ki vračajo objekte razreda, s katerim je vzpostavljena povezava. Namesto JOIN bo uporabljena sestava funkcij.
Preden preidem neposredno na naloge, bom opisal nalogo domenske logike. Za DDL bom uporabil sintakso PostgreSQL. Za funkcionalnost ima svojo sintakso.
Tabele in polja
Preprost predmet Sku s polji za ime in ceno:
Relacijska
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
Funkcionalna
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
Objavljamo dva funkcije, ki sprejmejo en parameter Sku kot vhod in vrnejo primitivni tip.
Predpostavlja se, da bo v funkcionalnem DBMS vsak objekt imel neko notranjo kodo, ki je samodejno ustvarjena in do katere lahko po potrebi dostopate.
Določimo ceno za izdelek/trgovino/dobavitelja. Sčasoma se lahko spremeni, zato v tabelo dodamo časovno polje. Preskočil bom deklaracijo tabel za imenike v relacijski bazi podatkov, da bi skrajšal kodo:
Relacijska
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)
)
Funkcionalna
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
Indeksi
Za zadnji primer bomo zgradili indeks na vseh ključih in datumu, da bomo lahko hitro našli ceno za določen čas.
Relacijska
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Funkcionalna
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
naloge
Začnimo z razmeroma preprostimi problemi, vzetimi iz ustreznih
Najprej deklarirajmo logiko domene (za relacijsko bazo podatkov je to storjeno neposredno v zgornjem članku).
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);
Izziv 1.1
Prikažite seznam zaposlenih, ki prejemajo višjo plačo od neposrednega nadrejenega.
Relacijska
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Funkcionalna
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
Izziv 1.2
Navedite zaposlene, ki prejemajo najvišjo plačo v svojem oddelku
Relacijska
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
Funkcionalna
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 izvedbi sta enakovredni. Za prvi primer lahko v relacijski bazi podatkov uporabite CREATE VIEW, ki bo na enak način najprej izračunal maksimalno plačo za določen oddelek v njej. V nadaljevanju bom zaradi jasnosti uporabil prvi primer, saj bolje odraža rešitev.
Izziv 1.3
Prikažite seznam ID-jev oddelkov, v katerih število zaposlenih ne presega 3 ljudi.
Relacijska
select department_id
from employee
group by department_id
having count(*) <= 3
Funkcionalna
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
Izziv 1.4
Prikažite seznam zaposlenih, ki nimajo imenovanega vodje, ki dela v istem oddelku.
Relacijska
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
Funkcionalna
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
Izziv 1.5
Poiščite seznam ID-jev oddelkov z najvišjo skupno plačo zaposlenega.
Relacijska
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 )
Funkcionalna
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();
Preidimo na bolj zapletene naloge z drugega
Izziv 2.1
Kateri prodajalci so leta 1997 prodali več kot 30 enot izdelka št. 1?
Logika domene (kot prej na RDBMS preskočimo deklaracijo):
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);
Relacijska
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
Funkcionalna
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;
Izziv 2.2
Za vsakega kupca (ime, priimek) poiščite dve dobrini (ime), za kateri je kupec leta 1997 zapravil največ denarja.
Razširimo logiko domene iz prejšnjega primera:
CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
Relacijska
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
Funkcionalna
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;
Operator PARTITION deluje po naslednjem principu: sešteje izraz, določen za SUM (tukaj 1), znotraj navedenih skupin (tukaj Stranka in Leto, vendar je lahko kateri koli izraz), razvrščanje znotraj skupin po izrazih, navedenih v ORDER ( tukaj kupljeno, in če je enako, potem v skladu z interno kodo izdelka).
Izziv 2.3
Koliko blaga je treba naročiti pri dobaviteljih za izpolnitev trenutnih naročil.
Ponovno razširimo domensko logiko:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
Relacijska
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
Funkcionalna
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;
Težava z zvezdico
In zadnji primer je od mene osebno. Obstaja logika družbenega omrežja. Ljudje smo lahko med seboj prijatelji in se imamo radi. Z vidika funkcionalne baze podatkov bi to izgledalo takole:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
Treba je najti možne kandidate za prijateljstvo. Bolj formalno, najti morate vse ljudi A, B, C, tako da je A prijatelj z B in B prijatelj s C, A je všeč C, vendar A ni prijatelj s C.
Z vidika funkcionalne baze podatkov bi bila poizvedba videti takole:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
Bralca spodbujamo, da to težavo reši v SQL sam. Predpostavlja se, da je prijateljev veliko manj kot ljudi, ki so vam všeč. Zato so v ločenih tabelah. V primeru uspeha je na voljo tudi naloga z dvema zvezdicama. V njej prijateljstvo ni simetrično. V funkcionalni bazi podatkov bi bilo videti takole:
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: rešitev težave s prvo in drugo zvezdico iz
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
Zaključek
Opozoriti je treba, da je podana sintaksa jezika le ena od možnosti za implementacijo danega koncepta. Za osnovo je bil vzet SQL, cilj pa je bil, da bi mu bil čim bolj podoben. Seveda nekaterim morda ne bodo všeč imena ključnih besed, besedni registri itd. Tu je glavna stvar sam koncept. Če želite, lahko naredite C++ in Python podobno sintakso.
Opisani koncept baze podatkov ima po mojem mnenju naslednje prednosti:
- preprostost. To je relativno subjektiven indikator, ki v preprostih primerih ni očiten. Če pa pogledate bolj zapletene primere (na primer težave z zvezdicami), potem je po mojem mnenju pisanje takšnih poizvedb veliko lažje.
- Инкапсуляция. V nekaterih primerih sem deklariral vmesne funkcije (npr. prodaja, Kupil itd.), iz katerih so bile zgrajene naslednje funkcije. To vam omogoča, da po potrebi spremenite logiko določenih funkcij, ne da bi spremenili logiko tistih, ki so od njih odvisne. Na primer, lahko prodajate prodaja so bili izračunani iz popolnoma različnih objektov, medtem ko se ostala logika ne bo spremenila. Da, to je mogoče implementirati v RDBMS z uporabo CREATE VIEW. Toda če je vsa logika zapisana na ta način, ne bo videti zelo berljivo.
- Brez pomenske vrzeli. Takšna zbirka podatkov deluje na funkcijah in razredih (namesto na tabelah in poljih). Tako kot pri klasičnem programiranju (če predpostavimo, da je metoda funkcija s prvim parametrom v obliki razreda, ki mu pripada). V skladu s tem bi moralo biti veliko lažje "sprijateljiti se" z univerzalnimi programskimi jeziki. Poleg tega ta koncept omogoča implementacijo veliko bolj kompleksne funkcionalnosti. Vdelate lahko na primer operatorje, kot so:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Dedovanje in polimorfizem. V funkcionalni bazi podatkov lahko uvedete večkratno dedovanje prek konstruktov CLASS ClassP: Class1, Class2 in implementirate večkratni polimorfizem. Verjetno bom v naslednjih člankih napisal, kako natančno.
Čeprav je to samo koncept, že imamo nekaj implementacije v Javi, ki prevede vso funkcionalno logiko v relacijsko logiko. Poleg tega je nanj lepo pripeta logika predstav in veliko drugih stvari, zaradi česar dobimo celoto
Vir: www.habr.com