SGBD funcional

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 artigos em Habr.

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 artigos. Ele contém uma análise detalhada de como implementar esta tarefa no MS SQL.

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

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 plataforma. Essencialmente, usamos o RDBMS (apenas PostgreSQL por enquanto) como uma “máquina virtual”. Às vezes surgem problemas com esta tradução porque o otimizador de consulta RDBMS não conhece certas estatísticas que o FDBMS conhece. Em teoria, é possível implementar um sistema de gerenciamento de banco de dados que utilizará uma determinada estrutura como armazenamento, adaptada especificamente para a lógica funcional.

Fonte: habr.com

Adicionar um comentário