Die Datenbankwelt wird seit langem von relationalen DBMS übernommen, die die SQL-Sprache verwenden. So sehr, dass aufkommende Varianten NoSQL genannt werden. Sie haben es geschafft, sich auf diesem Markt einen gewissen Platz zu sichern, aber relationale DBMS werden nicht aussterben und weiterhin aktiv für ihre eigenen Zwecke genutzt.
In diesem Artikel möchte ich das Konzept einer funktionalen Datenbank beschreiben. Zum besseren Verständnis werde ich dies durch einen Vergleich mit dem klassischen relationalen Modell tun. Als Beispiele werden Aufgaben aus verschiedenen im Internet gefundenen SQL-Tests herangezogen.
Einführung
Relationale Datenbanken arbeiten mit Tabellen und Feldern. In einer funktionalen Datenbank werden stattdessen Klassen bzw. Funktionen verwendet. Ein Feld in einer Tabelle mit N Schlüsseln wird als Funktion von N Parametern dargestellt. Anstelle von Verknüpfungen zwischen Tabellen werden Funktionen verwendet, die Objekte der Klasse zurückgeben, zu der die Verknüpfung führt. Anstelle von JOIN wird die Funktionskomposition verwendet.
Bevor ich direkt zu den Aufgaben übergehe, beschreibe ich die Aufgabe der Domänenlogik. Für DDL verwende ich die PostgreSQL-Syntax. Für funktionale eine eigene Syntax.
Tabellen und Felder
Ein einfaches Sku-Objekt mit Namens- und Preisfeldern:
relational
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
Funktional
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
Wir geben zwei bekannt Funktionen, die einen Sku-Parameter als Eingabe verwenden und einen primitiven Typ zurückgeben.
Es wird davon ausgegangen, dass in einem funktionierenden DBMS jedes Objekt über einen internen Code verfügt, der automatisch generiert wird und auf den bei Bedarf zugegriffen werden kann.
Lassen Sie uns den Preis für das Produkt / Geschäft / Lieferanten festlegen. Es kann sich im Laufe der Zeit ändern, also fügen wir der Tabelle ein Zeitfeld hinzu. Ich werde die Deklaration von Tabellen für Verzeichnisse in einer relationalen Datenbank überspringen, um den Code zu kürzen:
relational
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)
)
Funktional
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
Indizes
Für das letzte Beispiel erstellen wir einen Index aller Schlüssel und Datumsangaben, damit wir den Preis für eine bestimmte Zeit schnell finden können.
relational
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Funktional
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
Aufgaben
Beginnen wir mit relativ einfachen Problemen aus dem entsprechenden
Lassen Sie uns zunächst die Domänenlogik deklarieren (für eine relationale Datenbank erfolgt dies direkt im obigen Artikel).
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);
Aufgabe 1.1
Zeigen Sie eine Liste der Mitarbeiter an, deren Löhne höher sind als die des unmittelbaren Vorgesetzten.
relational
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Funktional
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
Aufgabe 1.2
Zeigen Sie eine Liste der Mitarbeiter an, die in ihrer Abteilung das höchste Gehalt verdienen
relational
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
Funktional
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));
Beide Implementierungen sind gleichwertig. Für den ersten Fall in der relationalen Datenbank können Sie CREATE VIEW verwenden, das auf die gleiche Weise zunächst das Höchstgehalt für eine bestimmte Abteilung darin berechnet. Der Übersichtlichkeit halber werde ich in Zukunft den ersten Fall verwenden, da er die Lösung besser widerspiegelt.
Aufgabe 1.3
Zeigen Sie eine Liste der Abteilungs-IDs an, deren Mitarbeiterzahl 3 Personen nicht überschreitet.
relational
select department_id
from employee
group by department_id
having count(*) <= 3
Funktional
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
Aufgabe 1.4
Zeigen Sie eine Liste der Mitarbeiter an, denen kein Vorgesetzter in derselben Abteilung zugewiesen ist.
relational
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
Funktional
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
Aufgabe 1.5
Finden Sie die Liste der Abteilungs-IDs mit dem maximalen Gesamtgehalt des Mitarbeiters.
relational
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 )
Funktional
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();
Kommen wir zu komplexeren Aufgaben von einem anderen
Aufgabe 2.1
Welche Verkäufer verkauften 1997 mehr als 30 Stück von Artikel Nr. 1?
Domänenlogik (wie zuvor überspringen wir die Deklaration im RDBMS):
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);
relational
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
Funktional
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;
Aufgabe 2.2
Finden Sie für jeden Kunden (Vorname, Nachname) die beiden Artikel (Name), für die der Kunde im Jahr 1997 das meiste Geld ausgegeben hat.
Erweiterung der Domänenlogik aus dem vorherigen Beispiel:
CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
relational
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
Funktional
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;
Der PARTITION-Operator funktioniert nach folgendem Prinzip: Er summiert den hinter SUM angegebenen Ausdruck (hier 1) innerhalb der angegebenen Gruppen (hier Kunde und Jahr, kann aber ein beliebiger Ausdruck sein) und sortiert innerhalb der Gruppen nach den in ORDER angegebenen Ausdrücken ( hier gekauft, und wenn gleich, dann durch den internen Produktcode).
Aufgabe 2.3
Wie viele Waren müssen bei Lieferanten bestellt werden, um aktuelle Bestellungen zu erfüllen?
Erweitern wir die Domänenlogik noch einmal:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
relational
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
Funktional
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;
Aufgabe mit einem Sternchen
Und das letzte Beispiel stammt von mir persönlich. Es gibt die Logik eines sozialen Netzwerks. Menschen können miteinander befreundet sein und sich mögen. Aus Sicht einer funktionalen Datenbank würde dies folgendermaßen aussehen:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
Es gilt, mögliche Kandidaten für eine Freundschaft zu finden. Formaler müssen Sie alle Personen A, B, C finden, sodass A mit B befreundet ist und B mit C befreundet ist, A C mag, A aber nicht mit C befreundet ist.
Aus funktionaler Datenbanksicht würde die Abfrage so aussehen:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
Der Leser ist eingeladen, dieses Problem selbstständig in SQL zu lösen. Es wird davon ausgegangen, dass es weitaus weniger Freunde gibt als diejenigen, die mögen. Daher befinden sie sich in separaten Tabellen. Bei erfolgreicher Lösung liegt auch ein Problem mit zwei Sternchen vor. Ihre Freundschaft ist nicht symmetrisch. Auf einer funktionsfähigen Datenbank würde es so aussehen:
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: Lösung des Problems mit dem ersten und zweiten Sternchen ab
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
Abschluss
Es ist zu beachten, dass die obige Syntax der Sprache nur eine der Möglichkeiten zur Umsetzung des oben genannten Konzepts ist. Als Grundlage diente SQL, mit dem Ziel, es diesem so ähnlich wie möglich zu machen. Natürlich kann es sein, dass jemandem die Namen von Schlüsselwörtern, die Groß- und Kleinschreibung von Wörtern usw. nicht gefallen. Hier kommt es vor allem auf das Konzept selbst an. Bei Bedarf können Sie eine ähnliche Syntax für C++ und Python erstellen.
Das beschriebene Datenbankkonzept hat meiner Meinung nach folgende Vorteile:
- Erleichtern. Dies ist ein relativ subjektiver Indikator, der in einfachen Fällen nicht offensichtlich ist. Wenn Sie sich jedoch komplexere Fälle ansehen (z. B. Aufgaben mit Sternchen), ist das Schreiben solcher Abfragen meiner Meinung nach viel einfacher.
- Инкапсуляция. In einigen Beispielen habe ich Zwischenfunktionen deklariert (z. B. verkauft, gekauft usw.), aus denen spätere Funktionen aufgebaut wurden. Dadurch können Sie bei Bedarf die Logik bestimmter Funktionen ändern, ohne die Logik der davon abhängigen Funktionen zu ändern. Sie können zum Beispiel Verkäufe tätigen verkauft wurden aus völlig unterschiedlichen Objekten berechnet, während sich die restliche Logik nicht ändern wird. Ja, in RDBMS kann dies mit CREATE VIEW erfolgen. Wenn Sie jedoch die gesamte Logik auf diese Weise schreiben, sieht sie nicht sehr lesbar aus.
- Keine semantische Lücke. Eine solche Datenbank arbeitet mit Funktionen und Klassen (anstelle von Tabellen und Feldern). Genauso wie in der klassischen Programmierung (vorausgesetzt, eine Methode ist eine Funktion mit dem ersten Parameter in Form einer Klasse, zu der sie gehört). Dementsprechend sollte es viel einfacher sein, sich mit universellen Programmiersprachen „anzufreunden“. Darüber hinaus ermöglicht dieses Konzept die Umsetzung wesentlich komplexerer Funktionen. Sie können beispielsweise Anweisungen wie diese in die Datenbank einbetten:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Vererbung und Polymorphismus. In einer funktionalen Datenbank können Sie Mehrfachvererbung durch die Konstrukte CLASS ClassP: Class1, Class2 einführen und Mehrfachpolymorphismus implementieren. Wie genau, werde ich vielleicht in den folgenden Artikeln schreiben.
Auch wenn dies nur ein Konzept ist, haben wir bereits eine Implementierung in Java, die die gesamte Funktionslogik in relationale Logik übersetzt. Außerdem sind die Logik der Darstellungen und viele andere Dinge wunderbar damit verschraubt, wodurch wir ein Ganzes erhalten
Source: habr.com