O mundo dos bancos de dados há muito é dominado por SGBDs relacionais, que utilizam a linguagem SQL. Tanto é verdade que as variantes emergentes são chamadas de NoSQL. Eles conseguiram conquistar um determinado lugar neste mercado, mas os SGBDs relacionais não vão morrer e continuam a ser usados ativamente para seus propósitos.
Neste artigo quero descrever o conceito de banco de dados funcional. Para uma melhor compreensão, farei isso comparando-o com o modelo relacional clássico. Problemas de diversos testes SQL encontrados na Internet serão usados como exemplos.
Introdução
Os bancos de dados relacionais operam em tabelas e campos. Em um banco de dados funcional, classes e funções serão usadas, respectivamente. Um campo em uma tabela com N chaves será representado em função de N parâmetros. Em vez de relacionamentos entre tabelas, serão utilizadas funções que retornam objetos da classe com a qual a conexão é feita. A composição da função será usada em vez de JOIN.
Antes de passar diretamente às tarefas, descreverei a tarefa da lógica de domínio. Para DDL usarei a sintaxe PostgreSQL. Para funcional tem sua própria sintaxe.
Tabelas e campos
Um objeto Sku simples com campos de nome e preço:
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 dois função, que recebe um parâmetro Sku como entrada e retorna um tipo primitivo.
Supõe-se que em um SGBD funcional cada objeto terá algum código interno que é gerado automaticamente e pode ser acessado se necessário.
Vamos definir o preço do produto/loja/fornecedor. Pode mudar com o tempo, então vamos adicionar um campo de hora à tabela. Ignorarei a declaração de tabelas para diretórios em um banco de dados relacional para encurtar 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, construiremos um índice em todas as chaves e na data para que possamos encontrar rapidamente o preço para um horário 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
Vamos começar com problemas relativamente simples retirados do correspondente
Primeiro, vamos declarar a lógica do domínio (para o banco de dados relacional isso é feito diretamente no artigo acima).
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
Exiba uma lista de funcionários que recebem salário superior ao de seu supervisor imediato.
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
Liste os funcionários que recebem o salário máximo em 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 as implementações são equivalentes. Para o primeiro caso, em um banco de dados relacional você pode utilizar CREATE VIEW, que da mesma forma irá primeiro calcular o salário máximo para um determinado departamento nele. A seguir, para maior clareza, utilizarei o primeiro caso, pois reflete melhor a solução.
Tarefa 1.3
Exiba uma lista de IDs de departamento, cujo número de funcionários não exceda 3 pessoas.
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
Exiba uma lista de funcionários que não possuem um gerente designado trabalhando 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
Encontre uma lista de IDs de departamento com o salário total máximo do funcionário.
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();
Vamos passar para tarefas mais complexas de outro
Tarefa 2.1
Quais vendedores venderam mais de 1997 unidades do produto nº 30 em 1?
Lógica de domínio (como antes no RDBMS, ignoramos a declaração):
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, sobrenome), encontre os dois bens (nome) nos quais o comprador gastou mais dinheiro em 1997.
Estendemos a lógica de domínio 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 funciona segundo o seguinte princípio: soma a expressão especificada após SUM (aqui 1), dentro dos grupos especificados (aqui Cliente e Ano, mas pode ser qualquer expressão), classificando dentro dos grupos pelas expressões especificadas no ORDER ( aqui comprado, e se igual, então de acordo com o código interno do produto).
Tarefa 2.3
Quantas mercadorias precisam ser encomendadas aos fornecedores para atender aos pedidos atuais.
Vamos expandir a lógica do domínio novamente:
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 com um asterisco
E o último exemplo é meu pessoalmente. Existe a lógica de uma rede social. As pessoas podem ser amigas e gostar umas das outras. Do ponto de vista funcional do banco de dados, seria assim:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
É preciso encontrar possíveis candidatos à amizade. Mais formalmente, você precisa encontrar todas as pessoas A, B, C, de modo que A seja amigo de B e B seja amigo de C, A goste de C, mas A não seja amigo de C.
Do ponto de vista funcional do banco de dados, a consulta ficaria assim:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
O leitor é incentivado a resolver esse problema em SQL por conta própria. Supõe-se que haja muito menos amigos do que pessoas de quem você gosta. Portanto, eles estão em tabelas separadas. Se for bem-sucedido, há também uma tarefa com duas estrelas. Nele, a amizade não é simétrica. Em um banco de dados funcional ficaria assim:
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: solução para o problema com o primeiro e o 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
Conclusão
Deve-se notar que a sintaxe da linguagem dada é apenas uma das opções para implementar o conceito dado. O SQL foi tomado como base e o objetivo era que fosse o mais semelhante possível a ele. Claro, alguns podem não gostar dos nomes das palavras-chave, registros de palavras, etc. O principal aqui é o próprio conceito. Se desejar, você pode criar sintaxe semelhante em C++ e Python.
O conceito de banco de dados descrito, na minha opinião, apresenta as seguintes vantagens:
- Simplicidade. Este é um indicador relativamente subjetivo que não é óbvio em casos simples. Mas se você observar casos mais complexos (por exemplo, problemas com asteriscos), então, na minha opinião, escrever essas consultas é muito mais fácil.
- Encapsulamento. Em alguns exemplos declarei funções intermediárias (por exemplo, vendido, comprou etc.), a partir do qual as funções subsequentes foram construídas. Isso permite alterar a lógica de determinadas funções, se necessário, sem alterar a lógica daquelas que delas dependem. Por exemplo, você pode fazer vendas vendido foram calculados a partir de objetos completamente diferentes, enquanto o resto da lógica não mudará. Sim, isso pode ser implementado em um RDBMS usando CREATE VIEW. Mas se toda a lógica for escrita desta forma, não parecerá muito legível.
- Sem lacuna semântica. Tal banco de dados opera em funções e classes (em vez de tabelas e campos). Assim como na programação clássica (se assumirmos que um método é uma função com o primeiro parâmetro na forma da classe à qual pertence). Conseqüentemente, deveria ser muito mais fácil “fazer amizade” com linguagens de programação universais. Além disso, este conceito permite a implementação de funcionalidades muito mais complexas. Por exemplo, você pode incorporar operadores como:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Herança e polimorfismo. Em um banco de dados funcional, você pode introduzir herança múltipla por meio das construções CLASS ClassP: Class1, Class2 e implementar polimorfismo múltiplo. Provavelmente escreverei exatamente como em artigos futuros.
Mesmo sendo apenas um conceito, já temos alguma implementação em Java que traduz toda a lógica funcional em lógica relacional. Além disso, a lógica das representações e muitas outras coisas estão lindamente ligadas a ela, graças às quais obtemos um todo
Fonte: habr.com