Funkcionalni DBMS

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 članci na Habr.

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 članci. Sadrži detaljnu analizu kako implementirati ovaj zadatak u MS SQL.

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

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 platforma. U suštini, koristimo RDBMS (do sada samo PostgreSQL) kao "virtuelnu mašinu". Ovaj prijevod ponekad uzrokuje probleme jer RDBMS optimizator upita ne zna određene statistike koje FDBMS zna. U teoriji, moguće je implementirati sistem upravljanja bazom podataka koji će koristiti određenu strukturu kao skladište, prilagođenu posebno za funkcionalnu logiku.

izvor: www.habr.com

Dodajte komentar