SGBD funcțional

Lumea bazelor de date a fost mult timp dominată de SGBD-urile relaționale, care folosesc limbajul SQL. Atât de mult încât variantele emergente se numesc NoSQL. Ei au reușit să-și facă un anumit loc pe această piață, dar SGBD-urile relaționale nu vor muri și continuă să fie utilizate în mod activ în scopurile lor.

În acest articol vreau să descriu conceptul de bază de date funcțională. Pentru o mai bună înțelegere, voi face acest lucru comparându-l cu modelul relațional clasic. Problemele din diverse teste SQL găsite pe Internet vor fi folosite ca exemple.

Introducere

Bazele de date relaționale operează pe tabele și câmpuri. Într-o bază de date funcțională vor fi folosite în schimb clase și, respectiv, funcții. Un câmp dintr-un tabel cu N chei va fi reprezentat în funcție de N parametri. În loc de relații între tabele, vor fi folosite funcții care returnează obiecte din clasa la care se face conexiunea. Compoziția funcției va fi folosită în loc de JOIN.

Înainte de a trece direct la sarcini, voi descrie sarcina logicii domeniului. Pentru DDL voi folosi sintaxa PostgreSQL. Pentru funcțional are propria sintaxă.

Tabele și câmpuri

Un obiect Sku simplu cu câmpuri de nume și preț:

Relațional

CREATE TABLE Sku
(
    id bigint NOT NULL,
    name character varying(100),
    price numeric(10,5),
    CONSTRAINT id_pkey PRIMARY KEY (id)
)

Funcţional

CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);

Vă anunțăm două funcții, care iau ca intrare un parametru Sku și returnează un tip primitiv.

Se presupune că într-un SGBD funcțional fiecare obiect va avea un cod intern care este generat automat și poate fi accesat dacă este necesar.

Să stabilim prețul pentru produs/magazin/furnizor. Se poate schimba în timp, așa că haideți să adăugăm un câmp de timp la tabel. Voi sări peste declararea tabelelor pentru directoare dintr-o bază de date relațională pentru a scurta codul:

Relațional

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

Funcţional

CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);

Indici

Pentru ultimul exemplu, vom construi un index pe toate cheile și data, astfel încât să putem găsi rapid prețul pentru o anumită oră.

Relațional

CREATE INDEX prices_date
    ON prices
    (skuId, storeId, supplierId, dateTime)

Funcţional

INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);

sarcini

Să începem cu probleme relativ simple luate din cele corespunzătoare articole pe Habr.

Mai întâi, să declarăm logica domeniului (pentru baza de date relațională acest lucru se face direct în articolul de mai sus).

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

Sarcina 1.1

Afișați o listă cu angajații care primesc un salariu mai mare decât cel al supervizorului lor imediat.

Relațional

select a.*
from   employee a, employee b
where  b.id = a.chief_id
and    a.salary > b.salary

Funcţional

SELECT name(Employee a) WHERE salary(a) > salary(chief(a));

Sarcina 1.2

Enumerați angajații care primesc salariul maxim în departamentul lor

Relațional

select a.*
from   employee a
where  a.salary = ( select max(salary) from employee b
                    where  b.department_id = a.department_id )

Funcţional

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

Ambele implementări sunt echivalente. Pentru primul caz, într-o bază de date relațională poți folosi CREATE VIEW, care în același mod va calcula mai întâi salariul maxim pentru un anumit departament din ea. În cele ce urmează, pentru claritate, voi folosi primul caz, deoarece reflectă mai bine soluția.

Sarcina 1.3

Afișează o listă de ID-uri de departament, numărul de angajați în care nu depășește 3 persoane.

Relațional

select department_id
from   employee
group  by department_id
having count(*) <= 3

Funcţional

countEmployees 'Количество сотрудников' (Department d) = 
    GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;

Sarcina 1.4

Afișați o listă de angajați care nu au un manager desemnat care lucrează în același departament.

Relațional

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

Funcţional

SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));

Sarcina 1.5

Găsiți o listă de ID-uri de departament cu salariul total maxim al angajaților.

Relațional

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 )

Funcţional

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();

Să trecem la sarcini mai complexe de la alta articole. Conține o analiză detaliată a modului de implementare a acestei sarcini în MS SQL.

Sarcina 2.1

Ce vânzători au vândut mai mult de 1997 de unități de produs nr. 30 în 1?

Logica domeniului (ca și înainte pe RDBMS, omitem declarația):

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

Relațional

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

Funcţional

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;

Sarcina 2.2

Pentru fiecare cumpărător (nume, prenume), găsiți cele două bunuri (nume) pe care cumpărătorul a cheltuit cei mai mulți bani în 1997.

Extindem logica domeniului din exemplul anterior:

CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);

customer = DATA Customer (Order);

unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);

Relațional

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

Funcţional

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;

Operatorul PARTITION funcționează pe următorul principiu: însumează expresia specificată după SUM (aici 1), în cadrul grupurilor specificate (aici Client și An, dar poate fi orice expresie), sortând în cadrul grupurilor după expresiile specificate în COMANDA ( aici cumpărat, iar dacă este egal, atunci conform codului intern al produsului).

Sarcina 2.3

Câte mărfuri trebuie comandate de la furnizori pentru a onora comenzile curente.

Să extindem din nou logica domeniului:

CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);

supplier = DATA Supplier (Product);

unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);

Relațional

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

Funcţional

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ă cu un asterisc

Iar ultimul exemplu este de la mine personal. Există logica unei rețele de socializare. Oamenii pot fi prieteni unii cu alții și se pot plăcea. Din perspectiva bazei de date funcționale, ar arăta astfel:

CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);

Este necesar să găsiți posibili candidați pentru prietenie. Mai formal, trebuie să găsiți toți oamenii A, B, C astfel încât A să fie prieten cu B și B să fie prieten cu C, lui A îi place C, dar A nu este prieten cu C.
Din perspectiva bazei de date funcționale, interogarea ar arăta astfel:

SELECT Person a, Person b, Person c WHERE 
    likes(a, c) AND NOT friends(a, c) AND 
    friends(a, b) AND friends(b, c);

Cititorul este încurajat să rezolve singur această problemă în SQL. Se presupune că există mult mai puțini prieteni decât oamenii care vă plac. Prin urmare, sunt în tabele separate. Dacă are succes, există și o sarcină cu două stele. În ea, prietenia nu este simetrică. Pe o bază de date funcțională ar arăta astfel:

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: soluție la problema cu primul și al doilea asterisc de la 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 

Concluzie

Trebuie remarcat faptul că sintaxa limbajului dat este doar una dintre opțiunile de implementare a conceptului dat. SQL a fost luat ca bază, iar scopul a fost ca acesta să fie cât mai asemănător cu acesta. Desigur, unora nu le plac numele cuvintelor cheie, registrelor de cuvinte etc. Principalul lucru aici este conceptul în sine. Dacă doriți, puteți face atât C++ cât și Python sintaxa similară.

Conceptul de bază de date descris, în opinia mea, are următoarele avantaje:

  • Ușura. Acesta este un indicator relativ subiectiv care nu este evident în cazuri simple. Dar dacă te uiți la cazuri mai complexe (de exemplu, probleme cu asteriscuri), atunci, în opinia mea, scrierea unor astfel de interogări este mult mai ușoară.
  • Инкапсуляция. În unele exemple am declarat funcții intermediare (de exemplu, vândut, au cumpărat etc.), din care s-au construit funcții ulterioare. Acest lucru vă permite să schimbați logica anumitor funcții, dacă este necesar, fără a modifica logica celor care depind de ele. De exemplu, puteți face vânzări vândut au fost calculate din obiecte complet diferite, în timp ce restul logicii nu se va schimba. Da, acest lucru poate fi implementat într-un RDBMS folosind CREATE VIEW. Dar dacă toată logica este scrisă în acest fel, nu va părea foarte ușor de citit.
  • Fără decalaj semantic. O astfel de bază de date operează pe funcții și clase (în loc de tabele și câmpuri). La fel ca în programarea clasică (dacă presupunem că o metodă este o funcție cu primul parametru sub forma clasei căreia îi aparține). În consecință, ar trebui să fie mult mai ușor să ne „împrieteniți” cu limbaje de programare universale. În plus, acest concept permite implementarea unor funcționalități mult mai complexe. De exemplu, puteți încorpora operatori precum:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE  'Что-то Петя продает слишком много одного товара в 2019 году';

  • Moștenirea și polimorfismul. Într-o bază de date funcțională, puteți introduce moștenirea multiplă prin CLASS ClassP: constructe Class1, Class2 și implementați polimorfismul multiplu. Probabil voi scrie cum exact în articolele viitoare.

Chiar dacă acesta este doar un concept, avem deja o implementare în Java care traduce toată logica funcțională în logică relațională. În plus, logica reprezentărilor și o mulțime de alte lucruri îi sunt atașate frumos, datorită cărora obținem un întreg platformă. În esență, folosim RDBMS (deocamdată doar PostgreSQL) ca „mașină virtuală”. Uneori apar probleme cu această traducere, deoarece optimizatorul de interogări RDBMS nu cunoaște anumite statistici pe care le cunoaște FDBMS. În teorie, este posibil să se implementeze un sistem de management al bazelor de date care să folosească o anumită structură ca stocare, adaptată special pentru logica funcțională.

Sursa: www.habr.com

Adauga un comentariu