Funktionales DBMS

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 Artikel auf Habr.

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 Artikel. Es enthält eine detaillierte Analyse, wie diese Aufgabe in MS SQL umgesetzt werden kann.

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

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 Plattform. Im Wesentlichen verwenden wir ein RDBMS (bisher nur PostgreSQL) als „virtuelle Maschine“. Diese Übersetzung verursacht manchmal Probleme, da der RDBMS-Abfrageoptimierer bestimmte Statistiken nicht kennt, die das FDBMS kennt. Theoretisch ist es möglich, ein Datenbankverwaltungssystem zu implementieren, das eine bestimmte Struktur als Speicher verwendet, die speziell an die Funktionslogik angepasst ist.

Source: habr.com

Kommentar hinzufügen