SGBD funcional

El món de les bases de dades ha estat ocupat durant molt de temps pels SGBD relacionals que utilitzen el llenguatge SQL. Tant és així que les varietats emergents s'anomenen NoSQL. Van aconseguir guanyar-se un lloc determinat en aquest mercat, però els SGBD relacionals no moriran i continuen utilitzant-se activament per als seus propis propòsits.

En aquest article, vull descriure el concepte de base de dades funcional. Per a una millor comprensió, ho faré comparant amb el model relacional clàssic. Com a exemples, s'utilitzaran tasques de diverses proves SQL que es troben a Internet.

Introducció

Les bases de dades relacionals funcionen sobre taules i camps. En una base de dades funcional, s'utilitzaran classes i funcions, respectivament. Un camp d'una taula amb N tecles es representarà en funció de N paràmetres. En lloc d'enllaços entre taules, s'utilitzaran funcions que retornin objectes de la classe a la qual va l'enllaç. La composició de funcions s'utilitzarà en lloc de JOIN.

Abans de passar directament a les tasques, descriuré la tasca de la lògica del domini. Per a DDL, utilitzaré la sintaxi PostgreSQL. Per funcional la seva pròpia sintaxi.

Taules i camps

Un objecte Sku senzill amb camps de nom i preu:

relacional

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

Funcional

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

Us n'anunciem dos funcions, que prenen un paràmetre Sku com a entrada i retornen un tipus primitiu.

Se suposa que en un SGBD funcional, cada objecte tindrà algun codi intern que es genera automàticament i es pot accedir si cal.

Fixem el preu del producte / botiga / proveïdor. Pot canviar amb el temps, així que afegim un camp de temps a la taula. Saltaré la declaració de taules per als directoris d'una base de dades relacional per escurçar el codi:

relacional

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

Funcional

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

Índexs

Per a l'últim exemple, construïm un índex de totes les claus i la data perquè puguem trobar ràpidament el preu durant un temps determinat.

relacional

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

Funcional

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

tasques

Comencem amb problemes relativament senzills extrets dels corresponents Article a Habr.

Primer, declarem la lògica del domini (per a una base de dades relacional, això es fa directament a l'article anterior).

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

Tasca 1.1

Mostra una llista d'empleats que reben salaris superiors als del supervisor immediat.

relacional

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

Funcional

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

Tasca 1.2

Mostra una llista dels empleats que guanyen el salari més alt del seu departament

relacional

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

Funcional

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

Les dues implementacions són equivalents. Per al primer cas de la base de dades relacional, podeu utilitzar CREATE VIEW, que de la mateixa manera calcularà primer el sou màxim d'un departament concret de la mateixa. En el futur, per a més claredat, utilitzaré el primer cas, ja que reflecteix millor la solució.

Tasca 1.3

Mostra una llista d'identificacions de departament, el nombre d'empleats en què no supera les 3 persones.

relacional

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

Funcional

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

Tasca 1.4

Mostra una llista d'empleats que no tenen cap responsable assignat al mateix departament.

relacional

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

Funcional

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

Tasca 1.5

Cerqueu la llista d'identificacions de departament amb el salari total màxim dels empleats.

relacional

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 )

Funcional

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

Passem a tasques més complexes d'una altra Article. Conté una anàlisi detallada de com implementar aquesta tasca en MS SQL.

Tasca 2.1

Quins venedors van vendre més de 1997 peces de l'article número 30 el 1?

Lògica de domini (com abans, saltem la declaració al RDBMS):

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

relacional

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

Funcional

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;

Tasca 2.2

Per a cada client (nom, cognom), cerqueu els dos articles (nom) en què el client va gastar més diners l'any 1997.

Ampliant la lògica del domini de l'exemple anterior:

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

customer = DATA Customer (Order);

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

relacional

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

Funcional

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'operador PARTICIÓ funciona segons el principi següent: suma l'expressió especificada després de SUMA (aquí 1) dins dels grups especificats (aquí Client i Any, però pot ser qualsevol expressió), ordenant dins dels grups segons les expressions especificades a ORDER ( aquí comprat, i si són iguals, llavors pel codi de producte intern).

Tasca 2.3

Quantes mercaderies s'han de demanar als proveïdors per complir les comandes actuals.

Tornem a estendre la lògica del domini:

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

supplier = DATA Supplier (Product);

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

relacional

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

Funcional

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;

Tasca amb un asterisc

I l'últim exemple és personalment meu. Hi ha la lògica d'una xarxa social. Les persones poden ser amigues entre elles i agradar-se. Des d'una perspectiva de base de dades funcional, això es veuria així:

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

Cal trobar possibles candidats a l'amistat. Més formalment, heu de trobar totes les persones A, B, C de manera que A sigui amiga de B i B sigui amiga de C, A A li agrada C, però A no sigui amiga de C.
Des del punt de vista de la base de dades funcional, la consulta seria així:

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

Es convida el lector a resoldre de manera independent aquest problema en SQL. Se suposa que hi ha molts menys amics que els que els agraden. Per tant, estan en taules separades. En cas de solució correcta, també hi ha un problema amb dos asteriscs. La seva amistat no és simètrica. En una base de dades funcional es veuria així:

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: solució del problema amb el primer i el segon asterisc de 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 

Conclusió

Cal tenir en compte que la sintaxi anterior del llenguatge és només una de les opcions per implementar el concepte anterior. Va ser SQL el que es va prendre com a base i l'objectiu era fer-lo el més semblant possible. Per descomptat, pot ser que a algú no li agradin els noms de les paraules clau, el cas de les paraules, etc. El més important aquí és el concepte en si. Si ho desitgeu, podeu fer que la sintaxi de C++ i Python sigui semblant.

El concepte de base de dades descrit, al meu entendre, té els següents avantatges:

  • Facilitat. Aquest és un indicador relativament subjectiu que no és evident en casos senzills. Però si ens fixem en casos més complexos (per exemple, tasques amb asteriscs), llavors, al meu entendre, escriure aquestes consultes és molt més fàcil.
  • Инкапсуляция. En alguns exemples, vaig declarar funcions intermèdies (per exemple, venut, comprar etc.), a partir de les quals es van construir funcions posteriors. Això permet canviar la lògica de determinades funcions, si cal, sense canviar la lògica de les que en depenen. Per exemple, podeu fer vendes venut es van calcular a partir d'objectes completament diferents, mentre que la resta de la lògica no canviarà. Sí, a RDBMS això es pot fer amb CREATE VIEW. Però si escriviu tota la lògica d'aquesta manera, no semblarà gaire llegible.
  • Sense bretxa semàntica. Aquesta base de dades funciona amb funcions i classes (en lloc de taules i camps). De la mateixa manera que en la programació clàssica (suposant que un mètode és una funció amb el primer paràmetre en forma de classe a la qual pertany). En conseqüència, hauria de ser molt més fàcil "fer amistat" amb els llenguatges de programació universals. A més, aquest concepte permet implementar funcions molt més complexes. Per exemple, podeu incrustar declaracions com aquesta a la base de dades:

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

  • Herència i polimorfisme. En una base de dades funcional, podeu introduir l'herència múltiple mitjançant les construccions CLASS ClassP: Class1, Class2 i implementar polimorfisme múltiple. Com exactament, potser escriuré en els articles següents.

Tot i que això és només un concepte, ja tenim alguna implementació a Java que tradueix tota la lògica funcional en lògica relacional. A més, la lògica de les representacions i moltes altres coses s'hi enganxen molt bé, gràcies a les quals obtenim un tot plataforma. Essencialment, estem utilitzant un RDBMS (només PostgreSQL fins ara) com a "màquina virtual". Aquesta traducció de vegades causa problemes perquè l'optimitzador de consultes RDBMS no coneix certes estadístiques que fa l'FDBMS. En teoria, és possible implementar un sistema de gestió de bases de dades que utilitzarà com a emmagatzematge una estructura determinada, adaptada específicament per a la lògica funcional.

Font: www.habr.com

Afegeix comentari