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 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 . 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 :
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 . 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
