Svijet baze podataka odavno su preuzeli relacijski DBMS-ovi koji koriste SQL jezik. Toliko da se nove varijante nazivaju NoSQL. Oni su uspjeli izboriti za sebe 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 usporedbom s klasičnim relacijskim modelom. Kao primjer će se koristiti zadaci iz različitih SQL testova koji se nalaze na Internetu.
Uvod
Relacijske baze podataka rade na tabelama i poljima. U funkcionalnoj bazi podataka, umjesto toga će se koristiti klase i funkcije. Polje u tabeli sa N ključeva biće predstavljeno kao funkcija od N parametara. Umjesto veza između tabela, koristit će se funkcije koje vraćaju objekte klase do koje veza ide. Kompozicija funkcije će se koristiti umjesto JOIN.
Prije nego što pređem direktno na zadatke, opisat ću zadatak domenske logike. Za DDL, koristiću PostgreSQL sintaksu. Za funkcionalnu vlastitu sintaksu.
Tabele i polja
Jednostavan Sku objekt s poljima imena i cijene:
relacijski
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
funkcionalan
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
Najavljujemo dva funkcije, koji uzimaju jedan Sku parametar kao ulaz i vraćaju primitivni tip.
Pretpostavlja se da će u funkcionalnom DBMS-u svaki objekat imati neki interni kod koji se automatski generiše i može mu se pristupiti ako je potrebno.
Postavimo cijenu za proizvod/trgovinu/dobavljača. Može se promijeniti tokom vremena, pa dodajmo vremensko polje u tabelu. Preskočit ću deklaraciju tablica za direktorije u relacijskoj bazi podataka da skratim kod:
relacijski
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)
)
funkcionalan
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
Indeksi
Za posljednji primjer, napravimo indeks na svim ključevima i datumu tako da možemo brzo pronaći cijenu za određeno vrijeme.
relacijski
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
funkcionalan
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
zadaci
Počnimo s relativno jednostavnim problemima preuzetim iz odgovarajućih
Prvo, deklarišemo logiku domena (za relacionu bazu podataka, to se radi direktno 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);
Problem 1.1
Prikažite listu zaposlenih koji primaju veće plate od onih koji imaju neposredno nadređeni.
relacijski
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
funkcionalan
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
Problem 1.2
Prikažite listu zaposlenih koji zarađuju najveću platu u svom odjeljenju
relacijski
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
funkcionalan
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 relacionoj bazi podataka možete koristiti CREATE VIEW, koji će na isti način prvo izračunati maksimalnu platu za određeni odjel u njemu. Ubuduće, radi jasnoće, koristiću prvi slučaj, jer bolje odražava rješenje.
Problem 1.3
Prikažite listu ID-ova odjela, u kojima broj zaposlenih ne prelazi 3 osobe.
relacijski
select department_id
from employee
group by department_id
having count(*) <= 3
funkcionalan
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
Problem 1.4
Prikažite listu zaposlenih koji nemaju dodijeljenog menadžera koji radi u istom odjelu.
relacijski
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
funkcionalan
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
Problem 1.5
Pronađite listu ID-ova odjela sa maksimalnom ukupnom platom zaposlenika.
relacijski
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 )
funkcionalan
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();
Pređimo na složenije zadatke iz drugog
Problem 2.1
Koji su prodavci prodali više od 1997 komada artikla br. 30 1. godine?
Logika domene (kao i ranije, preskačemo deklaraciju na RDBMS-u):
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);
relacijski
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
funkcionalan
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;
Problem 2.2
Za svakog kupca (ime, prezime) pronađite dvije stavke (ime) na koje je kupac potrošio najviše novca 1997. godine.
Proširivanje logike 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);
relacijski
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
funkcionalan
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 prema sljedećem principu: zbraja izraz specificiran 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 kupljen, a ako su jednaki, onda po internom kodu proizvoda).
Problem 2.3
Koliko robe treba naručiti od dobavljača da bi se ispunile tekuće narudžbe.
Proširimo logiku domene ponovo:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
relacijski
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
funkcionalan
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;
Zadatak sa zvjezdicom
I posljednji primjer je od mene lično. Postoji logika društvene mreže. Ljudi mogu biti prijatelji jedni drugima i voleti jedni druge. Iz perspektive funkcionalne baze podataka, ovo 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, morate pronaći sve ljude A, B, C tako da je A prijatelj sa B, a B prijatelj sa C, A voli C, ali A nije prijatelj sa C.
Sa funkcionalne tačke gledišta 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);
Čitalac je pozvan da samostalno riješi ovaj problem u SQL-u. Pretpostavlja se da je mnogo manje prijatelja od onih koji vole. Stoga su u posebnim tabelama. U slučaju uspješnog rješenja, javlja se i problem sa dvije zvjezdice. Njeno prijateljstvo nije simetrično. Na 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 sa prvom i drugom zvjezdicom od
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 gornja sintaksa jezika samo jedna od opcija za implementaciju gore navedenog koncepta. Za osnovu je uzet SQL, a cilj je bio da mu bude što sličniji. Naravno, nekome se možda neće dopasti nazivi ključnih riječi, velika i mala slova itd. Glavna stvar ovdje je 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:
- jednostavnost. Ovo je relativno subjektivan pokazatelj koji nije očigledan u jednostavnim slučajevima. Ali ako pogledate složenije slučajeve (na primjer, zadatke sa zvjezdicama), tada je, po mom mišljenju, pisanje takvih upita mnogo lakše.
- Inkapsulâciâ. U nekim primjerima deklarirao sam srednje funkcije (na primjer, prodan, kupio itd.), od kojih su izgrađene naknadne funkcije. Ovo vam omogućava da promijenite logiku određenih funkcija, ako je potrebno, bez promjene logike onih koje ovise o njima. Na primjer, možete ostvariti prodaju prodan izračunate su iz potpuno različitih objekata, dok se ostatak logike neće promijeniti. Da, u RDBMS-u se to može uraditi sa CREATE VIEW. Ali ako napišete svu logiku na ovaj način, onda to neće izgledati baš čitljivo.
- Nema semantičkog jaza. Takva baza podataka radi sa funkcijama i klasama (umjesto tabela i polja). Na isti način kao u klasičnom programiranju (pod pretpostavkom da je metoda funkcija sa prvim parametrom u obliku klase kojoj pripada). Shodno tome, trebalo bi da bude mnogo lakše „sprijateljiti se“ sa univerzalnim programskim jezicima. Osim toga, ovaj koncept vam omogućava implementaciju mnogo složenijih funkcija. Na primjer, ovakve izjave možete ugraditi u bazu podataka:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Nasljeđivanje i polimorfizam. U funkcionalnoj bazi podataka možete uvesti višestruko nasljeđivanje kroz CLASS ClassP: Class1, Class2 konstrukcije i implementirati višestruki polimorfizam. Kako tačno, možda ću napisati u sljedećim člancima.
Iako je ovo samo koncept, već imamo neku implementaciju u Javi koja prevodi svu funkcionalnu logiku u relaciju. Osim toga, logika reprezentacija i puno drugih stvari su lijepo uvrnute u to, zahvaljujući čemu dobijamo cjelinu
izvor: www.habr.com