SGBD fonctionnel

Le monde des bases de données a longtemps été dominé par les SGBD relationnels, qui utilisent le langage SQL. À tel point que les variantes émergentes sont appelées NoSQL. Ils ont réussi à se tailler une certaine place sur ce marché, mais les SGBD relationnels ne vont pas mourir et continuent d'être activement utilisés à leurs fins.

Dans cet article, je souhaite décrire le concept de base de données fonctionnelle. Pour une meilleure compréhension, je ferai cela en le comparant avec le modèle relationnel classique. Des problèmes issus de divers tests SQL trouvés sur Internet seront utilisés comme exemples.

introduction

Les bases de données relationnelles fonctionnent sur des tables et des champs. Dans une base de données fonctionnelle, les classes et les fonctions seront utilisées respectivement à la place. Un champ d'une table à N clés sera représenté en fonction de N paramètres. Au lieu de relations entre les tables, des fonctions seront utilisées qui renvoient des objets de la classe à laquelle la connexion est établie. La composition des fonctions sera utilisée à la place de JOIN.

Avant de passer directement aux tâches, je décrirai la tâche de logique de domaine. Pour DDL, j'utiliserai la syntaxe PostgreSQL. Pour le fonctionnel, il a sa propre syntaxe.

Tables et champs

Un simple objet Sku avec des champs de nom et de prix :

Relationnel

CREATE TABLE Sku
(
    id bigint NOT NULL,
    name character varying(100),
    price numeric(10,5),
    CONSTRAINT id_pkey PRIMARY KEY (id)
)

Fonctionnel

CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);

Nous annonçons deux fonctions, qui prennent un paramètre Sku en entrée et renvoient un type primitif.

On suppose que dans un SGBD fonctionnel, chaque objet aura un code interne généré automatiquement et accessible si nécessaire.

Fixons le prix du produit/magasin/fournisseur. Cela peut changer avec le temps, ajoutons donc un champ temporel au tableau. Je vais sauter la déclaration des tables pour les répertoires dans une base de données relationnelle pour raccourcir le code :

Relationnel

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)
)

Fonctionnel

CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);

Indices

Pour le dernier exemple, nous allons construire un index sur toutes les clés et la date afin de pouvoir retrouver rapidement le prix pour une heure précise.

Relationnel

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

Fonctionnel

INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);

Tâches

Commençons par des problèmes relativement simples tirés du articles sur Habr.

Tout d'abord, déclarons la logique du domaine (pour la base de données relationnelle cela se fait directement dans l'article ci-dessus).

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);

Tâche 1.1

Afficher une liste des employés qui reçoivent un salaire supérieur à celui de leur supérieur immédiat.

Relationnel

select a.*
from   employee a, employee b
where  b.id = a.chief_id
and    a.salary > b.salary

Fonctionnel

SELECT name(Employee a) WHERE salary(a) > salary(chief(a));

Tâche 1.2

Lister les employés qui reçoivent le salaire maximum dans leur service

Relationnel

select a.*
from   employee a
where  a.salary = ( select max(salary) from employee b
                    where  b.department_id = a.department_id )

Fonctionnel

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));

Les deux implémentations sont équivalentes. Pour le premier cas, dans une base de données relationnelle, vous pouvez utiliser CREATE VIEW, qui de la même manière calculera d'abord le salaire maximum pour un département spécifique. Dans ce qui suit, par souci de clarté, j'utiliserai le premier cas, car il reflète mieux la solution.

Tâche 1.3

Afficher une liste des numéros de service dont le nombre d'employés ne dépasse pas 3 personnes.

Relationnel

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

Fonctionnel

countEmployees 'Количество сотрудников' (Department d) = 
    GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;

Tâche 1.4

Affichez une liste des employés qui n'ont pas de responsable désigné travaillant dans le même service.

Relationnel

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

Fonctionnel

SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));

Tâche 1.5

Recherchez une liste d’ID de service avec le salaire total maximum de l’employé.

Relationnel

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 )

Fonctionnel

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();

Passons à des tâches plus complexes d'un autre articles. Il contient une analyse détaillée de la manière de mettre en œuvre cette tâche dans MS SQL.

Tâche 2.1

Quels vendeurs ont vendu plus de 1997 unités du produit n°30 en 1 ?

Logique de domaine (comme auparavant sur le SGBDR, nous ignorons la déclaration) :

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);

Relationnel

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

Fonctionnel

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;

Tâche 2.2

Pour chaque acheteur (nom, prénom), trouvez les deux biens (nom) pour lesquels l'acheteur a dépensé le plus d'argent en 1997.

Nous étendons la logique de domaine de l'exemple précédent :

CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);

customer = DATA Customer (Order);

unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);

Relationnel

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

Fonctionnel

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;

L'opérateur PARTITION fonctionne sur le principe suivant : il additionne l'expression spécifiée après SOMME (ici 1), au sein des groupes spécifiés (ici Client et Année, mais il peut s'agir de n'importe quelle expression), en triant au sein des groupes selon les expressions spécifiées dans ORDER ( ici acheté, et si égal, alors selon le code produit interne).

Tâche 2.3

Combien de marchandises doivent être commandées auprès des fournisseurs pour honorer les commandes en cours.

Développons à nouveau la logique du domaine :

CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);

supplier = DATA Supplier (Product);

unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);

Relationnel

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

Fonctionnel

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;

Problème avec un astérisque

Et le dernier exemple vient de moi personnellement. Il y a la logique d’un réseau social. Les gens peuvent être amis les uns avec les autres et s’apprécier. Du point de vue d'une base de données fonctionnelle, cela ressemblerait à ceci :

CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);

Il est nécessaire de trouver des candidats possibles à l'amitié. Plus formellement, vous devez trouver toutes les personnes A, B, C telles que A soit ami avec B et B soit ami avec C, A aime C, mais A n'est pas ami avec C.
Du point de vue d'une base de données fonctionnelle, la requête ressemblerait à ceci :

SELECT Person a, Person b, Person c WHERE 
    likes(a, c) AND NOT friends(a, c) AND 
    friends(a, b) AND friends(b, c);

Le lecteur est encouragé à résoudre ce problème en SQL par lui-même. On suppose qu’il y a beaucoup moins d’amis que de personnes que l’on aime. Ils sont donc dans des tableaux séparés. En cas de réussite, il existe également une tâche avec deux étoiles. Dans ce document, l'amitié n'est pas symétrique. Sur une base de données fonctionnelle, cela ressemblerait à ceci :

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 : solution au problème du premier et du deuxième astérisque 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 

Conclusion

Il convient de noter que la syntaxe du langage donnée n'est qu'une des options permettant de mettre en œuvre ce concept. SQL a été pris comme base et l'objectif était qu'il soit aussi similaire que possible. Bien sûr, certains n’aimeront peut-être pas les noms de mots-clés, les registres de mots, etc. L'essentiel ici est le concept lui-même. Si vous le souhaitez, vous pouvez créer une syntaxe similaire à C++ et Python.

Le concept de base de données décrit présente, à mon avis, les avantages suivants :

  • Facilité. Il s’agit d’un indicateur relativement subjectif qui n’est pas évident dans les cas simples. Mais si vous examinez des cas plus complexes (par exemple, des problèmes avec les astérisques), alors, à mon avis, écrire de telles requêtes est beaucoup plus facile.
  • Инкапсуляция. Dans certains exemples, j'ai déclaré des fonctions intermédiaires (par exemple, vendu, acheté etc.), à partir desquels les fonctions ultérieures ont été construites. Cela permet de changer la logique de certaines fonctions, si nécessaire, sans changer la logique de celles qui en dépendent. Par exemple, vous pouvez réaliser des ventes vendu ont été calculés à partir d’objets complètement différents, alors que le reste de la logique ne changera pas. Oui, cela peut être implémenté dans un SGBDR à l'aide de CREATE VIEW. Mais si toute la logique est écrite de cette façon, elle ne semblera pas très lisible.
  • Pas de vide sémantique. Une telle base de données fonctionne sur des fonctions et des classes (au lieu de tables et de champs). Tout comme en programmation classique (si l'on suppose qu'une méthode est une fonction dont le premier paramètre est la forme de la classe à laquelle elle appartient). En conséquence, il devrait être beaucoup plus facile de « se faire des amis » avec les langages de programmation universels. De plus, ce concept permet de mettre en œuvre des fonctionnalités beaucoup plus complexes. Par exemple, vous pouvez intégrer des opérateurs tels que :

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE  'Что-то Петя продает слишком много одного товара в 2019 году';

  • Héritage et polymorphisme. Dans une base de données fonctionnelle, vous pouvez introduire l'héritage multiple via les constructions CLASS ClassP : Class1, Class2 et implémenter un polymorphisme multiple. J’écrirai probablement comment exactement dans les prochains articles.

Même s’il ne s’agit que d’un concept, nous disposons déjà d’une implémentation en Java qui traduit toute la logique fonctionnelle en logique relationnelle. De plus, la logique des représentations et bien d'autres choses y sont magnifiquement attachées, grâce à quoi on obtient tout un plate-forme. Essentiellement, nous utilisons le SGBDR (uniquement PostgreSQL pour l'instant) comme une « machine virtuelle ». Des problèmes surviennent parfois avec cette traduction car l'optimiseur de requêtes RDBMS ne connaît pas certaines statistiques que connaît le FDBMS. En théorie, il est possible de mettre en œuvre un système de gestion de base de données qui utilisera une certaine structure comme stockage, adaptée spécifiquement à la logique fonctionnelle.

Source: habr.com

Ajouter un commentaire