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