Base de datos funcional

El mundo de las bases de datos ha estado dominado por los sistemas de gestión de bases de datos relacionales, que utilizan el lenguaje SQL. Tan pronunciado es esto, que las variantes que han surgido se conocen como NoSQL. Han logrado hacerse un hueco en este mercado, pero las bases de datos relacionales no tienen intención de desaparecer y continúan utilizándose activamente para sus propósitos.

En este artículo quiero describir el concepto de base de datos funcional. Para una mejor comprensión, lo haré comparándolo con el modelo relacional clásico. Se utilizarán ejemplos de tareas de diversas pruebas de SQL encontradas en Internet.

Introducción

Las bases de datos relacionales operan con tablas y campos. En la base de datos funcional, en lugar de estos, se utilizarán clases y funciones respectivamente. Un campo en una tabla con N claves será representado como una función de N parámetros. En lugar de relaciones entre tablas, se utilizarán funciones que devuelven objetos de la clase a la que se hace referencia. En lugar de JOIN, se utilizará la composición de funciones.

Antes de pasar directamente a las tareas, describiré la asignación de la lógica de dominio. Para DDL utilizaré la sintaxis de PostgreSQL. Para la funcional, usaré mi propia sintaxis.

Tablas y campos

Un objeto sencillo Sku con los campos 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

CLASE Sku;
nombre = CADENA DE DATOS[100] (Sku);
precio = NÚMEROS DE DATOS[10,5] (Sku);

Declaramos dos funciones, que aceptan un parámetro Sku y devuelven un tipo primitivo.

Se supone que en la base de datos funcional, cada objeto tendrá un código interno que se genera automáticamente y al que se puede acceder cuando sea necesario.

Establezcamos un precio para el producto / tienda / proveedor. Este puede cambiar con el tiempo, así que añadiremos un campo de tiempo a la tabla. Omitiré la declaración de tablas para los catálogos en la 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

CLASE Sku;
CLASE Almacén;
CLASE Proveedor;
fechaHora = DATOS FECHA Y HORA (Sku, Almacén, Proveedor);
precio = DATOS NUMÉRICO[10,5] (Sku, Almacén, Proveedor);

Índices

Para el último ejemplo, construiremos un índice basado en todas las claves y la fecha, para poder encontrar rápidamente el precio en un momento determinado.

Relacional

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

Funcional

ÍNDICE Sku sk, Tienda st, Proveedor sp, dateTime(sk, st, sp);

Tareas

Comencemos con tareas relativamente sencillas, tomadas de la correspondiente artículo en Habr.

Primero, declararemos la lógica del dominio (para la base de datos relacional, esto se hace directamente en el artículo proporcionado).

Clase Departamento;
nombre = CADENA DE DATOS[100] (Departamento);

CLASS Empleado;
departamento = DATA Departamento (Empleado);
jefe = DATA Empleado (Empleado);
nombre = DATA STRING[100] (Empleado);
salario = DATA NUMERICO[14,2] (Empleado);

Tarea 1.1

Mostrar la lista de empleados que ganan más que su superior inmediato.

Relacional

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

Funcional

SELECCIONAR nombre(Empleado a) DONDE salario(a) > salario(jefe(a));

Tarea 1.2

Mostrar la lista de empleados que ganan el salario máximo en su departamento.

Relacional

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

Funcional

maxSalary 'Salario máximo' (Departamento s) = 
    GRUPO MAX salario (Empleado e) SI departamento(e) = s;
SELECCIONAR nombre (Empleado a) DONDE salario(a) = maxSalary(departamento(a));

// или если "заинлайнить"
SELECT nombre(Empleado a) WHERE 
    salario(a) = maxSalario(GRUPO MAX salary(Empleado e) IF departamento(e) = departamento(a));

Ambas implementaciones son equivalentes. Para el primer caso, en la base de datos relacional se puede usar CREATE VIEW, que de la misma manera primero calculará el salario máximo para un departamento específico. Más adelante, por claridad, utilizaré el primer caso, ya que refleja mejor la solución.

Tarea 1.3

Mostrar la lista de ID de departamentos cuyo número de empleados no excede a 3 personas.

Relacional

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

Funcional

countEmployees 'Cantidad de empleados' (Departamento d) = 
    AGRUPAR SUMA 1 SI departamento(Empleado e) = d;
SELECCIONAR Departamento d DONDE countEmployees(d) <= 3;

Tarea 1.4

Mostrar la lista de empleados que no tienen un jefe designado que trabaje en el mismo departamento.

Relacional

select a.*
from   empleado a
left   join empleado b on (b.id = a.chief_id and b.department_id = a.department_id)
where  b.id is null

Funcional

SELECCIONAR nombre(Empleado a) DONDE NO (departamento(jefe(a)) = departamento(a));

Tarea 1.5

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

Relacional

with suma_salario as
  ( select department_id, sum(salary) salario
    from   empleado
    group  by department_id )
select department_id
from   suma_salario a       
where  a.salary = ( select max(salary) from suma_salario )

Funcional

salarySum 'Salario máximo' (Departamento d) = 
    AGRUPAR SUM salary(Empleado e) SI departamento(e) = d;
maxSalarySum 'Salario máximo de departamentos' () = 
    AGRUPAR MAX salarySum(Departamento d);
SELECCIONAR Departamento d DONDE salarySum(d) = maxSalarySum();

Pasemos a tareas más complejas de otra artículo. En ella hay un análisis detallado de cómo implementar esta tarea en MS SQL.

Tarea 2.1

¿Qué vendedores vendieron más de 30 piezas del producto nº1 en 1997?

La lógica de dominio (como antes en RDBMS omitimos la declaración):

CLASE Empleado 'Vendedor';
apellido 'Apellido' = CADENA DE DATOS[100] (Empleado);

CLASS Producto 'Producto';
id = DATA INTEGER (Producto);
nombre = DATA STRING[100] (Producto);

CLASS Pedido 'Pedido';
fecha = DATA DATE (Pedido);
empleado = DATA Empleado (Pedido);

CLASS Detalle 'Línea de pedido';

pedido = DATA Pedido (Detalle);
producto = DATA Producto (Detalle);
cantidad = DATA NUMERICO[10,5] (Detalle);

Relacional

select Apellido
from Empleados as e
where (
  select sum(od.Cantidad)
  from [Detalles de Pedido] as od
  where od.ProductID = 1 and od.OrderID in (
    select o.OrderID
    from Pedidos as o
    where year(o.FechaDePedido) = 1997 and e.EmployeeID = o.EmployeeID)
) > 30

Funcional

vendido (Empleado e, ENTERO productId, ENTERO año) = 
    SUMA GRUPO cantidad(D detallePedido d) SI 
        empleado(orden(d)) = e Y 
        id(producto(d)) = productId Y 
        extraerAño(fecha(orden(d))) = año;
SELECT apellido(Empleado e) DONDE vendido(e, 1, 1997) > 30;

Tarea 2.2

Para cada comprador (nombre, apellido), encontrar dos productos (nombre) en los que el comprador gastó más dinero en 1997.

Ampliamos la lógica de dominio del ejemplo anterior:

CLASS Customer 'Cliente';
contactName 'Nombre completo' = DATA STRING[100] (Cliente);

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

suma (Detalle d) = cantidad(d) * precioUnitario(d) * (1 - descuento(d));
comprado 'Comprado' (Cliente c, Producto p, ENTERO y) = 
    SUMA DE GRUPO suma(Detalle d) SI 
        cliente(pedido(d)) = c Y 
        producto(d) = p Y 
        extraerAño(fecha(pedido(d))) = y;
calificación 'Calificación' (Cliente c, Producto p, ENTERO y) = 
    SUMA DE PARTIDA 1 ORDEN DESC comprado(c, p, y), p POR c, y;
SELECCIONAR nombreContacto(Cliente c), nombre(Producto p) DONDE calificación(c, p, 1997) < 3;

El operador PARTITION funciona de la siguiente manera: suma la expresión especificada después de SUM (aquí 1) dentro de los grupos especificados (aquí Customer y Year, pero puede ser cualquier expresión), ordenando dentro de los grupos por las expresiones indicadas en ORDER (aquí bought, y si son iguales, por el código interno del producto).

Tarea 2.3

Cuántos productos se deben pedir a los proveedores para cumplir con los pedidos actuales.

Nuevamente ampliamos la lógica de dominio:

CLASS Supplier 'Proveedor';
companyName = DATA STRING[100] (Proveedor);

supplier = DATA Supplier (Product);

unitsInStock 'Disponibilidad en el almacén' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Nivel de reabastecimiento' = 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

pedidoNoEnviado 'Pedido realizado, pero no enviado' (Producto p) = 
    SUMA GRUPO cantidad(DatoOrden d) SI producto(d) = p;
paraOrdenar 'Para el pedido' (Producto p) = pedidoNoEnviado(p) + nivelReorden(p) - unidadesEnStock(p);
SELECCIONAR nombreEmpresa(proveedor(Producto p)), nombre(p), paraOrdenar(p) DONDE paraOrdenar(p) > 0;

Tarea con asterisco

Y el último ejemplo de mi parte. Hay lógica de red social. Las personas pueden ser amigas entre sí y gustarse mutuamente. Desde el punto de vista funcional de la base de datos, esto se vería de la siguiente manera:

CLASE Persona;
gustos = DATOS BOOLEANO (Persona, Persona);
amigos = DATOS BOOLEANO (Persona, Persona);

Es necesario encontrar posibles candidatos para la amistad. Más formalmente, se necesita encontrar a todas las personas A, B, C tales que A sea amigo de B, B sea amigo de C, A le agrade C, pero A no sea amigo de C.
Desde el punto de vista funcional de la base de datos, la consulta se vería de la siguiente manera:

SELECCIONAR Persona a, Persona b, Persona c DONDE 
    les gusta(a, c) Y NO amigos(a, c) Y 
    amigos(a, b) Y amigos(b, c);

Se invita al lector a resolver este problema en SQL por sí mismo. Se supone que hay muchos más que gustan que amigos. Por lo tanto, se almacenan en tablas separadas. En caso de solución exitosa, también hay una tarea con dos asteriscos. En esta, la amistad no es simétrica. En la base de datos funcional, esto se vería así:

SELECCIONAR Persona a, Persona b, Persona c DONDE 
    les gusta(a, c) Y NO amigos(a, c) Y 
    (amigos(a, b) OR amigos(b, a)) Y 
    (amigos(b, c) OR amigos(c, b));

UPD: solución del problema con el primer y segundo asterisco de dss_kalika:

SELECCIONAR 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
DE Personas                 COMO p
--Me gusta                      
UNIR PersonRelationShip      COMO pl EN pl.PersonAID = p.PersonID
                                  Y pl.Relation  = 'Me gusta'
--Amigos                     
UNIR PersonRelationShip      COMO pf EN pf.PersonAID = p.PersonID 
                                  Y pf.Relation = 'Amigo'
--Amigos de Amigos              
UNIR PersonRelationShip      COMO pff EN pff.PersonAID = pf.PersonBID
                                   Y pff.PersonBID = pl.PersonBID
                                   Y pff.Relation = 'Amigo'
--Aún no son amigos         
UNIÓN IZQUIERDA PersonRelationShip COMO pnf EN pnf.PersonAID = p.PersonID
                                   Y pnf.PersonBID = pff.PersonBID
                                   Y pnf.Relation = 'Amigo'
DONDE pnf.PersonAID ES NULO 

;CON PersonRelationShipColapsado COMO (
  SELECCIONAR pl.PersonAID
        ,pl.PersonBID
        ,pl.Relation 
  DE #PersonRelationShip      COMO pl 
  
  UNIÓN 

  SELECCIONAR pl.PersonBID COMO PersonAID
        ,pl.PersonAID COMO PersonBID
        ,pl.Relation
  DE #PersonRelationShip      COMO pl 
)
SELECCIONAR 
   pl.PersonAID
  ,pf.PersonBID
  ,pff.PersonBID
DE #Persons                      COMO p
--Me gusta                      
UNIR PersonRelationShipColapsado  COMO pl EN pl.PersonAID = p.PersonID
                                 Y pl.Relation  = 'Me gusta'                                  
--Amigos                          
UNIR PersonRelationShipColapsado  COMO pf EN pf.PersonAID = p.PersonID 
                                 Y pf.Relation = 'Amigo'
--Amigos de Amigos                   
UNIR PersonRelationShipColapsado  COMO pff EN pff.PersonAID = pf.PersonBID
                                 Y pff.PersonBID = pl.PersonBID
                                 Y pff.Relation = 'Amigo'
--Aún no son amigos                   
UNIÓN IZQUIERDA PersonRelationShipColapsado COMO pnf EN pnf.PersonAID = p.PersonID
                                   Y pnf.PersonBID = pff.PersonBID
                                   Y pnf.Relation = 'Amigo'
DONDE pnf.[PersonAID] ES NULO 

Conclusión

Es importante señalar que la sintaxis del lenguaje presentada es solo una de las formas de implementar el concepto que se ha expuesto. Se basó en SQL, y el objetivo era mantenerlo lo más parecido posible a él. Por supuesto, a algunos les pueden desagradar los nombres de las palabras clave, la capitalización y demás. Aquí lo importante es el concepto mismo. Si se desea, también se puede hacer una sintaxis similar en C++ o Python.

En mi opinión, el concepto de base de datos descrito tiene las siguientes ventajas:

  • Simplicidad. Este es un indicador relativamente subjetivo, que no es obvio en casos simples. Pero si se observan casos más complejos (por ejemplo, problemas con asteriscos), entonces, en mi opinión, escribir tales consultas es significativamente más fácil.
  • Encapsulamiento. En algunos ejemplos he declarado funciones intermedias (por ejemplo, vendido, comprado etc.), de las cuales se construyeron las funciones posteriores. Esto permite, si es necesario, cambiar la lógica de ciertas funciones sin alterar la lógica de las que dependen. Por ejemplo, se puede hacer que las ventas vendido Se consideraba a partir de objetos completamente diferentes, mientras que el resto de la lógica no cambiará. Sí, en un SGBD relacional esto se puede implementar utilizando CREATE VIEW. Pero si se escribe toda la lógica de esta manera, no será muy legible.
  • Ausencia de ruptura semántica. Esta base de datos opera con funciones y clases (en lugar de tablas y campos). De la misma manera que en la programación clásica (si se considera que el método es una función con el primer parámetro en forma de clase a la que pertenece). Por consiguiente, debe ser mucho más fácil 'amigarse' con los lenguajes de programación universales. Además, este concepto permite implementar funciones mucho más complejas. Por ejemplo, se pueden incorporar en la base de datos operadores del tipo:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Petya' MESSAGE 'Algo Petya está vendiendo demasiado de un mismo producto en 2019';

  • Herencia y polimorfismo. En una base de datos funcional se puede introducir herencia múltiple a través de construcciones CLASS ClassP: Class1, Class2 y se puede implementar polimorfismo múltiple. Cómo hacerlo, tal vez lo escribiré en los próximos artículos.

A pesar de que esto es solo un concepto, ya tenemos cierta implementación en Java que traduce toda la lógica funcional a la lógica relacional. Además, se ha incorporado elegantemente la lógica de vistas y mucho más, lo que resulta en una completa plataforma. En esencia, estamos utilizando un SGBD relacional (hasta ahora solo PostgreSQL) como una 'máquina virtual'. Con tal traducción, a veces surgen problemas, ya que el optimizador de consultas del SGBD relacional no conoce ciertas estadísticas que sí conoce el SGBD funcional. En teoría, se puede implementar un sistema de gestión de bases de datos que utilice como almacenamiento una estructura adaptada específicamente a la lógica funcional.

Fuente: habr.com

Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS 🔥 Compra un hosting fiable para sitios web con protección contra DDoS, servidores VPS VDS | ProHoster