Sistema de Gerenciamento de Banco de Dados Funcional

O mundo dos bancos de dados há muito tempo é dominado por SGBDs relacionais, que utilizam a linguagem SQL. Isso é tão forte que as variantes que surgem são chamadas de NoSQL. Elas conseguiram conquistar um espaço no mercado, mas os SGBDs relacionais não estão com intenção de desaparecer e continuam a ser amplamente utilizados para suas finalidades.

Neste artigo, quero descrever o conceito de banco de dados funcional. Para melhor compreensão, farei isso comparando com o modelo relacional clássico. Como exemplos, serão utilizados desafios de diversos testes de SQL encontrados na internet.

Introdução

Os bancos de dados relacionais operam com tabelas e campos. Em um banco de dados funcional, eles serão substituídos por classes e funções, respectivamente. Um campo em uma tabela com N chaves será representado como uma função de N parâmetros. Em vez de relacionamentos entre tabelas, serão usadas funções que retornam objetos da classe à qual se conecta. Em vez de JOIN, usaremos a composição de funções.

Antes de entrar diretamente nos desafios, descreverei a tarefa da lógica de domínio. Para DDL, utilizarei a sintaxe PostgreSQL. Para funcional, minha própria sintaxe.

Tabelas e campos

Um objeto simples Sku com campos 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 = STRING DE DADOS[100] (Sku);
price = NÚMERO DE DADOS[10,5] (Sku);

Declaramos duas funções, que aceitam um parâmetro Sku e retornam um tipo primitivo.

Pressupõe-se que em um SGBD funcional, cada objeto terá um código interno, que é gerado automaticamente e que pode ser acessado quando necessário.

Definiremos o preço para o produto / loja / fornecedor. Ele pode mudar com o tempo, portanto, adicionaremos um campo de tempo na tabela. A declaração das tabelas para os dicionários em um banco de dados relacional será omitida 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 Loja;
CLASS Fornecedor;
dateTime = DADOS DATETIME(Sku, Loja, Fornecedor);
preço = DADOS NUMÉRICO[10,5](Sku, Loja, Fornecedor);

Índices

Para o último exemplo, construiremos um índice com todas as chaves e a data, para que seja possível encontrar rapidamente o preço em um determinado momento.

Relacional

CREATE INDEX prices_date
    ON prices
    (skuId, storeId, supplierId, dateTime)

Funcional

ÍNDICE Sku sk, Loja st, Fornecedor sp, dateTime(sk, st, sp)

Tarefas

Vamos começar com tarefas relativamente simples, retiradas do correspondente artigo no Habr.

Primeiro, vamos declarar a lógica de domínio (para banco de dados relacional, isso é feito diretamente no artigo apresentado).

Departamento de CLASS;
nome = STRING DE DADOS[100] (Departamento);

CLASS Employee;
department = DATA Department (Employee);
chief = DATA Employee (Employee);
name = DATA STRING[100] (Employee);
salary = DATA NUMERIC[14,2] (Employee);

Tarefa 1.1

Exibir a lista de funcionários com 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

SELECIONE nome(Funcionário a) ONDE salário(a) > salário(líder(a));

Tarefa 1.2

Exibir a lista de funcionários que recebem o maior salário 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 'Salário máximo' (Departamento s) = 
    GROUP MAX salário(Funcionário e) SE departamento(e) = s;
SELECT nome(Funcionário a) ONDE salário(a) = maxSalary(departamento(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, pode-se usar CREATE VIEW, que calculará da mesma forma inicialmente o maior salário para um departamento específico. Daqui em diante, para maior clareza, utilizarei o primeiro caso, pois ele reflete melhor a solução.

Tarefa 1.3

Exibir a lista de IDs de departamentos, cujo número de funcionários não excede 3.

Relacional

select department_id
from employee
group by department_id
having count(*) <= 3

Funcional

countEmployees 'Número de funcionários' (Departamento d) = 
    GROUP SUM 1 IF departamento(Employee e) = d;
SELECT Departamento d WHERE countEmployees(d) <= 3;

Tarefa 1.4

Exibir a lista de funcionários que não têm um supervisor designado, mas que trabalham 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

SELECIONE nome(Funcionário a) ONDE NÃO (departamento(chefe(a)) = departamento(a));

Tarefa 1.5

Encontrar a lista de IDs de departamentos com o maior salário total dos funcionários.

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 'Salário Máximo' (Departamento d) = 
    GROUP SUM salário(Employee e) IF departamento(e) = d;
maxSalarySum 'Salário Máximo dos Departamentos' () = 
    GROUP MAX salarySum(Departamento d);
SELECT Departamento d WHERE salarySum(d) = maxSalarySum();

Vamos passar para tarefas mais complexas de outro artigo. Nele, há uma análise detalhada de como implementar essa tarefa no MS SQL.

Tarefa 2.1

Quais vendedores venderam mais de 30 unidades do produto nº 1 em 1997?

Lógica de domínio (como antes em RDBMS, pulamos a declaração):

CLASS Employee 'Vendedor';
lastName 'Sobrenome' = DATA STRING[100] (Employee);

CLASS Product 'Produto';
id = DATA INTEGER (Product);
name = DATA STRING[100] (Product);

CLASS Order 'Pedido';
date = DATA DATE (Order);
employee = DATA Employee (Order);

CLASS Detail 'Linha do pedido';

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

vendido (Funcionário e, INTEIRO idProduto, INTEIRO ano) = 
    SOMA GRUPO quantidade(DetalhePedido d) SE 
        funcionário(pedido(d)) = e E 
        id(produto(d)) = idProduto E 
        extrairAno(data(pedido(d))) = ano;
SELECIONE sobrenome(Funcionário e) ONDE vendido(e, 1, 1997) > 30;

Tarefa 2.2

Para cada cliente (nome, sobrenome), encontrar dois produtos (nome) nos quais o cliente gastou mais dinheiro em 1997.

Expandindo a lógica de domínio do exemplo anterior:

CLASS Customer 'Cliente';
contactName 'Nome Completo' = DATA STRING[100] (Cliente);

cliente = DADOS Cliente (Pedido);

precoUnitario = DADOS NUMÉRICO[14,2] (Detalhe);
desconto = DADOS NUMÉRICO[6,2] (Detalhe);

Relacional

SELECIONAR NomeContato, NomeProduto DE (
SELECIONAR c.NomeContato, p.NomeProduto
, NÚMERO_LINHA() SOBRE (
    PARTIÇÃO POR c.NomeContato
    ORDENAÇÃO POR SOMA(od.Quantidade * od.PrecoUnitario * (1 - od.Desconto)) DESC
) COMO AvaliaçãoPorValor
DE Clientes c
JUNTAR Pedidos o ON o.IDCliente = c.IDCliente
JUNTAR [Detalhes do Pedido] od ON od.IDPedido = o.IDPedido
JUNTAR Produtos p ON p.IDProduto = od.IDProduto
ONDE ANO(o.DataPedido) = 1997
AGRUPAR POR c.NomeContato, p.NomeProduto
) t
ONDE AvaliaçãoPorValor < 3

Funcional

soma (Detalhe d) = quantidade(d) * precoUnitario(d) * (1 - desconto(d));
comprou 'Comprou' (Cliente c, Produto p, INTEIRO y) = 
    SOMA DO GRUPO soma(Detalhe d) SE 
        cliente(pedido(d)) = c E 
        produto(d) = p E 
        extrairAno(data(pedido(d))) = y;
avaliação 'Avaliação' (Cliente c, Produto p, INTEIRO y) = 
    SOMA DE PARTIÇÃO 1 ORDENAR DESC comprado(c, p, y), p POR c, y;
SELECIONE nomeContato(Cliente c), nome(Produto p) ONDE avaliação(c, p, 1997) < 3;

O operador PARTIÇÃO funciona da seguinte forma: ele soma a expressão indicada após SOMA (aqui 1), dentro dos grupos especificados (aqui Cliente e Ano, mas pode ser qualquer expressão), ordenando dentro dos grupos de acordo com as expressões especificadas na ORDENAÇÃO (aqui comprado, e se forem iguais, pelo código interno do produto).

Tarefa 2.3

Quantos produtos precisam ser encomendados aos fornecedores para atender aos pedidos atuais.

Mais uma vez, ampliamos a lógica de domínio:

CLASS Supplier 'Fornecedor';
companyName = DATA STRING[100] (Fornecedor);

fornecedor = DADOS Fornecedor (Produto);

unidadesEmEstoque 'Saldo em estoque' = DADOS NUMÉRICO[10,3] (Produto);
nivelDeReabastecimento 'Nível de reabastecimento' = DADOS NUMÉRICO[10,3] (Produto);

Relacional

selecionar s.NomeEmpresa, p.NomeProduto, soma(od.Quantidade) + p.NivelDeReabastecimento - p.UnidadesEmEstoque como ParaOrdenar
de Pedidos o
juntar [Detalhes do Pedido] od ON o.IDPedido = od.IDPedido
juntar Produtos p ON od.IDProduto = p.IDProduto
juntar Fornecedores s ON p.IDFornecedor = s.IDFornecedor
onde o.DataEnviada é nula
grupo por s.NomeEmpresa, p.NomeProduto, p.UnidadesEmEstoque, p.NivelDeReabastecimento
tendo p.UnidadesEmEstoque < soma(od.Quantidade) + p.NivelDeReabastecimento

Funcional

pedidoNaoDespachado 'Encomendado, mas não enviado' (Produto p) = 
    SOMA GRUPO quantidade(DetalhePedido d) SE produto(d) = p;
paraEncomendar 'Para encomendar' (Produto p) = pedidoNaoDespachado(p) + nivelReorder(p) - unidadesEmEstoque(p);
SELECIONE nomeEmpresa(fornecedor(Produto p)), nome(p), paraEncomendar(p) ONDE paraEncomendar(p) > 0;

Tarefa com estrela

E um último exemplo pessoal. Existe a lógica de uma rede social. As pessoas podem ser amigas umas das outras e gostar umas das outras. Do ponto de vista do banco de dados funcional, isso seria assim:

CLASS Pessoa;
curte = DADOS BOOLEANO (Pessoa, Pessoa);
amigos = DADOS BOOLEANO (Pessoa, Pessoa);

É necessário encontrar possíveis candidatos à amizade. Mais formalmente, precisamos encontrar todas as pessoas A, B, C, tais que A é amigo de B, B é amigo de C, A gosta de C, mas A não é amigo de C.
Do ponto de vista do banco de dados funcional, a consulta será assim:

SELECIONE a Pessoa a, a Pessoa b, a Pessoa c ONDE 
    gosta(a, c) E NÃO amigos(a, c) E 
    amigos(a, b) E amigos(b, c);

O leitor é convidado a resolver esta questão em SQL. Supõe-se que haja muito menos amigos do que pessoas de quem se gosta. Portanto, eles estão em tabelas separadas. Em caso de solução bem-sucedida, há também uma tarefa com duas estrelas. Nela, a amizade não é simétrica. No banco de dados funcional, isso seria assim:

SELECIONE a Pessoa a, a Pessoa b, a Pessoa c ONDE 
    gosta(a, c) E NÃO amigos(a, c) E 
    (amigos(a, b) OU amigos(b, a)) E 
    (amigos(b, c) OU amigos(c, b));

UPD: solução da tarefa com a primeira e segunda estrelas de dss_kalika:

SELECIONE 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
DE Pessoas                 AS p
--Curtidas                      
JUNTE-SE à RelaçãoDePessoa      AS pl ON pl.PersonAID = p.PersonID
                                  E pl.Relation  = 'Curtir'
--Amigos                     
JUNTE-SE à RelaçãoDePessoa      AS pf ON pf.PersonAID = p.PersonID 
                                  E pf.Relation = 'Amigo'
--Amigos de Amigos              
JUNTE-SE à RelaçãoDePessoa      AS pff ON pff.PersonAID = pf.PersonBID
                                   E pff.PersonBID = pl.PersonBID
                                   E pff.Relation = 'Amigo'
--Ainda não são amigos         
DEIXE JUNTA a RelaçãoDePessoa AS pnf ON pnf.PersonAID = p.PersonID
                                   E pnf.PersonBID = pff.PersonBID
                                   E pnf.Relation = 'Amigo'
ONDE pnf.PersonAID É NULO 

;COM RelaçãoDePessoaColapsada AS (
  SELECIONE pl.PersonAID
        ,pl.PersonBID
        ,pl.Relation 
  DE #RelaçãoDePessoa      AS pl 
  
  UNIÃO 

  SELECIONE pl.PersonBID AS PersonAID
        ,pl.PersonAID AS PersonBID
        ,pl.Relation
  DE #RelaçãoDePessoa      AS pl 
)
SELECIONE 
   pl.PersonAID
  ,pf.PersonBID
  ,pff.PersonBID
DE #Pessoas                      AS p
--Curtidas                      
JUNTE-SE à RelaçãoDePessoaColapsada  AS pl ON pl.PersonAID = p.PersonID
                                 E pl.Relation  = 'Curtir'                                  
--Amigos                          
JUNTE-SE à RelaçãoDePessoaColapsada  AS pf ON pf.PersonAID = p.PersonID 
                                 E pf.Relation = 'Amigo'
--Amigos de Amigos                   
JUNTE-SE à RelaçãoDePessoaColapsada  AS pff ON pff.PersonAID = pf.PersonBID
                                 E pff.PersonBID = pl.PersonBID
                                 E pff.Relation = 'Amigo'
--Ainda não são amigos                   
DEIXE JUNTA a RelaçãoDePessoaColapsada AS pnf ON pnf.PersonAID = p.PersonID
                                   E pnf.PersonBID = pff.PersonBID
                                   E pnf.Relation = 'Amigo'
ONDE pnf.[PersonAID] É NULO 

Conclusão

Deve-se notar que a sintaxe da linguagem apresentada é apenas uma das opções para implementar o conceito mencionado. O SQL foi usado como base, e o objetivo era que se assemelhasse ao máximo a ele. É claro que algumas pessoas podem não gostar dos nomes das palavras-chave, dos registros das palavras e assim por diante. Aqui, o principal é exatamente o conceito em si. Se desejado, pode-se criar uma sintaxe semelhante em C++ ou Python.

Na minha opinião, o conceito de banco de dados descrito possui as seguintes vantagens:

  • Facilidade. Este é um indicador relativamente subjetivo, que não é óbvio em casos simples. Mas se examinarmos casos mais complexos (por exemplo, tarefas com asteriscos), então, a meu ver, escrever tais consultas é significativamente mais fácil.
  • Encapsulamento. Em alguns exemplos, declarei funções intermediárias (por exemplo, vendido, comprado e assim por diante), das quais se baseavam as funções subsequentes. Isso permite, se necessário, alterar a lógica de determinadas funções sem mudar a lógica das que dependem delas. Por exemplo, pode-se fazer com que as vendas vendido consideravam-se de objetos completamente diferentes, enquanto a lógica restante não mudará. Sim, em SGBDs isso pode ser implementado através do CREATE VIEW. Mas se toda a lógica for escrita dessa maneira, ela se tornará bastante ilegível.
  • Ausência de ruptura semântica. Esse banco de dados opera com funções e classes (em vez de tabelas e campos). Assim como na programação clássica (considerando que o método é uma função com o primeiro parâmetro sendo a classe à qual pertence). Portanto, "casar" com linguagens de programação universais deve ser significativamente mais fácil. Além disso, esse conceito permite implementar funções muito mais complexas. Por exemplo, é possível incorporar no banco de dados operadores como:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Petya' MESSAGE 'Algo Petya está vendendo demais de um único produto em 2019';

  • Herança e polimorfismo. Em um banco de dados funcional, é possível introduzir herança múltipla através da construção CLASS ClassP: Class1, Class2 e implementar polimorfismo múltiplo. Como exatamente, talvez eu escreva em artigos futuros.

Embora isso seja apenas um conceito, já temos alguma implementação em Java que traduz toda a lógica funcional para lógica relacional. Além disso, foi elegantemente integrada a lógica de visualizações e muitas outras coisas, resultando em um a plataforma. Essencialmente, estamos usando SGBDs (por enquanto apenas PostgreSQL) como uma "máquina virtual". Com essa tradução, às vezes ocorrem problemas, uma vez que o otimizador de consultas do SGBD não conhece certas estatísticas que o SGBD funcional conhece. Em teoria, é possível implementar um sistema gerenciador de banco de dados que usará uma estrutura adaptada especificamente para a lógica funcional como armazenamento.

Fonte: habr.com

Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS 🔥 Compre hospedagem confiável para sites com proteção contra DDoS, servidores VPS VDS | ProHoster