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 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 . 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 :
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 . 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
