SGBD funcional

El mundo de las bases de datos ha sido ocupado durante mucho tiempo por los DBMS relacionales que utilizan el lenguaje SQL. Tanto es así que las variedades emergentes se denominan NoSQL. Se las arreglaron para ganar un cierto lugar en este mercado, pero los DBMS relacionales no van a morir y continúan siendo utilizados activamente para sus propios fines.

En este artículo, quiero describir el concepto de una base de datos funcional. Para una mejor comprensión, lo haré comparando con el modelo relacional clásico. Como ejemplos, se utilizarán tareas de varias pruebas de SQL que se encuentran en Internet.

introducción

Las bases de datos relacionales operan en tablas y campos. En una base de datos funcional, se utilizarán clases y funciones en su lugar, respectivamente. Un campo en una tabla con N claves se representará como una función de N parámetros. En lugar de enlaces entre tablas, se utilizarán funciones que devuelvan objetos de la clase a la que va el enlace. Se utilizará la composición de funciones en lugar de JOIN.

Antes de pasar directamente a las tareas, describiré la tarea de lógica de dominio. Para DDL, usaré la sintaxis de PostgreSQL. Para funcional su propia sintaxis.

tablas y campos

Un objeto Sku simple con campos de nombre y precio:

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 dos Características, que toman un parámetro Sku como entrada y devuelven un tipo primitivo.

Se supone que en un DBMS funcional, cada objeto tendrá algún código interno que se genera automáticamente y se puede acceder si es necesario.

Vamos a fijar el precio para el producto/tienda/proveedor. Puede cambiar con el tiempo, así que agreguemos un campo de tiempo a la tabla. Omitiré la declaración de tablas para directorios en una base de datos relacional para acortar el 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 el último ejemplo, construyamos un índice en todas las claves y fechas para que podamos encontrar rápidamente el precio para un tiempo determinado.

relacional

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

Funcional

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

Tareas

Comencemos con problemas relativamente simples tomados de los correspondientes Artículo en Habr.

Primero, declaremos la lógica del dominio (para una base de datos relacional, esto se hace directamente en el artículo 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);

Tarea 1.1

Mostrar una lista de empleados que reciben salarios superiores a los del 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));

Tarea 1.2

Mostrar una lista de los empleados que ganan el salario más alto en su 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 implementaciones son equivalentes. Para el primer caso en la base de datos relacional, puede utilizar CREAR VISTA, que de la misma manera calculará primero el salario máximo para un departamento específico en ella. En el futuro, para mayor claridad, usaré el primer caso, ya que refleja mejor la solución.

Tarea 1.3

Muestre una lista de ID de departamento, el número de empleados en el que no exceda de 3 personas.

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;

Tarea 1.4

Muestre una lista de empleados que no tienen un gerente asignado trabajando en el mismo 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));

Tarea 1.5

Encuentre la lista de ID de departamentos con el salario máximo total de los empleados.

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 tareas más complejas de otro Artículo. Contiene un análisis detallado de cómo implementar esta tarea en MS SQL.

Tarea 2.1

¿Qué vendedores vendieron más de 1997 piezas del artículo #30 en 1?

Lógica de dominio (como antes, omitimos la declaración en el 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;

Tarea 2.2

Para cada cliente (nombre, apellido), encuentre los dos artículos (nombre) en los que el cliente gastó más dinero en 1997.

Extendiendo la lógica del dominio del ejemplo 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;

El operador PARTICIÓN funciona según el siguiente principio: suma la expresión especificada después de SUMA (aquí 1) dentro de los grupos especificados (aquí Cliente y Año, pero puede ser cualquier expresión), clasificando dentro de los grupos según las expresiones especificadas en ORDEN ( aquí comprados, y si son iguales, entonces por el código interno del producto).

Tarea 2.3

Cuántos bienes deben pedirse a los proveedores para cumplir con los pedidos actuales.

Ampliemos la lógica del dominio de nuevo:

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;

Tarea con un asterisco

Y el último ejemplo es mío personalmente. Existe la lógica de una red social. Las personas pueden ser amigas entre sí y gustarse. Desde una perspectiva de base de datos funcional, esto se vería así:

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

Es necesario encontrar posibles candidatos para la amistad. De manera más formal, debe encontrar a todas las personas A, B, C de modo que A sea amigo de B y B sea amigo de C, a A le guste C, pero A no sea amigo de C.
Desde el punto de vista de una base de datos funcional, la consulta se verí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);

Se invita al lector a resolver de forma independiente este problema en SQL. Se supone que hay muchos menos amigos que los que gustan. Por lo tanto, están en tablas separadas. En caso de una solución exitosa, también hay un problema con dos asteriscos. Su amistad no es simétrica. En una base de datos funcional se verí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 del problema con el primer y 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

Cabe señalar que la sintaxis anterior del lenguaje es solo una de las opciones para implementar el concepto anterior. Fue SQL el que se tomó como base, y el objetivo era hacerlo lo más similar posible a él. Por supuesto, es posible que a alguien no le gusten los nombres de las palabras clave, el caso de las palabras, etc. Lo principal aquí es el concepto en sí mismo. Si lo desea, puede hacer que la sintaxis de C ++ y Python sea similar.

El concepto de base de datos descrito, en mi opinión, tiene las siguientes ventajas:

  • Facilidad. Este es un indicador relativamente subjetivo que no es obvio en casos simples. Pero si observa casos más complejos (por ejemplo, tareas con asteriscos), entonces, en mi opinión, escribir consultas de este tipo es mucho más fácil.
  • Инкапсуляция. En algunos ejemplos, declaré funciones intermedias (por ejemplo, vendido, compró etc.), a partir de las cuales se construyeron funciones posteriores. Esto le permite cambiar la lógica de ciertas funciones, si es necesario, sin cambiar la lógica de aquellas que dependen de ellas. Por ejemplo, puede hacer ventas vendido se calcularon a partir de objetos completamente diferentes, mientras que el resto de la lógica no cambiará. Sí, en RDBMS esto se puede hacer con CREAR VISTA. Pero si escribe toda la lógica de esta manera, no se verá muy legible.
  • Sin brecha semántica. Tal base de datos opera con funciones y clases (en lugar de tablas y campos). De la misma forma que en la programación clásica (asumiendo que un método es una función con el primer parámetro en forma de clase a la que pertenece). En consecuencia, debería ser mucho más fácil "hacerse amigo" de los lenguajes de programación universales. Además, este concepto te permite implementar funciones mucho más complejas. Por ejemplo, puede incrustar declaraciones como esta en la base de datos:

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

  • Herencia y polimorfismo. En una base de datos funcional, puede introducir la herencia múltiple a través de las construcciones CLASS ClassP: Class1, Class2 e implementar el polimorfismo múltiple. Cómo exactamente, tal vez escribiré en los siguientes artículos.

A pesar de que esto es solo un concepto, ya tenemos alguna implementación en Java que traduce toda la lógica funcional en lógica relacional. Además, la lógica de las representaciones y muchas otras cosas están maravillosamente atornilladas, gracias a lo cual obtenemos un todo plataforma. Esencialmente, estamos usando un RDBMS (solo PostgreSQL hasta ahora) como una "máquina virtual". Esta traducción a veces causa problemas porque el optimizador de consultas RDBMS no conoce ciertas estadísticas que sí conoce el FDBMS. En teoría, es posible implementar un sistema de gestión de base de datos que utilizará una determinada estructura como almacenamiento, adaptada específicamente para la lógica funcional.

Fuente: habr.com

Añadir un comentario