Funkcionalni DBMS

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 Članak na Habru.

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

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 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 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 platforma. U biti, mi koristimo RDBMS (za sada samo PostgreSQL) kao "virtualni stroj". S ovim prijevodom ponekad nastaju problemi jer RDBMS optimizator upita ne zna određene statistike koje zna FDBMS. U teoriji je moguće implementirati sustav za upravljanje bazom podataka koji će koristiti određenu strukturu kao pohranu, prilagođenu specifično za funkcionalnu logiku.

Izvor: www.habr.com

Dodajte komentar