Svijetom baza podataka dugo su dominirali relacijski DBMS-ovi koji koriste SQL jezik. Toliko da se nove varijante nazivaju NoSQL. Uspjeli su sebi izboriti određeno mjesto na ovom tržištu, ali relacijski DBMS neće umrijeti, već će se i dalje aktivno koristiti za svoje potrebe.
U ovom članku želim opisati koncept funkcionalne baze podataka. Radi boljeg razumijevanja, učinit ću to uspoređujući ga s klasičnim relacijskim modelom. Zadaci iz raznih SQL testova koji se nalaze na Internetu koristit će se kao primjeri.
Uvod
Relacijske baze podataka rade na tablicama i poljima. U funkcionalnoj bazi podataka umjesto toga koristit će se klase i funkcije. Polje u tablici s N ključeva bit će predstavljeno kao funkcija N parametara. Umjesto relacija između tablica, koristit će se funkcije koje vraćaju objekte klase s kojom je uspostavljena veza. Umjesto JOIN koristit će se sastav funkcije.
Prije nego što prijeđem izravno na zadatke, opisat ću zadatak domenske logike. Za DDL ću koristiti PostgreSQL sintaksu. Za funkcionalno ima svoju sintaksu.
Tablice i polja
Jednostavan Sku objekt s poljima za naziv i cijenu:
Relacijska
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
Funkcionalni
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
Najavljujemo dva funkcije, koji uzimaju jedan parametar Sku kao ulaz i vraćaju primitivni tip.
Pretpostavlja se da će u funkcionalnom DBMS-u svaki objekt imati neki interni kod koji se automatski generira i može mu se pristupiti ako je potrebno.
Postavimo cijenu za proizvod/trgovinu/dobavljača. Može se promijeniti tijekom vremena, pa dodajmo vremensko polje u tablicu. Preskočit ću deklariranje tablica za direktorije u relacijskoj bazi podataka kako bih skratio kod:
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)
)
Funkcionalni
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
Indeksi
Za posljednji primjer, napravit ćemo indeks na svim ključevima i datumu kako bismo mogli brzo pronaći cijenu za određeno vrijeme.
Relacijska
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Funkcionalni
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
zadaci
Počnimo s relativno jednostavnim problemima preuzetim iz odgovarajućih
Prvo, deklarirajmo logiku domene (za relacijsku bazu podataka to se radi izravno u gornjem č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);
Zadatak 1.1
Prikažite popis zaposlenika koji primaju plaću veću od plaće njihovog neposrednog nadređenog.
Relacijska
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Funkcionalni
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
Zadatak 1.2
Navedite zaposlenike koji primaju maksimalnu plaću u svom odjelu
Relacijska
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
Funkcionalni
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));
Obje implementacije su ekvivalentne. Za prvi slučaj, u relacijskoj bazi možete koristiti CREATE VIEW, koji će na isti način prvo izračunati maksimalnu plaću za određeni odjel u njoj. U nastavku, radi jasnoće, koristit ću prvi slučaj jer on bolje odražava rješenje.
Zadatak 1.3
Prikažite popis ID-ova odjela, broj zaposlenika u kojem ne prelazi 3 osobe.
Relacijska
select department_id
from employee
group by department_id
having count(*) <= 3
Funkcionalni
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
Zadatak 1.4
Prikažite popis zaposlenika koji nemaju određenog voditelja koji radi u istom odjelu.
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
Funkcionalni
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
Zadatak 1.5
Pronađite popis ID-ova odjela s maksimalnom ukupnom plaćom zaposlenika.
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 )
Funkcionalni
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();
Prijeđimo na složenije zadatke s drugog
Zadatak 2.1
Koji su prodavači prodali više od 1997 jedinica proizvoda br. 30 u 1.?
Logika domene (kao i prije na RDBMS-u preskačemo deklaraciju):
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
Funkcionalni
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;
Zadatak 2.2
Za svakog kupca (ime, prezime) pronađite dvije robe (naziv) na koje je kupac potrošio najviše novca 1997. godine.
Proširujemo logiku domene iz prethodnog primjera:
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
Funkcionalni
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 radi na sljedećem principu: zbraja izraz naveden nakon SUM (ovdje 1), unutar navedenih grupa (ovdje Kupac i Godina, ali može biti bilo koji izraz), sortirajući unutar grupa prema izrazima navedenim u ORDER ( ovdje kupljeno, a ako je jednako, onda prema internoj šifri proizvoda).
Zadatak 2.3
Koliko je robe potrebno naručiti od dobavljača da bi se ispunile trenutne narudžbe.
Proširimo ponovno logiku domene:
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
Funkcionalni
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;
Problem sa zvjezdicom
I zadnji primjer je od mene osobno. Postoji logika društvene mreže. Ljudi mogu biti prijatelji jedni drugima i sviđati se jedni drugima. Iz perspektive funkcionalne baze podataka to bi izgledalo ovako:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
Potrebno je pronaći moguće kandidate za prijateljstvo. Formalnije, trebate pronaći sve ljude A, B, C tako da je A prijatelj s B, a B prijatelj s C, A voli C, ali A nije prijatelj s C.
Iz perspektive funkcionalne baze podataka, upit bi izgledao ovako:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
Čitatelja se potiče da sam riješi ovaj problem u SQL-u. Pretpostavlja se da ima puno manje prijatelja nego ljudi koji vam se sviđaju. Stoga su u zasebnim tablicama. U slučaju uspjeha, postoji i zadatak s dvije zvjezdice. U njemu prijateljstvo nije simetrično. U funkcionalnoj bazi podataka to bi izgledalo ovako:
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: rješenje problema s prvom i drugom zvjezdicom 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čak
Treba napomenuti da je navedena jezična sintaksa samo jedna od opcija za implementaciju zadanog koncepta. Za osnovu je uzet SQL, a cilj je bio da mu bude što sličniji. Naravno, nekima se možda neće svidjeti nazivi ključnih riječi, registar riječi itd. Ovdje je glavna stvar sam koncept. Ako želite, možete napraviti sličnu sintaksu za C++ i Python.
Opisani koncept baze podataka, po mom mišljenju, ima sljedeće prednosti:
- Ublažiti. Ovo je relativno subjektivan pokazatelj koji nije očit u jednostavnim slučajevima. Ali ako pogledate složenije slučajeve (na primjer, probleme sa zvjezdicama), tada je, po mom mišljenju, pisanje takvih upita puno lakše.
- Инкапсуляция. U nekim primjerima deklarirao sam srednje funkcije (na primjer, prodan, Kupio itd.), od kojih su izgrađene sljedeće funkcije. To vam omogućuje da promijenite logiku određenih funkcija, ako je potrebno, bez promjene logike onih koje ovise o njima. Na primjer, možete prodavati prodan su izračunate iz potpuno različitih objekata, dok se ostatak logike neće promijeniti. Da, ovo se može implementirati u RDBMS koristeći CREATE VIEW. Ali ako je sva logika ovako napisana, neće izgledati baš čitljivo.
- Nema semantičkog jaza. Takva baza podataka radi na funkcijama i klasama (umjesto na tablicama i poljima). Baš kao u klasičnom programiranju (ako pretpostavimo da je metoda funkcija s prvim parametrom u obliku klase kojoj pripada). Sukladno tome, trebalo bi biti puno lakše "sprijateljiti se" s univerzalnim programskim jezicima. Osim toga, ovaj koncept omogućuje implementaciju mnogo složenije funkcionalnosti. Na primjer, možete ugraditi operatore poput:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Nasljeđe i polimorfizam. U funkcionalnoj bazi podataka možete uvesti višestruko nasljeđivanje kroz CLASS ClassP: Class1, Class2 konstrukcije i implementirati višestruki polimorfizam. Vjerojatno ću u budućim člancima napisati kako točno.
Iako je ovo samo koncept, već imamo neku implementaciju u Javi koja svu funkcionalnu logiku prevodi u relacijsku logiku. Plus, logika prikaza i puno drugih stvari lijepo je vezano uz to, zahvaljujući čemu dobivamo cjelinu
Izvor: www.habr.com