DBMS funcional

O mundo das bases de datos estivo dominado durante moito tempo polos DBMS relacionais, que usan a linguaxe SQL. Tanto é así que as variantes emerxentes chámanse NoSQL. Conseguiron facerse un determinado lugar neste mercado, pero os DBMS relacionais non van morrer e seguen utilizándose activamente para os seus fins.

Neste artigo quero describir o concepto dunha base de datos funcional. Para unha mellor comprensión, fareino comparándoo co modelo relacional clásico. Utilizaranse como exemplos problemas de varias probas SQL atopadas en Internet.

Introdución

As bases de datos relacionais operan en táboas e campos. Nunha base de datos funcional, empregaranse no seu lugar clases e funcións, respectivamente. Un campo nunha táboa con N claves representarase en función de N parámetros. En lugar de relacións entre táboas, empregaranse funcións que devolvan obxectos da clase coa que se fai a conexión. Usarase a composición de funcións en lugar de JOIN.

Antes de pasar directamente ás tarefas, describirei a tarefa da lóxica do dominio. Para DDL usarei a sintaxe PostgreSQL. Para funcional ten a súa propia sintaxe.

Táboas e campos

Un obxecto Sku sinxelo con campos de nome e prezo:

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

Anunciamos dous funcións, que toman un parámetro Sku como entrada e devolven un tipo primitivo.

Suponse que nun DBMS funcional cada obxecto terá algún código interno que se xera automaticamente e ao que se pode acceder se é necesario.

Establecemos o prezo do produto/tenda/provedor. Pode cambiar co paso do tempo, así que imos engadir un campo de tempo á táboa. Omitirei declarar táboas para directorios nunha base de datos relacional para acurtar o código:

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

Índices

Para o último exemplo, imos construír un índice en todas as claves e a data para que poidamos atopar rapidamente o prezo para un momento específico.

Relacional

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

Funcional

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

tarefas

Comecemos con problemas relativamente sinxelos tomados dos correspondentes Artigo en Habr.

En primeiro lugar, imos declarar a lóxica do dominio (para a base de datos relacional isto faise directamente no artigo 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);

Tarefa 1.1

Mostrar unha lista de empregados que perciben un salario superior ao do seu supervisor inmediato.

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

Tarefa 1.2

Enumerar os empregados que perciben o salario máximo no seu departamento

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

Ambas implementacións son equivalentes. Para o primeiro caso, nunha base de datos relacional pódese empregar CREATE VIEW, que do mesmo xeito calculará primeiro o salario máximo dun departamento específico da mesma. No que segue, para claridade, empregarei o primeiro caso, xa que reflicte mellor a solución.

Tarefa 1.3

Mostrar unha lista de ID de departamento, o número de empregados no que non exceda de 3 persoas.

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;

Tarefa 1.4

Mostra unha lista de empregados que non teñen un xestor designado que traballe no mesmo departamento.

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

Tarefa 1.5

Busca unha lista de ID de departamento co salario máximo total dos empregados.

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

Pasemos a tarefas máis complexas doutro Artigo. Contén unha análise detallada de como implementar esta tarefa en MS SQL.

Tarefa 2.1

Que vendedores venderon máis de 1997 unidades do produto número 30 en 1?

Lóxica de dominio (como antes en RDBMS, saltamos a declaración):

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;

Tarefa 2.2

Para cada comprador (nome, apelidos), busque os dous bens (nome) nos que o comprador gastou máis diñeiro en 1997.

Ampliamos a lóxica do dominio do exemplo 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;

O operador PARTITION traballa co seguinte principio: suma a expresión especificada despois de SUMA (aquí 1), dentro dos grupos especificados (aquí Cliente e Ano, pero pode ser calquera expresión), ordenando dentro dos grupos polas expresións especificadas no ORDE ( aquí comprado e, se é igual, segundo o código interno do produto).

Tarefa 2.3

Cantas mercadorías hai que pedir aos provedores para cumprir os pedidos actuais.

Ampliemos de novo a lóxica do dominio:

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;

Problema cun asterisco

E o último exemplo é de min persoalmente. Hai a lóxica dunha rede social. As persoas poden ser amigas entre elas e gustarse. Desde unha perspectiva de base de datos funcional, quedaría así:

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

É necesario buscar posibles candidatos para a amizade. Máis formalmente, cómpre atopar todas as persoas A, B, C de tal forma que A sexa amigo de B e B sexa amigo de C, A gústalle C, pero A non sexa amigo de C.
Desde unha perspectiva de base de datos funcional, a consulta sería así:

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

Anímase ao lector a resolver este problema en SQL por si mesmo. Suponse que hai moitos menos amigos que persoas que che gustan. Polo tanto, están en táboas separadas. Se ten éxito, tamén hai unha tarefa con dúas estrelas. Nela, a amizade non é simétrica. Nunha base de datos funcional sería así:

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ón ao problema co primeiro e segundo asterisco 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ón

Nótese que a sintaxe da linguaxe é só unha das opcións para implementar o concepto dado. Tomouse SQL como base e o obxectivo era que fose o máis semellante posible a el. Por suposto, a algúns non lles gustan os nomes de palabras clave, rexistros de palabras, etc. O principal aquí é o propio concepto. Se o desexa, pode facer que ambas sintaxes de C++ e Python sexan similares.

O concepto de base de datos descrito, na miña opinión, ten as seguintes vantaxes:

  • Simplicidade. Este é un indicador relativamente subxectivo que non é obvio en casos sinxelos. Pero se miras casos máis complexos (por exemplo, problemas cos asteriscos), entón, na miña opinión, escribir tales consultas é moito máis sinxelo.
  • Инкапсуляция. Nalgúns exemplos declarei funcións intermedias (por exemplo, vendido, comprou etc.), a partir do cal se construíron funcións posteriores. Isto permite cambiar a lóxica de determinadas funcións, se é necesario, sen cambiar a lóxica das que dependen delas. Por exemplo, podes facer vendas vendido foron calculados a partir de obxectos completamente diferentes, mentres que o resto da lóxica non cambiará. Si, isto pódese implementar nun RDBMS usando CREATE VIEW. Pero se toda a lóxica está escrita deste xeito, non parecerá moi lexible.
  • Sen fenda semántica. Tal base de datos opera en funcións e clases (en lugar de táboas e campos). Igual que na programación clásica (se asumimos que un método é unha función co primeiro parámetro na forma da clase á que pertence). En consecuencia, debería ser moito máis fácil "facer amizade" coas linguaxes de programación universais. Ademais, este concepto permite implementar unha funcionalidade moito máis complexa. Por exemplo, pode incorporar operadores como:

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

  • Herdanza e polimorfismo. Nunha base de datos funcional, pode introducir a herdanza múltiple a través das construcións CLASS ClassP: Class1, Class2 e implementar polimorfismos múltiples. Probablemente escribirei exactamente como en artigos futuros.

Aínda que este é só un concepto, xa temos algunha implementación en Java que traduce toda a lóxica funcional en lóxica relacional. Ademais, a lóxica das representacións e moitas outras cousas están moi unidas a ela, grazas ao cal obtemos un conxunto plataforma. Esencialmente, usamos o RDBMS (só PostgreSQL por agora) como unha "máquina virtual". Ás veces xorden problemas con esta tradución porque o optimizador de consultas RDBMS non coñece determinadas estatísticas que coñece o FDBMS. En teoría, é posible implementar un sistema de xestión de bases de datos que utilizará unha determinada estrutura como almacenamento, adaptada especificamente para a lóxica funcional.

Fonte: www.habr.com

Engadir un comentario