Il mondo dei database è da tempo dominato dai DBMS relazionali, che utilizzano il linguaggio SQL. Così tanto che le nuove varianti vengono chiamate NoSQL. Sono riusciti a ritagliarsi un certo spazio in questo mercato, ma i DBMS relazionali non hanno intenzione di scomparire e continuano a essere utilizzati attivamente per i loro scopi.
In questo articolo voglio descrivere il concetto di database funzionale. Per una migliore comprensione, lo farò confrontando con il modello relazionale classico. Come esempi, utilizzerò problemi tratti da vari test SQL trovati su Internet.
Introduzione
I database relazionali operano su tabelle e campi. In un database funzionale, al loro posto verranno utilizzate classi e funzioni rispettivamente. Un campo in una tabella con N chiavi sarà rappresentato come una funzione con N parametri. Al posto delle relazioni tra le tabelle, verranno utilizzate funzioni che restituiscono oggetti della classe a cui si riferisce la relazione. Al posto di JOIN verrà utilizzata la composizione di funzioni.
Prima di passare direttamente ai problemi, descriverò l'incarico della logica di dominio. Per il DDL utilizzerò la sintassi di PostgreSQL. Per il funzionale, 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
CLASSE Sku;
nome = STRINGA DATI[100] (Sku);
prezzo = NUMERICO DATI[10,5] (Sku);
Dichiariamo 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 a cui si può accedere quando necessario.
Impostiamo un prezzo per il prodotto / negozio / fornitore. Può variare nel tempo, quindi aggiungiamo alla tabella un campo per il tempo. Salterò 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 senza fuso orario,
price numeric(10,5),
CONSTRAINT prices_pkey PRIMARY KEY (skuId, storeId, supplierId)
)
Funzionale
CLASSE Sku;
CLASSE Negozio;
CLASSE Fornitore;
dataTime = DATI DATETIME (Sku, Negozio, Fornitore);
prezzo = DATI NUMERICI[10,5] (Sku, Negozio, Fornitore);
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 determinato momento.
Relazionale
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Funzionale
INDICE Sku sk, Negozio st, Fornitore sp, dateTime(sk, st, sp);
Problemi
Iniziamo con problemi relativamente semplici, tratti dal corrispondente È essenzialmente vero che le persone hanno capito che il modello relazionale completo non è necessario per risolvere molti problemi, specialmente per quelli dove, ad esempio, le prestazioni sono fondamentali e dominano determinate semplici interrogazioni con aggregazione — è critico calcolare rapidamente le metriche e scriverle nel database, mentre la maggior parte delle funzionalità relazionali si è rivelata non necessaria e, anzi, dannosa — perché normalizzare qualcosa se questo compromettere l'aspetto più importante per noi (per un determinato compito) — le prestazioni?
Iniziamo dichiarando la logica di dominio (per il database relazionale questo è stato fatto direttamente nell'articolo fornito).
CLASSE Dipartimento;
nome = STRINGA DATI[100] (Dipartimento);
CLASS Employee;
department = DATA Department (Employee);
chief = DATA Employee (Employee);
name = DATA STRING[100] (Employee);
salary = DATA NUMERIC[14,2] (Employee);
Compito 1.1
Visualizzare l'elenco dei dipendenti con uno stipendio superiore a quello del proprio immediato superiore.
Relazionale
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Funzionale
SELEZIONA nome(Employee a) DOVE stipendio(a) > stipendio(capofamiglia(a));
Compito 1.2
Visualizzare l'elenco dei dipendenti che ricevono il massimo stipendio nel proprio reparto
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' (Department s) =
GROUP MAX salario(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 in questo modo calcolerà prima il massimo stipendio per un determinato reparto. In seguito, per chiarezza, utilizzerò il primo caso poiché riflette meglio la soluzione.
Compito 1.3
Visualizzare l'elenco degli ID dei reparti, il cui numero di dipendenti non supera 3.
Relazionale
select department_id
from employee
group by department_id
having count(*) <= 3
Funzionale
conteggioDipendenti 'Numero di dipendenti' (Dipartimento d) =
RAGGRUPPA somma 1 SE dipartimento (Dipendente e) = d;
SELECT Dipartimento d DOVE conteggioDipendenti(d) <= 3;
Compito 1.4
Visualizzare l'elenco dei dipendenti senza un superiore designato, 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(Dipendente a) DOVE NON (reparto(capo(a)) = reparto(a));
Compito 1.5
Trovare l'elenco degli 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 'Retribuzione Massima' (Dipartimento d) =
SOMMA GRUPPO stipendio (Dipendente e) SE reparto(e) = d;
maxSalarySum 'Retribuzione Massima dei Dipartimenti' () =
GROUP MAX salarySum(Dipartimento d);
SELECT Dipartimento d WHERE salarySum(d) = maxSalarySum();
Passiamo a compiti più complessi da un altro . Qui viene fornita un'analisi dettagliata su come implementare questo compito su MS SQL.
Compito 2.1
Quali venditori hanno venduto più di 30 pezzi del prodotto n. 1 nel 1997?
Logica di dominio (come prima, per RDBMS saltiamo la dichiarazione):
CLASSE Dipendente 'Venditore';
cognome 'Cognome' = DATA STRING[100] (Dipendente);
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;
SELECT 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 più soldi nel 1997.
Espandiamo la logica di dominio dall'esempio precedente:
CLASS Customer 'Cliente';
contactName 'Nome e cognome' = DATA STRING[100] (Cliente);
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
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(ordina(d)) = c E
prodotto(d) = p E
estraeAnno(data(ordine(d))) = y;
valutazione 'Valutazione' (Cliente c, Prodotto p, INTERO y) =
SOMMA PARTIZIONE 1 ORDINA DESC 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 secondo il seguente principio: somma l'espressione indicata dopo SUM (qui 1), all'interno dei gruppi specificati (qui Customer e Year, ma può essere qualsiasi espressione), ordinando all'interno dei gruppi in base alle espressioni specificate in ORDER (qui bought, e in caso di uguaglianza, in base al codice prodotto interno).
Compito 2.3
Quanti prodotti è necessario ordinare dai fornitori per soddisfare gli ordini attuali.
Espandiamo di nuovo la logica di dominio:
CLASS Fornitore 'Fornitore';
nomeAzienda = DATA STRING[100] (Fornitore);
supplier = DATA Supplier (Product);
unitsInStock 'Giacenza' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Livello di riordino' = 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
'Ordinato, ma non spedito' (Prodotto p) =
SOMMA GRUPPO quantità(DettaglioOrdine d) SE prodotto(d) = p;
'Da ordinare' (Prodotto p) = ordinatoNonSpedito(p) + livelloRiordino(p) - unitàInStock(p);
SELEZIONA nomeAzienda(forbitore(Prodotto p)), nome(p), daOrdinare(p) DOVE daOrdinare(p) > 0;
Compito con asterisco
E l'ultimo esempio personalmente da parte mia. C'è una logica di rete sociale. Le persone possono fare amicizia tra loro e piacere l'una all'altra. Dal punto di vista di una base di dati funzionale, questo apparirebbe nel seguente modo:
CLASSE Persona;
piace = DATI BOOLEANI (Persona, Persona);
amici = DATI BOOLEANI (Persona, Persona);
È necessario trovare i potenziali candidati all'amicizia. Più formalmente, è necessario trovare tutte le persone A, B, C tali che A è amico di B, B è amico di C, A piace a C, ma A non è amico di C.
Dal punto di vista di una base di dati funzionale, la query apparirebbe nel seguente modo:
SELEZIONA Persona a, Persona b, Persona c DOVE
likes(a, c) E NON amici(a, c) E
amici(a, b) E amici(b, c);
Si invita il lettore a risolvere personalmente questo compito in SQL. Si presume che gli amici siano molto meno numerosi rispetto a coloro che piacciono. Pertanto, essi si trovano in tabelle separate. In caso di successo, c'è anche un compito con due asterischi. In esso, l'amicizia non è simmetrica. Nella base di dati funzionale, questo apparirebbe in questo modo:
SELEZIONA Persona a, Persona b, Persona c DOVE
likes(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 del compito con il primo e secondo asterisco da :
SELEZIONA
pl.PersonAID
,pf.PersonAID
,pff.PersonAID
DA Persone COME p
--Mi piace
UNISCITI a RelazionePersona COME pl ON pl.PersonAID = p.PersonID
E pl.Relazione = 'Mi piace'
--Amici
UNISCITI a RelazionePersona COME pf ON pf.PersonAID = p.PersonID
E pf.Relazione = 'Amico'
--Amici degli Amici
UNISCITI a RelazionePersona COME pff ON pff.PersonAID = pf.PersonBID
E pff.PersonBID = pl.PersonBID
E pff.Relazione = 'Amico'
--Ancora non amici
LEFT JOIN RelazionePersona COME pnf ON pnf.PersonAID = p.PersonID
E pnf.PersonBID = pff.PersonBID
E pnf.Relazione = 'Amico'
DOVE pnf.PersonAID È NULL
;CON PersonRelationShipConsolidato COME (
SELEZIONA pl.PersonAID
,pl.PersonBID
,pl.Relazione
DA #RelazionePersona COME pl
UNIONE
SELEZIONA pl.PersonBID COME PersonAID
,pl.PersonAID COME PersonBID
,pl.Relazione
DA #RelazionePersona COME pl
)
SELEZIONA
pl.PersonAID
,pf.PersonBID
,pff.PersonBID
DA #Persone COME p
--Mi piace
UNISCITI a PersonRelationShipConsolidato COME pl ON pl.PersonAID = p.PersonID
E pl.Relazione = 'Mi piace'
--Amici
UNISCITI a PersonRelationShipConsolidato COME pf ON pf.PersonAID = p.PersonID
E pf.Relazione = 'Amico'
--Amici degli Amici
UNISCITI a PersonRelationShipConsolidato COME pff ON pff.PersonAID = pf.PersonBID
E pff.PersonBID = pl.PersonBID
E pff.Relazione = 'Amico'
--Ancora non amici
LEFT JOIN PersonRelationShipConsolidato COME pnf ON pnf.PersonAID = p.PersonID
E pnf.PersonBID = pff.PersonBID
E pnf.Relazione = 'Amico'
DOVE pnf.[PersonAID] È NULL
Conclusione
È importante notare che la sintassi fornita è solo una delle possibili implementazioni del concetto presentato. È stato adottato SQL come base, e l'obiettivo era quello di renderlo il più simile possibile a esso. Naturalmente, a qualcuno potrebbero non piacere i nomi delle parole chiave, il case delle parole e altro. Qui l'importante è il concetto stesso. Se lo si desidera, si può realizzare una sintassi simile anche in C++ o Python.
La concezione del database descritta, a mio avviso, ha i seguenti vantaggi:
- Semplicità. Si tratta di un indicatore relativamente soggettivo, che non è immediato nei casi semplici. Ma se si osservano casi più complessi (ad esempio, problemi con asterischi), allora, a mio avviso, scrivere tali query è decisamente più semplice.
- Incapsulamento. In alcuni esempi ho dichiarato funzioni intermedie (ad esempio, venduto, acquistato e così via), dalle quali sono state costruite le funzioni successive. Questo consente, se necessario, di modificare la logica di alcune funzioni senza cambiare la logica di quelle dipendenti da esse. Ad esempio, è possibile modificare le vendite venduto erano considerati da oggetti completamente diversi, mentre il resto della logica non cambierà. Sì, in RSDB questo può essere realizzato tramite CREATE VIEW. Ma se tutta la logica viene scritta in questo modo, non sembrerà molto leggibile.
- Assenza di interruzione semantica. Questo database opera con funzioni e classi (anziché tabelle e campi). Proprio come nella programmazione classica (se consideriamo che il metodo è una funzione con il primo parametro sotto forma di classe a cui appartiene). Di conseguenza, ‘abbinarlo’ ai linguaggi di programmazione universali dovrebbe essere molto 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) = 'Pietro' MESSAGE 'Pietro sta vendendo troppo dello stesso prodotto nel 2019';
- Eredità e polimorfismo. In un database funzionale è possibile introdurre ereditarietà multipla attraverso costruzioni del tipo CLASS ClassP: Class1, Class2 e realizzare polimorfismo multiplo. Come, probabilmente lo scriverò nei prossimi articoli.
Nonostante si tratti solo di un concetto, abbiamo già una certa implementazione in Java che trasmette tutta la logica funzionale in logica relazionale. Inoltre, è elegantemente integrata la logica delle viste e molto altro, grazie a cui si ottiene un intero . In sostanza, utilizziamo RSDB (per ora solo PostgreSQL) come 'macchina virtuale'. Durante una tale traduzione si presentano a volte problemi, poiché l'ottimizzatore delle query di RSDB non conosce determinate statistiche che conosce FSDB. In teoria, si può implementare un sistema di gestione di database che utilizzi come archivio una certa struttura adattata specificamente alla logica funzionale.
Fonte: habr.com
