Le monde des bases de données est depuis longtemps dominé par les SGBD relationnels, qui utilisent le langage SQL. À tel point que les variantes qui apparaissent sont appelées NoSQL. Elles ont réussi à se tailler une place sur ce marché, mais les SGBD relationnels ne sont pas près de disparaître et continuent d'être utilisés activement pour leurs objectifs.
Dans cet article, je souhaite décrire le concept de base de données fonctionnelle. Pour une meilleure compréhension, je procéderai par comparaison avec le modèle relationnel classique. Des tâches issues de divers tests SQL trouvés sur Internet seront utilisées comme exemples.
Introduction
Les bases de données relationnelles fonctionnent avec des tables et des champs. Dans une base de données fonctionnelle, on utilisera à la place des classes et des fonctions respectivement. Un champ dans une table avec N clés sera représenté comme une fonction à N paramètres. Au lieu de relations entre les tables, des fonctions qui retournent des objets de la classe liée seront utilisées. Au lieu de JOIN, on utilisera la composition de fonctions.
Avant de passer aux tâches, je vais décrire l'affectation de la logique métier. Pour le DDL, j'utiliserai la syntaxe PostgreSQL. Pour la partie fonctionnelle, j'utiliserai ma propre syntaxe.
Tables et champs
Un objet Sku simple avec les champs nom et prix :
Relationnel
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
Fonctionnelle
CLASSE Sku;
nom = CHAÎNE DE DONNÉES[100] (Sku);
prix = DÉCIMAL NUMÉRIQUE[10,5] (Sku);
Nous déclarons deux des fonctions, qui prennent en entrée un paramètre Sku, et retournent un type primitif.
Il est supposé que dans un SGBD fonctionnel, chaque objet aura un code interne, qui est généré automatiquement, et auquel on peut faire appel si nécessaire.
Définissons le prix pour un produit / magasin / fournisseur. Celui-ci peut changer au fil du temps, c'est pourquoi nous ajoutons un champ temps à la table. Je vais passer la déclaration des tables pour les répertoires dans la base de données relationnelle pour réduire 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)
)
Fonctionnelle
CLASSE Sku;
CLASS Magasin;
CLASS Fournisseur;
dateTime = DONNÉE DATETIME (Sku, Magasin, Fournisseur);
prix = DONNÉE NUMÉRIQUE[10,5] (Sku, Magasin, Fournisseur);
Indices
Pour le dernier exemple, construisons un index sur toutes les clés et la date, afin de pouvoir trouver rapidement le prix à un moment donné.
Relationnel
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Fonctionnelle
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
Objectifs
Commençons par des tâches relativement simples, tirées de la correspondante sur Habr.
Tout d'abord, déclarons la logique de domaine (pour une base de données relationnelle, cela est fait directement dans l'article présenté).
CLASS Département;
nom = CHAÎNE DE DONNÉES[100] (Département);
CLASS Employé;
département = DONNÉES Département (Employé);
chef = DONNÉES Employé (Employé);
nom = DONNÉES CHAÎNE[100] (Employé);
salaire = DONNÉES NUMÉRIQUE[14,2] (Employé);
Tâche 1.1
Afficher la liste des employés ayant un salaire supérieur à celui de leur supérieur direct.
Relationnel
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Fonctionnelle
SÉLECTIONNER nom(Employee a) OÙ salaire(a) > salaire(chief(a));
Tâche 1.2
Afficher la liste des employés ayant le salaire maximal dans leur département
Relationnel
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
Fonctionnelle
maxSalary 'Salaire maximum' (Département s) =
GROUPE MAX salaire (Employé e) SI département(e) = s ;
SÉLECTIONNER name(Employé a) OÙ salaire(a) = maxSalary(département(a));
// или если "заинлайнить"
SELECT nom(Employé a) OÙ
salaire(a) = maxSalaire(GROUPE MAX salaire(Employé e) SI département(e) = département(a));
Les deux réalisations sont équivalentes. Pour le premier cas, une vue relationnelle peut être utilisée, qui calculera de la même manière le salaire maximal pour un département donné. Par la suite, je vais utiliser le premier cas pour plus de clarté, car il reflète mieux la solution.
Tâche 1.3
Afficher la liste des ID des départements comptant trois employés ou moins.
Relationnel
select department_id
from employee
group by department_id
having count(*) <= 3
Fonctionnelle
countEmployees 'Nombre d'employés' (Department d) =
GROUPE SOMME 1 SI department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
Tâche 1.4
Afficher la liste des employés n'ayant pas de chef désigné mais travaillant dans le même département.
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
Fonctionnelle
SÉLECTIONNER nom(Employee a) OÙ NON (département(chief(a)) = département(a));
Tâche 1.5
Trouver la liste des ID des départements avec le salaire total maximal des employés.
Relationnel
with somme_salaire as
( select department_id, sum(salary) salaire
from employee
group by department_id )
select department_id
from somme_salaire a
where a.salaire = ( select max(salaire) from somme_salaire )
Fonctionnelle
salaireSomme 'Salaire maximum' (Département d) =
GROUPE SOMME salary(Employee e) SI department(e) = d;
salaireMaxSomme 'Salaire maximum des départements' () =
GROUPE MAX salaireSomme(Département d);
SELECT Département d WHERE salaireSomme(d) = salaireMaxSomme();
Passons à des tâches plus complexes d'un autre . Il contient une explication détaillée sur la manière de réaliser cette tâche sur MS SQL.
Tâche 2.1
Quels vendeurs ont vendu plus de 30 pièces du produit n°1 en 1997 ?
La logique de domaine (comme auparavant, dans RDBMS nous ignorons la déclaration) :
CLASS Employee 'Vendeur';
lastName 'Nom de famille' = DATA STRING[100] (Employee);
CLASS Produit 'Produit';
id = DONNÉES ENTIER (Produit);
nom = DONNÉES CHAÎNE[100] (Produit);
CLASS Commande 'Commande';
date = DONNÉES DATE (Commande);
employé = DONNÉES Employé (Commande);
CLASS Détail 'Ligne de commande';
commande = DONNÉES Commande (Détail);
produit = DONNÉES Produit (Détail);
quantité = DONNÉES NUMÉRIQUE[10,5] (Détail);
Relationnel
select Nom
from Employés as e
where (
select sum(od.Quantity)
from [Détails de Commande] as od
where od.ProductID = 1 and od.OrderID in (
select o.OrderID
from Commandes as o
where year(o.OrderDate) = 1997 and e.EmployeeID = o.EmployeeID)
) > 30
Fonctionnelle
vendu (Employé e, INTEGER produitId, INTEGER année) =
GROUPE SOMME quantité(DétailCommande d) SI
employé(commande(d)) = e ET
id(produit(d)) = produitId ET
extraireAnnée(date(commande(d))) = année;
SELECTIONNER nomDeFamille(Employé e) WHERE vendu(e, 1, 1997) > 30;
Tâche 2.2
Pour chaque client (prénom, nom), trouver deux produits (noms) sur lesquels le client a dépensé le plus d'argent en 1997.
Nous élargissons la logique de domaine de l'exemple précédent :
CLASS Customer 'Client';
contactName 'Nom complet' = DATA STRING[100] (Client);
client = DATA Client (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
Fonctionnelle
somme (Détail d) = quantité(d) * prixUnitaire(d) * (1 - remise(d));
acheté 'Achat' (Client c, Produit p, ENTIER y) =
SOMME DU GROUPE sum(Détail d) SI
client(commande(d)) = c ET
produit(d) = p ET
extraireAnnée(date(commande(d))) = y;
évaluation 'Évaluation' (Client c, Produit p, ENTIER y) =
SOMME DE PARTITION 1 ORDRE DESC acheté(c, p, y), p PAR c, y;
SÉLECTIONNER contactNom(Client c), nom(Produit p) OÙ évaluation(c, p, 1997) < 3;
L'opérateur PARTITION fonctionne de la manière suivante : il additionne l'expression spécifiée après SUM (ici 1), à l'intérieur des groupes spécifiés (ici Client et Année, mais cela peut être n'importe quelle expression), en triant à l'intérieur des groupes selon les expressions spécifiées dans ORDER (ici achetées, et si égales, selon le code produit interne).
Tâche 2.3
Combien de produits doivent être commandés auprès des fournisseurs pour satisfaire les commandes en cours.
Nous élargissons à nouveau la logique de domaine :
CLASS Supplier 'Fournisseur';
companyName = DATA STRING[100] (Fournisseur);
supplier = DATA Supplier (Product);
unitsInStock 'Stock restant' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Niveau de réapprovisionnement' = 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
Fonctionnelle
commandéMaisNonExpédié 'Commandé, mais non expédié' (Produit p) =
SOMME_GROUPE quantité(DétailCommande d) SI produit(d) = p;
àCommander 'À commander' (Produit p) = commandéMaisNonExpédié(p) + niveauDeRéapprovisionnement(p) - unitésEnStock(p);
SELECT nomEntreprise(fournisseur(Produit p)), nom(p), àCommander(p) WHERE àCommander(p) > 0;
Tâche avec étoile
Et enfin un exemple personnel. Il existe une logique de réseau social. Les gens peuvent être amis les uns avec les autres et s'aimer. Du point de vue d'une base de données fonctionnelle, cela ressemblera à ceci :
CLASSE Person;
aime = DONNÉE BOOLEAN (Person, Person);
amis = DONNÉE BOOLEAN (Person, Person);
Il est nécessaire de trouver de potentiels candidats à l'amitié. Plus formellement, il faut trouver toutes les personnes A, B, C telles que A est ami avec B, B est 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 ressemblera à ceci :
SÉLECTIONNER Personne a, Personne b, Personne c OÙ
aime(a, c) ET PAS amis(a, c) ET
amis(a, b) ET amis(b, c);
Il est demandé au lecteur de résoudre cette tâche en SQL par lui-même. On suppose que les amis sont beaucoup moins nombreux que ceux qui aiment. Ainsi, ils se trouvent dans des tables distinctes. En cas de solution réussie, il y a également une tâche avec deux étoiles. Dans celle-ci, l'amitié n'est pas symétrique. Du point de vue d'une base de données fonctionnelle, cela ressemblera à cela :
SÉLECTIONNER Personne a, Personne b, Personne c OÙ
aime(a, c) ET PAS amis(a, c) ET
(amis(a, b) OU amis(b, a)) ET
(amis(b, c) OU amis(c, b));
UPD : solution de la tâche avec la première et la deuxième étoile de :
SÉLECTIONNER
pl.PersonAID
,pf.PersonAID
,pff.PersonAID
DE Persons AS p
--Likes
JOIN PersonRelationShip AS pl ON pl.PersonAID = p.PersonID
ET pl.Relation = 'Like'
--Amis
JOIN PersonRelationShip AS pf ON pf.PersonAID = p.PersonID
ET pf.Relation = 'Friend'
--Amis des amis
JOIN PersonRelationShip AS pff ON pff.PersonAID = pf.PersonBID
ET pff.PersonBID = pl.PersonBID
ET pff.Relation = 'Friend'
--Pas encore amis
LEFT JOIN PersonRelationShip AS pnf ON pnf.PersonAID = p.PersonID
ET pnf.PersonBID = pff.PersonBID
ET pnf.Relation = 'Friend'
OÙ pnf.PersonAID EST NULL
;AVEC PersonRelationShipCollapsed AS (
SÉLECTIONNER pl.PersonAID
,pl.PersonBID
,pl.Relation
DE #PersonRelationShip AS pl
UNION
SÉLECTIONNER pl.PersonBID COMME PersonAID
,pl.PersonAID COMME PersonBID
,pl.Relation
DE #PersonRelationShip AS pl
)
SÉLECTIONNER
pl.PersonAID
,pf.PersonBID
,pff.PersonBID
DE #Persons AS p
--Likes
JOIN PersonRelationShipCollapsed AS pl ON pl.PersonAID = p.PersonID
ET pl.Relation = 'Like'
--Amis
JOIN PersonRelationShipCollapsed AS pf ON pf.PersonAID = p.PersonID
ET pf.Relation = 'Friend'
--Amis des amis
JOIN PersonRelationShipCollapsed AS pff ON pff.PersonAID = pf.PersonBID
ET pff.PersonBID = pl.PersonBID
ET pff.Relation = 'Friend'
--Pas encore amis
LEFT JOIN PersonRelationShipCollapsed AS pnf ON pnf.PersonAID = p.PersonID
ET pnf.PersonBID = pff.PersonBID
ET pnf.Relation = 'Friend'
OÙ pnf.[PersonAID] EST NULL
Conclusion
Il convient de noter que la syntaxe du langage proposée n'est qu'une des nombreuses façons de mettre en œuvre le concept évoqué. La base a été prise dans SQL, et l'objectif était qu'elle ressemble le plus possible à cela. Bien sûr, certains peuvent ne pas aimer les noms des mots-clés, la casse des mots, et ainsi de suite. L'essentiel ici est la conception elle-même. Si l'on le souhaite, on peut également créer une syntaxe similaire en C++ ou Python.
Selon moi, le concept de base de données décrit présente les avantages suivants :
- Simplicité. C'est un indicateur relativement subjectif, qui n'est pas évident dans les cas simples. Mais si nous examinons des cas plus complexes (par exemple, des problèmes avec des étoiles), il me semble que rédiger de telles requêtes est considérablement plus facile.
- Encapsulation. Dans certains exemples, j'ai déclaré des fonctions intermédiaires (par exemple, vendu, acheté et ainsi de suite), sur la base desquelles les fonctions suivantes étaient construites. Cela permet, si nécessaire, de modifier la logique de certaines fonctions sans changer la logique des fonctions qui en dépendent. Par exemple, il est possible de faire en sorte que les ventes vendu Ils étaient considérés comme des objets complètement différents, tout en conservant la logique restante. Oui, dans une base de données relationnelle, cela peut être réalisé à l'aide de CREATE VIEW. Mais si toute la logique est écrite de cette façon, elle sera plutôt difficile à lire.
- Absence de rupture sémantique. Cette base de données opère avec des fonctions et des classes (au lieu de tables et de champs). Tout comme dans la programmation classique (si l'on considère qu'une méthode est une fonction avec le premier paramètre étant la classe à laquelle elle se rapporte). Par conséquent, il devrait être beaucoup plus simple de « lier » avec des langages de programmation universels. De plus, ce concept permet de réaliser des fonctions beaucoup plus complexes. Par exemple, on peut intégrer dans la base de données des opérateurs de type :
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Petya' MESSAGE 'Quelque chose Petya vend trop d'un produit en 2019.';
- Héritage et polymorphisme. Dans une base de données fonctionnelle, on peut introduire un héritage multiple via des constructions CLASS ClassP : Class1, Class2 et réaliser un polymorphisme multiple. Comment exactement, je pourrai peut-être l'écrire dans de prochains articles.
Bien que ce ne soit qu'un concept, nous avons déjà une certaine réalisation en Java, qui traduit toute la logique fonctionnelle en logique relationnelle. En plus, une belle logique de vues y est intégrée, ainsi que beaucoup d'autres choses, ce qui en fait un véritable . En effet, nous utilisons une base de données relationnelle (actuellement seulement PostgreSQL) comme une "machine virtuelle". Avec cette traduction, il y a parfois des problèmes, car l’optimiseur de requêtes de la base de données relationnelle ne connaît pas certaines statistiques que connaît la base de données fonctionnelle. En théorie, il serait possible de réaliser un système de gestion de bases de données qui utiliserait une certaine structure, adaptée précisément à la logique fonctionnelle.
Source : habr.com
