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
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
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
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
Fuente: habr.com