Database funzionale

Il mondo dei database è da tempo dominato dai sistemi di gestione di database relazionali, nei quali si utilizza il linguaggio SQL. Tanto che le nuove varianti sono state ribattezzate NoSQL. Queste sono riuscite a ritagliarsi il loro spazio in questo mercato, ma i sistemi relazionali non hanno intenzione di scomparire e continuano a essere utilizzati attivamente per i loro scopi.

In questo articolo desidero descrivere il concetto di database funzionale. Per una migliore comprensione, farò ciò confrontando con il modello relazionale classico. Per gli esempi, utilizzerò compiti tratti da vari test di SQL trovati online.

Introduzione

I database relazionali operano con tabelle e campi. In un database funzionale verranno utilizzati classi e funzioni, rispettivamente. Un campo in una tabella con N chiavi sarà rappresentato come funzione di N parametri. Al posto delle relazioni tra le tabelle si utilizzeranno funzioni che restituiscono oggetti della classe a cui si fa riferimento. Al posto di JOIN si utilizzerà la composizione di funzioni.

Prima di entrare nei dettagli delle attività, descriverò il compito della logica di dominio. Per DDL utilizzerò la sintassi PostgreSQL. Per la funzionalità utilizzerò la mia sintassi.

Tabelle e campi

Un semplice oggetto Sku con i 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;
nome = STRINGA DATI[100] (Sku);
prezzo = NUMERICO DATI[10,5] (Sku);

Dichiareremo due funzioni, che accettano un parametro Sku e restituiscono un tipo primitivo.

Si presume che in un DBMS funzionale ogni oggetto avrà un codice interno che viene generato automaticamente e al quale è possibile accedere se necessario.

Imposteremo il prezzo per prodotto / negozio / fornitore. Può cambiare nel tempo, quindi aggiungeremo alla tabella un campo tempo. Passerò la dichiarazione delle tabelle per i dizionari nel database relazionale per ridurre 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 Negozio;
CLASS Fornitore;
dateTime = DATA DATETIME (Sku, Negozio, Fornitore);
price = DATA NUMERIC[10,5] (Sku, Negozio, Fornitore);

Indici

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

Relazionale

CREA INDICE prices_date
    SU prezzi
    (skuId, storeId, supplierId, dateTime)

Funzionale

INDICE Sku sk, Negozio st, Fornitore sp, dataTime(sk, st, sp);

Attività

Iniziamo con compiti relativamente semplici, presi dalla corrispondente articolo su Habr.

Iniziamo dichiarando la logica di dominio (per un database relazionale, questo è fatto direttamente nell'articolo fornito).

CLASS Dipartimento;
nome = STRINGA DATI[100] (Dipartimento);

CLASSE Employee;
department = DATI Department (Employee);
chief = DATI Employee (Employee);
name = DATI STRING[100] (Employee);
salary = DATI NUMERIC[14,2] (Employee);

Compito 1.1

Visualizzare l'elenco dei dipendenti che guadagnano uno stipendio maggiore di quello del proprio supervisore diretto.

Relazionale

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

Funzionale

SELEZIONA name(Dipendente a) DOVE stipendio(a) > stipendio(Capo(a));

Compito 1.2

Visualizzare l'elenco dei dipendenti che ricevono il massimo stipendio nel proprio 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 'Salario massimo' (Dipartimenti) = 
    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, nel database relazionale, si può utilizzare CREATE VIEW, che calcolerà in questo modo prima il massimo stipendio per un determinato dipartimento. In seguito, per maggiore chiarezza, utilizzerò il primo caso, poiché riflette meglio la soluzione.

Compito 1.3

Visualizza un elenco di ID dei reparti con un numero di dipendenti non superiore a 3.

Relazionale

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

Funzionale

countEmployees 'Numero di dipendenti' (Dipartimento d) = 
    SOMMA GRUPPO 1 SE dipartimento (Dipendente e) = d;
SELEZIONA Dipartimento d DOVE countEmployees(d) <= 3;

Compito 1.4

Visualizza un elenco di dipendenti senza un capo assegnato che lavorano 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

SELEZIONA nome(impiegato a) DOVE NON (dipartimento(capo(a)) = dipartimento(a));

Compito 1.5

Trova un elenco di ID dei reparti con il massimo stipendio totale 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 'Salario massimo' (Dipartimento d) = 
    GROUP SUM salary (Dipendente e) IF department(e) = d;
maxSalarySum 'Salario massimo dei dipartimenti' () = 
    GROUP MAX salarySum (Dipartimento d);
SELECT Dipartimento d WHERE salarySum(d) = maxSalarySum();

Passiamo a compiti più complessi da un'altra articolo. In essa c'è un'analisi dettagliata su come implementare questo compito in MS SQL.

Compito 2.1

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

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

CLASS Employee 'Venditore';
lastName 'Cognome' = DATA STRING[100] (Employee);

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

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

CLASS Detail 'Riga d'ordine';

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

venduto (Dipendente e, INTERO productId, INTERO anno) = 
    SOMMA GRUPPO quantità(DettaglioOrdine d) SE 
        dipendente(ordina(d)) = e E 
        id(prodotto(d)) = productId E 
        estraiAnno(data(ordina(d))) = anno;
SELEZIONA cognome(Dipendente e) DOVE venduto(e, 1, 1997) > 30;

Compito 2.2

Per ciascun cliente (nome, cognome), trovare due prodotti (nome) sui quali il cliente ha speso di più nel 1997.

Espandiamo la logica di dominio dall'esempio precedente:

CLASS Customer 'Cliente';
contactName 'Nome e Cognome' = DATA STRING[100] (Cliente);

cliente = DATA Cliente (Ordine);

prezzoUnitario = DATA NUMERIC[14,2] (Dettaglio);
sconto = DATA NUMERIC[6,2] (Dettaglio);

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

somma (Dettaglio d) = quantità(d) * prezzoUnitario(d) * (1 - sconto(d));
acquistato 'Acquistato' (Cliente c, Prodotto p, INTERO y) = 
    SOMMA GRUPPO somma(Dettaglio d) SE 
        cliente(forzaOrdine(d)) = c E 
        prodotto(d) = p E 
        estraiAnno(data(forzaOrdine(d))) = y;
valutazione 'Valutazione' (Cliente c, Prodotto p, INTERO y) = 
    SOMMA PARTIZIONE 1 ORDINAMENTO DISCENTE acquistato(c, p, y), p PER c, y;
SELEZIONA nomeContatto(Cliente c), nome(Prodotto p) DOVE valutazione(c, p, 1997) < 3;

L'operatore PARTITION funziona nel seguente modo: somma l'espressione indicata dopo SUM (qui 1), all'interno dei gruppi specificati (qui Cliente e Anno, ma può essere qualsiasi espressione), ordinando all'interno dei gruppi in base alle espressioni indicate in ORDER (qui acquisto, e in caso di parità, per il codice prodotto interno).

Compito 2.3

Quanti prodotti devono essere ordinati dai fornitori per soddisfare gli ordini attuali.

Ancora una volta espandiamo la logica di dominio:

CLASS Fornitore 'Fornitore';
companyName = DATA STRING[100] (Fornitore);

fornitore = DATA Fornitore (Prodotto);

unitsInStock 'Disponibilità' = DATA NUMERICA[10,3] (Prodotto);
reorderLevel 'Norma di vendita' = DATA NUMERICA[10,3] (Prodotto);

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

ordinatoNonSpedito 'Ordine effettuato, ma non spedito' (Prodotto p) = 
    SOMMA GRUPPO quantità(DettaglioOrdine d) SE prodotto(d) = p;
daOrdinare 'Da ordinare' (Prodotto p) = ordinatoNonSpedito(p) + livelloRiordino(p) - unitàInMagazzino(p);
SELEZIONA nomeAzienda(fornitore(Prodotto p)), nome(p), daOrdinare(p) DOVE daOrdinare(p) > 0;

Compito con stella

E l'ultimo esempio, personale da parte mia. Esiste una logica di rete sociale. Le persone possono essere amiche tra loro e piacere reciprocamente. Dal punto di vista della base dati funzionale, questo apparirà come segue:

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

È necessario trovare potenziali candidati per l'amicizia. In modo più formale, bisogna cercare tutte le persone A, B, C tali che A sia amico di B, B sia amico di C, A piaccia a C, ma A non sia amico di C.
Dal punto di vista della base dati funzionale, la query apparirà come segue:

SELEZIONA Persona a, Persona b, Persona c DOVE 
    mi piace(a, c) E NON amici(a, c) E 
    amici(a, b) E amici(b, c);

Si invita il lettore a risolvere autonomamente questo compito in SQL. Si presume che ci siano molte meno amicizie rispetto a chi piace. Pertanto, sono in tabelle separate. In caso di soluzione riuscita, c'è anche un compito con due stelle. Qui l'amicizia non è simmetrica. Su un database funzionale apparirà così:

SELEZIONA Persona a, Persona b, Persona c DOVE 
    mi piace(a, c) E NON amici(a, c) E 
    (amici(a, b) O amici(b, a)) E 
    (amici(b, c) O amici(c, b));

UPD: soluzione per il compito con la prima e la seconda stella da 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'
--Still not friends         
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
--Mi piace                      
JOIN PersonRelationShipCollapsed  AS pl ON pl.PersonAID = p.PersonID
                                 AND pl.Relation  = 'Like'                                  
--Amici                          
JOIN PersonRelationShipCollapsed  AS pf ON pf.PersonAID = p.PersonID 
                                 AND pf.Relation = 'Friend'
--Amici degli Amici               
JOIN PersonRelationShipCollapsed  AS pff ON pff.PersonAID = pf.PersonBID
                                 AND pff.PersonBID = pl.PersonBID
                                 AND pff.Relation = 'Friend'
--Ancora non amici                
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

È importante notare che la sintassi presentata è solo una delle possibili implementazioni del concetto descritto. È stata scelta in particolare la sintassi SQL e l'obiettivo era farla somigliare il più possibile ad essa. Certamente, a qualcuno potrebbero non piacere i nomi delle parole chiave, i registri delle parole e altro. Qui l'importante è proprio il concetto stesso. Se lo si desidera, si potrebbe realizzare anche una sintassi simile in C++ o Python.

A mio avviso, il concetto descritto della base di dati presenta i seguenti vantaggi:

  • Semplicità. Questo è un indicatore relativamente soggettivo, che non è evidente nei casi semplici. Ma se si considerano casi più complessi (ad esempio, compiti con stelle), scrivere tali query è, secondo me, significativamente più semplice.
  • Incapsulamento. In alcuni esempi, ho dichiarato funzioni intermedie (ad esempio, venduto, acquistato ecc.), da cui sono state costruite le funzioni successive. Questo consente di modificare la logica di determinate funzioni senza cambiare la logica delle funzioni dipendenti. Ad esempio, si potrebbe fare in modo che le vendite venduto vengano calcolate da oggetti completamente diversi, mantenendo invariata la logica restante. Sì, in un RDBMS questo si può realizzare tramite CREATE VIEW. Ma se tutta la logica viene scritta in questo modo, sembrerà non molto leggibile.
  • Assenza di discontinuità semantica. Questo database funziona con funzioni e classi (anziché con tabelle e campi). Proprio come nella programmazione classica (se si considera che un metodo sia una funzione con il primo parametro rappresentante la classe a cui appartiene). Di conseguenza, integrarlo con linguaggi di programmazione universali dovrebbe essere significativamente più semplice. Inoltre, questo concetto consente di implementare funzioni molto più complesse. Ad esempio, è possibile incorporare nel database operatori del tipo:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Petya' MESSAGE 'Qualcosa Petya sta vendendo troppo del stesso prodotto nel 2019';

  • Ereditarietà e polimorfismo. In un database funzionale, è possibile introdurre eredità multipla attraverso la costruzione CLASS ClassP: Class1, Class2 e implementare polimorfismo multiplo. Come esattamente, forse ne scriverò nei prossimi articoli.

Nonostante sia solo un concetto, abbiamo già una certa implementazione in Java che traduce tutta la logica funzionale in logica relazionale. Inoltre, è stata integrata elegantemente la logica delle rappresentazioni e molto altro, il che porta a un complesso la piattaforma. In sostanza, utilizziamo un RDBMS (al momento solo PostgreSQL) come "macchina virtuale". In questo tipo di traduzione, a volte si verificano problemi, poiché l'ottimizzatore di query dell'RDBMS non conosce alcune statistiche specifiche di cui è a conoscenza il DBMS. In teoria, si potrebbe implementare un sistema di gestione di database che utilizzi una struttura adattata specificamente alla logica funzionale.

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server 🔥 Acquista hosting affidabile per siti web con protezione DDoS, VPS VDS server | ProHoster