Bază de date funcțională

Lumea bazelor de date este de mult timp ocupată de SGBD-urile relaționale, care folosesc limbajul SQL. Atât de mult, încât variantele apărute sunt numite NoSQL. Acestea au reușit să-și câștige un anumit loc pe această piață, dar SGBD-urile relaționale nu au de gând să dispară și continuă să fie utilizate activ pentru scopurile lor.

În acest articol vreau să descriu conceptul de bază de date funcțională. Pentru o mai bună înțelegere, voi face asta prin compararea cu modelul relațional clasic. Ca exemple vor fi folosite sarcini din diverse teste SQL găsite pe internet.

Introducere

Baze de date relaționale operează cu tabele și câmpuri. În baza de date funcțională, în locul acestora, vor fi folosite clase și funcții, respectiv. Un câmp dintr-un tabel cu N chei va fi reprezentat ca o funcție cu N parametrii. În locul relațiilor între tabele vor fi folosite funcții care returnează obiecte ale clasei la care se face referire. În locul JOIN-ului va fi folosită compunerea funcțiilor.

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

Tabele și câmpuri

Un obiect simplu Sku cu câmpurile denumire ș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

CLASĂ Sku;
nume = ȘIR DE DATE[100] (Sku);
preț = NUMERIC DE DATE[10,5] (Sku);

Declarăm două funcții, care primesc ca parametru un Sku și returnează un tip primitiv.

Se presupune că în SGBD-ul funcțional, fiecare obiect va avea un anumit cod intern, care este generat automat și la care se poate apela dacă este necesar.

Să stabilim prețul pentru produs / magazin / furnizor. Acesta poate varia în timp, așa că adăugăm un câmp pentru timp în tabel. Voi ignora declarația tabelelor pentru dicționare în baza de date relațională pentru a reduce 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

CLASĂ Sku;
CLASIC Magazin;
CLASIC Furnizor;
dateTime = DATE DATETIME (Sku, Magazin, Furnizor);
preț = DATA NUMERIC[10,5] (Sku, Magazin, Furnizor);

Indecși

Pentru ultimul exemplu, vom construi un index pe toate cheile și data, pentru a putea găsi rapid prețul pentru un anumit moment.

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

Vom începe cu sarcini relativ simple, preluate din corespunzătoarea recomandările sunt aplicabile coronavirusurilor în general și COVID-19 în particular. Așadar, recomand să descărcați și să printați articolul (pentru cei interesați de acest subiect). pe Habr.

Mai întâi, să declarăm logica de domeniu (pentru o bază de date relațională, acest lucru este realizat direct în articolul prezentat).

CLASA Departament;
nume = ȘIR DE DATE[100] (Departament);

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 lista angajaților care câștigă un salariu mai mare decât cel al superiorului direct.

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

Afișați lista angajaților care au 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 'Salariul maxim' (Departamentul s) = 
    GRUPA MAX salariu (Angajat e) DACĂ departamentul(e) = s;
SELECTAȚI numele (Angajat a) UNDE salariu(a) = maxSalary(departamentul(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, în baza de date relațională se poate utiliza CREATE VIEW, care astfel va calcula mai întâi salariul maxim pentru un departament specific. În continuare, pentru claritate, voi folosi primul caz, deoarece reflectă mai bine soluția.

Sarcina 1.3

Afișați lista ID-urilor departamentelor, 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 'Numărul de angajați' (Departamentul d) = 
    GROUP SUM 1 IF department(Employee e) = d;
SELECT Departamentul d WHERE countEmployees(d) <= 3;

Sarcina 1.4

Afișați lista angajaților care nu au un superior 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 lista ID-urilor departamentelor 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 'Salariul maxim' (Department d) = 
    GROUP SUM salary(Employee e) IF department(e) = d;
maxSalarySum 'Salariul maxim al departamentelor' () = 
    GROUP MAX salarySum(Department d);
SELECT Department d WHERE salarySum(d) = maxSalarySum();

Să trecem la sarcini mai complexe din altă recomandările sunt aplicabile coronavirusurilor în general și COVID-19 în particular. Așadar, recomand să descărcați și să printați articolul (pentru cei interesați de acest subiect).. Aceasta conține o descriere detaliată a modului de implementare a acestei sarcini pe MS SQL.

Sarcina 2.1

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

Logica de domeniu (ca și anterior, la RDBMS, săriți declarația):

CLASS Employee 'Vânzător';
lastName 'Nume' = DATA STRING[100] (Employee);

CLASS Product 'Produs';
id = DATA INTEGER (Product);
name = DATA STRING[100] (Product);

CLASS Order 'Comandă';
date = DATA DATE (Order);
employee = DATA Employee (Order);

CLASS Detail 'Rândul comenzii';

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

vândut (Angajat e, INTEGER productId, INTEGER an) = 
    GRUPA SUMĂ cantitate(OrderDetail d) IF 
        angajat(order(d)) = e ȘI 
        id(product(d)) = productId ȘI 
        extrageAn(data(order(d))) = an;
SELECT lastName(Angajat e) CÂND vândut(e, 1, 1997) > 30;

Sarcina 2.2

Pentru fiecare client (nume, prenume) găsiți două produse (denumire) pe care clientul a cheltuit cei mai mulți bani în anul 1997.

Extindem logica de domeniu din exemplul anterior:

CLASS Customer 'Client';
contactName 'Nume complet' = DATA STRING[100] (Customer);

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

suma (Detaliu d) = cantitate(d) * pretUnitar(d) * (1 - discount(d));
cumpărat 'Купил' (Client c, Produs p, NUMĂR întreg y) = 
    GRUP SUMA sum(Detaliu d) ÎN CAZ 
        client(ordonare(d)) = c ȘI 
        produs(d) = p ȘI 
        extrageAnul(data(comanda(d))) = y;
rating 'Рейтинг' (Client c, Produs p, NUMĂR întreg y) = 
    SUMA PARTIȚIONATĂ 1 ORDINE DESC cumpărat(c, p, y), p GRUPAT după c, y;
SELECTEAZĂ contactName(Client c), nume(Produs p) UNDE rating(c, p, 1997) < 3;

Operatorul PARTITION funcționează după următorul principiu: acesta sumază 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 ORDER (aici cumpărat, iar în cazul egalității, după codul intern al produsului).

Sarcina 2.3

Câte produse trebuie comandate de la furnizori pentru a îndeplini comenzile actuale.

Extindem din nou logica domeniului:

CLASĂ Furnizor 'Furnizor';
numeCompanie = ȘIR DE DATE[100] (Furnizor);

supplier = DATA Supplier (Product);

unitsInStock 'Stoc disponibil' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Norma de vânzare' = 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

comandatNeexpediat 'Comandat, dar neexpediat' (Produs p) = 
    SUMA GRUP de cantitate(DetaliuComandă d) DACĂ produs(d) = p;
pentruComandă 'Pentru comandă' (Produs p) = comandatNeexpediat(p) + nivelRecomandare(p) - unitățiÎnStoc(p);
SELECTAȚI numeCompanie(furnizor(Produs p)), nume(p), pentruComandă(p) UNDE pentruComandă(p) > 0;

Sarcina cu stea

Și ultimul exemplu personal. Există o logică a rețelei sociale. Oamenii pot fi prieteni unii cu alții și se pot plăcea unii pe alții. Din perspectiva unei baze de date funcționale, aceasta ar arăta astfel:

CLASA Person;
like = DATE BOOLEAN (Person, Person);
prieteni = DATE BOOLEAN (Person, Person);

Este necesar să găsim potențiali candidați pentru prietenie. Mai formal, trebuie să găsim toate persoanele A, B, C astfel încât A este prieten cu B, și B este prieten cu C, A îi place C, dar A nu este prieten cu C.
Din perspectiva unei baze de date funcționale, interogarea ar arăta astfel:

SELECT Persoană a, Persoană b, Persoană c UNDE 
    îi place(a, c) ȘI NU sunt prieteni(a, c) ȘI 
    sunt prieteni(a, b) ȘI sunt prieteni(b, c);

Cititorului i se oferă ocazia de a rezolva această sarcină pe SQL. Se presupune că prietenii sunt mult mai puțini decât cei care îi plac. Prin urmare, aceștia se află în tabele separate. În cazul unei soluții reușite, există de asemenea o sarcină cu două stele. Aici prietenia nu este simetrică. Pe baza funcționalității bazei de date, aceasta ar arăta astfel:

SELECT Persoană a, Persoană b, Persoană c UNDE 
    îi place(a, c) ȘI NU sunt prieteni(a, c) ȘI 
    (prieteni(a, b) OR prieteni(b, a)) AND 
    (prieteni(b, c) OR prieteni(c, b));

UPD: soluția sarcinii cu prima și a doua stea de la dss_kalika:

SELECT 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
FROM Persons                 AS p
--Like                      
JOIN PersonRelationShip      AS pl ON pl.PersonAID = p.PersonID
                                  AND pl.Relation  = 'Like'
--Friends                     
JOIN PersonRelationShip      AS pf ON pf.PersonAID = p.PersonID 
                                  AND pf.Relation = 'Friend'
--Friends of Friends              
JOIN PersonRelationShip      AS pff ON pff.PersonAID = pf.PersonBID
                                   AND pff.PersonBID = pl.PersonBID
                                   AND pff.Relation = 'Friend'
--Not Friends Yet         
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
--Like                      
JOIN PersonRelationShipCollapsed  AS pl ON pl.PersonAID = p.PersonID
                                 AND pl.Relation  = 'Like'                                  
--Friends                          
JOIN PersonRelationShipCollapsed  AS pf ON pf.PersonAID = p.PersonID 
                                 AND pf.Relation = 'Friend'
--Friends of Friends                   
JOIN PersonRelationShipCollapsed  AS pff ON pff.PersonAID = pf.PersonBID
                                 AND pff.PersonBID = pl.PersonBID
                                 AND pff.Relation = 'Friend'
--Not Friends Yet                   
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

Este sintax al limbajului este doar una dintre variantele de implementare a conceptului prezentat. S-a folosit SQL ca model, iar scopul a fost să fie cât mai asemănător cu acesta. Desigur, unora le pot displace denumirile cuvintelor cheie, stilul scrierii și multe altele. Aici cel mai important este conceptul în sine. Dacă se dorește, poate fi creată o sintaxă similară în C++ sau Python.

Conceptul descris al bazei de date, din punctul meu de vedere, are următoarele avantaje:

  • Simplitate. Este un indicator relativ subiectiv, care nu este evident în cazurile simple. Însă, dacă ne uităm la cazuri mai complexe (de exemplu, problemele cu stele), cred că a scrie astfel de interogări este mult mai simplu.
  • Încapsularea. În unele exemple, am declarat funcții intermediare (de exemplu, sold, bought ș.a.m.d.), pe baza cărora s-au construit funcțiile ulterioare. Acest lucru permite, dacă este necesar, modificarea logicii anumitor funcții fără a modifica logica funcțiilor care depind de ele. De exemplu, se poate face în așa fel încât vânzările sold se consideră din obiecte complet diferite, însă restul logicii nu se va schimba. Da, în RDBMS acest lucru poate fi realizat prin CREATE VIEW. Dar dacă întreaga logică este scrisă în acest mod, va părea mai puțin lizibilă.
  • Lipsa unei discontinuități semantice. Această bază de date operează cu funcții și clase (în loc de tabele și câmpuri). La fel ca în programarea clasică (dacă considerăm că metoda este o funcție cu primul parametru sub forma clasei corespunzătoare). Prin urmare, "a face prieteni" cu limbajele de programare universale ar trebui să fie semnificativ mai simplu. În plus, această concepție permite implementarea unor funcții mult mai complexe. De exemplu, se pot încorpora în baza de date operatori de tip:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Petya' MESSAGE 'Ceva, Petya vinde prea multe din același produs în 2019;'

  • Moștenire și polimorfism. În baza de date funcțională se poate introduce moștenire multiplă prin construcții de tip CLASS ClassP: Class1, Class2 și se poate implementa polimorfismul multiplu. Cum anume, poate voi scrie în articolele următoare.

Deși este doar o concepție, avem deja o anumită implementare în Java, care transpune toată logica funcțională în logica relațională. În plus, este frumos integrată logica vizualizărilor și multe altele, datorită cărora se obține un întreg platforma. Practic, folosim RDBMS (până acum doar PostgreSQL) ca o "mașină virtuală". În această translație apar uneori probleme, deoarece optimizatorul de interogări RDBMS nu cunoaște statistica specifică pe care o cunoaște FDBMS. Teoretic, se poate implementa un sistem de gestionare a bazelor de date, care va folosi ca stocare o anumită structură adaptată exact la logica funcțională.

Sursa: habr.com

Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS 🔥 Cumpără un hosting fiabil pentru site-uri cu protecție DDoS, servere VPS VDS | ProHoster