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
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
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
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
Source: habr.com