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
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
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
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
Fonte: habr.com