DBMS funzionale

Il mondo dei database è stato a lungo dominato dai DBMS relazionali, che utilizzano il linguaggio SQL. Tanto che le varianti emergenti si chiamano NoSQL. Sono riusciti a ritagliarsi un certo posto in questo mercato, ma i DBMS relazionali non moriranno e continueranno ad essere utilizzati attivamente per i loro scopi.

In questo articolo voglio descrivere il concetto di database funzionale. Per una migliore comprensione lo farò confrontandolo con il modello relazionale classico. Verranno utilizzati come esempi i problemi derivanti da vari test SQL trovati su Internet.

Introduzione

I database relazionali operano su tabelle e campi. In un database funzionale verranno invece utilizzate rispettivamente classi e funzioni. Un campo in una tabella con N chiavi verrà rappresentato in funzione di N parametri. Al posto delle relazioni tra tabelle verranno utilizzate funzioni che restituiscono oggetti della classe a cui viene effettuata la connessione. Verrà utilizzata la composizione della funzione anziché JOIN.

Prima di passare direttamente ai compiti, descriverò il compito della logica del dominio. Per DDL userò la sintassi PostgreSQL. Per funzionale ha una propria sintassi.

Tabelle e campi

Un semplice oggetto Sku con campi nome e prezzo:

Relazionale

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

Funzionale

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

Ne annunciamo due funzioni, che accettano un parametro Sku come input e restituiscono un tipo primitivo.

Si presuppone che in un DBMS funzionale ogni oggetto avrà del codice interno che viene generato automaticamente e a cui è possibile accedere se necessario.

Impostiamo il prezzo per il prodotto/negozio/fornitore. Potrebbe cambiare nel tempo, quindi aggiungiamo un campo orario alla tabella. Tralascerò la dichiarazione delle tabelle per le directory in un database relazionale per abbreviare il codice:

Relazionale

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

Funzionale

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

Indici

Per l'ultimo esempio, costruiremo un indice su tutte le chiavi e sulla data in modo da poter trovare rapidamente il prezzo per un momento specifico.

Relazionale

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

Funzionale

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

compiti

Cominciamo con problemi relativamente semplici presi dal corrispondente articoli su Habr.

Per prima cosa dichiariamo la logica del dominio (per il database relazionale questo viene fatto direttamente nell'articolo precedente).

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

Attività 1.1

Visualizza un elenco di dipendenti che ricevono uno stipendio superiore a quello del loro diretto supervisore.

Relazionale

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

Funzionale

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

Attività 1.2

Elenca i dipendenti che ricevono lo stipendio massimo nel loro dipartimento

Relazionale

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

Funzionale

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

Entrambe le implementazioni sono equivalenti. Per il primo caso, in un database relazionale è possibile utilizzare CREATE VIEW, che allo stesso modo calcolerà prima lo stipendio massimo per un dipartimento specifico al suo interno. Nel seguito, per chiarezza, utilizzerò il primo caso, poiché riflette meglio la soluzione.

Attività 1.3

Visualizza un elenco di ID di reparto, il numero di dipendenti in cui non supera le 3 persone.

Relazionale

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

Funzionale

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

Attività 1.4

Visualizza un elenco di dipendenti che non hanno un manager designato che lavora nello stesso reparto.

Relazionale

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

Funzionale

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

Attività 1.5

Trova un elenco di ID dipartimento con lo stipendio totale massimo dei dipendenti.

Relazionale

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 )

Funzionale

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

Passiamo a compiti più complessi da un altro articoli. Contiene un'analisi dettagliata di come implementare questa attività in MS SQL.

Attività 2.1

Quali venditori hanno venduto più di 1997 unità del prodotto n. 30 nel 1?

Logica del dominio (come prima su RDBMS saltiamo la dichiarazione):

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

Relazionale

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

Funzionale

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;

Attività 2.2

Per ciascun acquirente (nome, cognome), trova i due beni (nome) per i quali l'acquirente ha speso più denaro nel 1997.

Estendiamo la logica del dominio dall'esempio precedente:

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

customer = DATA Customer (Order);

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

Relazionale

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

Funzionale

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;

L'operatore PARTITION funziona secondo il seguente principio: somma l'espressione specificata dopo SUM (qui 1), all'interno dei gruppi specificati (qui Cliente e Anno, ma potrebbe essere qualsiasi espressione), ordinando all'interno dei gruppi in base alle espressioni specificate nell'ORDINE ( qui acquistato e, se uguale, secondo il codice prodotto interno).

Attività 2.3

Quante merci devono essere ordinate ai fornitori per soddisfare gli ordini correnti.

Espandiamo nuovamente la logica del dominio:

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

supplier = DATA Supplier (Product);

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

Relazionale

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

Funzionale

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;

Problema con un asterisco

E l'ultimo esempio è mio. C’è la logica di un social network. Le persone possono essere amiche tra loro e piacersi. Dal punto di vista del database funzionale sarebbe simile a questo:

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

È necessario trovare possibili candidati all'amicizia. Più formalmente, devi trovare tutte le persone A, B, C in modo tale che A sia amico di B e B sia amico di C, ad A piaccia C, ma A non è amico di C.
Dal punto di vista del database funzionale, la query sarebbe simile a questa:

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

Il lettore è incoraggiato a risolvere da solo questo problema in SQL. Si presume che ci siano molti meno amici delle persone che ti piacciono. Pertanto si trovano in tabelle separate. In caso di successo, c'è anche un compito con due stelle. In esso, l'amicizia non è simmetrica. Su un database funzionale sarebbe simile a questo:

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: soluzione al problema con il primo e il secondo asterisco da 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 

conclusione

Va notato che la sintassi della lingua data è solo una delle opzioni per implementare il concetto dato. Come base è stato preso SQL e l'obiettivo era che fosse il più simile possibile ad esso. Naturalmente, ad alcuni potrebbero non piacere i nomi delle parole chiave, i registri delle parole, ecc. La cosa principale qui è il concetto stesso. Se lo si desidera, è possibile creare una sintassi simile sia per C++ che per Python.

Il concetto di database descritto, a mio avviso, presenta i seguenti vantaggi:

  • Alleviare. Questo è un indicatore relativamente soggettivo che non è evidente nei casi semplici. Ma se guardi casi più complessi (ad esempio, problemi con gli asterischi), allora, a mio avviso, scrivere tali query è molto più semplice.
  • Инкапсуляция. In alcuni esempi ho dichiarato funzioni intermedie (ad esempio, venduto, comprato ecc.), da cui sono state costruite le funzioni successive. Ciò consente di modificare, se necessario, la logica di alcune funzioni, senza modificare la logica di quelle che da esse dipendono. Ad esempio, puoi effettuare vendite venduto sono stati calcolati da oggetti completamente diversi, mentre il resto della logica non cambierà. Sì, questo può essere implementato in un RDBMS utilizzando CREATE VIEW. Ma se tutta la logica è scritta in questo modo, non sembrerà molto leggibile.
  • Nessuna lacuna semantica. Un database di questo tipo opera su funzioni e classi (invece che su tabelle e campi). Esattamente come nella programmazione classica (se assumiamo che un metodo sia una funzione con il primo parametro nella forma della classe a cui appartiene). Di conseguenza, dovrebbe essere molto più semplice “fare amicizia” con i linguaggi di programmazione universali. Inoltre, questo concetto consente di implementare funzionalità molto più complesse. Ad esempio, puoi incorporare operatori come:

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

  • Ereditarietà e polimorfismo. In un database funzionale, è possibile introdurre l'ereditarietà multipla tramite i costrutti CLASS ClassP: Class1, Class2 e implementare il polimorfismo multiplo. Probabilmente scriverò come esattamente nei prossimi articoli.

Anche se questo è solo un concetto, abbiamo già qualche implementazione in Java che traduce tutta la logica funzionale in logica relazionale. Inoltre, la logica delle rappresentazioni e molte altre cose sono meravigliosamente collegate ad essa, grazie alle quali otteniamo il tutto piattaforma. In sostanza, utilizziamo l'RDBMS (solo PostgreSQL per ora) come una “macchina virtuale”. A volte sorgono problemi con questa traduzione perché l'ottimizzatore delle query RDBMS non conosce alcune statistiche conosciute dall'FDBMS. In teoria, è possibile implementare un sistema di gestione di database che utilizzerà una determinata struttura come archiviazione, adattata specificamente alla logica funzionale.

Fonte: habr.com

Aggiungi un commento