Funktionale Datenbank

Die Welt der Datenbanken wird seit langem von relationalen Datenbankmanagementsystemen (RDBMS) dominiert, die SQL als Abfragesprache verwenden. So sehr, dass neue Varianten als NoSQL bezeichnet werden. Diese haben sich einen Platz auf dem Markt erkämpft, doch die relationalen Datenbanken sind alles andere als am Ende und werden weiterhin aktiv für ihre Zwecke genutzt.

In diesem Artikel möchte ich das Konzept einer funktionalen Datenbank erläutern. Um dies besser zu verstehen, werde ich es im Vergleich zu dem klassischen relationalen Modell tun. Als Beispiele werden Aufgaben aus verschiedenen SQL-Tests verwendet, die im Internet zu finden sind.

Einführung

Relationale Datenbanken arbeiten mit Tabellen und Feldern. In einer funktionalen Datenbank hingegen werden Klassen und Funktionen zum Einsatz kommen. Ein Feld in einer Tabelle mit N Schlüsseln wird als Funktion von N Parametern dargestellt. Statt Beziehungen zwischen Tabellen werden Funktionen verwendet, die Objekte der betreffenden Klasse zurückgeben. Anstelle von JOIN wird die Funktion der Komposition genutzt.

Bevor ich mich direkt den Aufgaben widme, möchte ich die Logik der Domäne beschreiben. Für die DDL werde ich die PostgreSQL-Syntax verwenden. Für die funktionale werde ich meine eigene Syntax verwenden.

Tabellen und Felder

Ein einfaches Objekt Sku mit den Feldern Bezeichnung und Preis:

Relational

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

Funktional

KLASSEN Sku;
name = DATEN STRING[100] (Sku);
preis = DATEN NUMERISCH[10,5] (Sku);

Wir deklarieren zwei Funktionen, die einen Parameter Sku entgegennehmen und einen primitiven Typ zurückgeben.

Es wird angenommen, dass jedes Objekt in der funktionalen DB ein internes, automatisch generiertes Code hat, auf den bei Bedarf zugegriffen werden kann.

Legen wir den Preis für das Produkt / Geschäft / Lieferanten fest. Dieser kann sich im Laufe der Zeit ändern, daher fügen wir das Zeitfeld in die Tabelle ein. Die Deklaration der Tabellen für Verzeichnisse in der relationalen Datenbank lasse ich weg, um den Code zu verkü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

KLASSEN Sku;
KLASSEN Shop;
KLASSEN Lieferant;
datumUhrzeit = DATEN DATETIME (Sku, Store, Supplier);
preis = DATEN NUMERISCH[10,5] (Sku, Store, Supplier);

Indizes

Für das letzte Beispiel erstellen wir einen Index auf allen Schlüssel- und Datumsfeldern, um die Preise zu einem bestimmten Zeitpunkt schnell zu finden.

Relational

INDEX für das Datum der Preise erstellen
    ON Preise
    (skuId, storeId, supplierId, dateTime)

Funktional

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

Aufgaben

Lassen Sie uns mit relativ einfachen Aufgaben beginnen, die aus dem entsprechenden Kontext entnommen wurden. einem Artikel auf Habr erstellt haben.

Zuerst erklären wir die Domänenlogik (für relationale Datenbanken wird dies direkt im angegebenen Artikel behandelt).

KLASSE Abteilung;
name = DATENSTRING[100] (Abteilung);

KLASSE Mitarbeiter;
Abteilung = DATEN Abteilung (Mitarbeiter);
Vorgesetzter = DATEN Mitarbeiter (Mitarbeiter);
Name = DATEN STRING[100] (Mitarbeiter);
Gehalt = DATEN NUMERISCH[14,2] (Mitarbeiter);

Aufgabe 1.1

Geben Sie eine Liste von Mitarbeitern aus, die ein höheres Gehalt als ihr unmittelbarer Vorgesetzter erhalten.

Relational

SELECT a.*
FROM   mitarbeiter a, mitarbeiter b
WHERE  b.id = a.vorgesetzten_id
AND    a.gehalt > b.gehalt

Funktional

WÄHLEN Sie name(Mitarbeiter a) WO salary(a) > salary(chef(a));

Aufgabe 1.2

Geben Sie eine Liste von Mitarbeitern aus, die in ihrer Abteilung das höchste Gehalt verdienen.

Relational

SELECT a.*
FROM   mitarbeiter a
WHERE  a.gehalt = ( SELECT MAX(gehalt) FROM mitarbeiter b
                    WHERE  b.abteilungs_id = a.abteilungs_id )

Funktional

maxSalary 'Maximale Gehalt' (Abteilung s) = 
    GRUPPE MAX Gehalt (Mitarbeiter e) WENN abteilung(e) = s;
WÄHLEN Sie name(Mitarbeiter a) WO gehalts(a) = maxSalary(abteilung(a));

// или если "заинлайнить"
SELECT name(Mitarbeiter a) WHERE 
    gehalt(a) = maxGehalt(GRUPPIEREN MAX gehalt(Mitarbeiter e) WENN abteilung(e) = abteilung(a));

Beide Implementierungen sind äquivalent. Im ersten Fall kann in einer relationalen Datenbank CREATE VIEW verwendet werden, das ebenfalls zunächst das maximale Gehalt für eine bestimmte Abteilung berechnet. Um es anschaulicher zu machen, werde ich weiterhin den ersten Fall verwenden, da er die Lösung besser widerspiegelt.

Aufgabe 1.3

Geben Sie eine Liste der Abteilungs-IDs aus, in denen die Anzahl der Mitarbeiter 3 nicht überschreitet.

Relational

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

Funktional

countEmployees 'Anzahl der Mitarbeiter' (Abteilung d) = 
    GRUPPE SUMME 1 WENN Abteilung(Mitarbeiter e) = d;
SELECT Abteilung d WHERE countEmployees(d) <= 3;

Aufgabe 1.4

Geben Sie eine Liste der Mitarbeiter aus, die keinen zugewiesenen Vorgesetzten haben, der in derselben Abteilung arbeitet.

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

WÄHLEN Sie name(Mitarbeiter a) WOHER NICHT (abteilung(chef(a)) = abteilung(a));

Aufgabe 1.5

Finden Sie eine Liste der Abteilungs-IDs mit dem höchsten Gesamtgehalt der Mitarbeiter.

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 'Maximale Gehalt' (Abteilung d) = 
    GRUPPE SUMME gehalt(Mitarbeiter e) WENN abteilung(e) = d;
maxSalarySum 'Maximalgehalt der Abteilungen' () = 
    GROUP MAX salarySum(Abteilung d);
SELECT Abteilung d WHERE salarySum(d) = maxSalarySum();

Kommen wir zu schwierigeren Aufgaben aus einem anderen Bereich einem Artikel. In diesem Abschnitt finden Sie eine detaillierte Analyse, wie man diese Aufgabe in MS SQL umsetzt.

Aufgabe 2.1

Welche Verkäufer haben im Jahr 1997 mehr als 30 Stück des Produkts Nr. 1 verkauft?

Datenmodellierung (wie zuvor bei RSDB, wir überspringen die Deklaration):

KLASSE Mitarbeiter 'Verkäufer';
nachname 'Nachname' = DATEN STRING[100] (Mitarbeiter);

CLASS Product 'Produkt';
id = DATA INTEGER (Produkt);
name = DATA STRING[100] (Produkt);

CLASS Order 'Bestellung';
date = DATA DATE (Bestellung);
employee = DATA Employee (Bestellung);

CLASS Detail 'Bestellposition';

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

verkauft (Mitarbeiter e, INTEGER produktId, INTEGER jahr) = 
    GRUPPE SUMME menge(Bestelldetail d) WENN 
        Mitarbeiter(order(d)) = e UND 
        id(product(d)) = produktId UND 
        jahr(extrahiereDatum(date(order(d)))) = jahr;
WÄHLE nachname(Mitarbeiter e) WO verkauft(e, 1, 1997) > 30;

Aufgabe 2.2

Für jeden Kunden (Vorname, Nachname) zwei Produkte (Bezeichnung) finden, auf die der Kunde im Jahr 1997 das meiste Geld ausgegeben hat.

Wir erweitern die Domain-Logik aus dem vorherigen Beispiel:

CLASS Customer 'Kunde';
contactName 'Name' = 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) = Menge(d) * Einheitenpreis(d) * (1 - Rabatt(d));
gekauft 'Kauft' (Kunde c, Produkt p, GANZZAHL y) = 
    GRUPPENSUMME sum(Detail d) WENN 
        kunde(bestellung(d)) = c UND 
        produkt(d) = p UND 
        extrahiereJahr(datum(bestellung(d))) = y;
bewertung 'Bewertung' (Kunde c, Produkt p, GANZZAHL y) = 
    PARTITION SUM 1 ORDER DESC gekauft(c, p, y), p BY c, y;
WÄHLEN contactName(Kunde c), name(Produkt p) WO bewertung(c, p, 1997) < 3;

Der PARTITION-Operator funktioniert nach folgendem Prinzip: Er summiert den angegebenen Ausdruck nach SUM (hier 1) innerhalb der angegebenen Gruppen (hier Kunde und Jahr, aber es kann jeder Ausdruck sein), sortiert innerhalb der Gruppen nach den in ORDER angegebenen Ausdrücken (hier gekauft, und wenn gleich, nach dem internen Produktcode).

Aufgabe 2.3

Wie viele Produkte müssen bei den Lieferanten bestellt werden, um die aktuellen Bestellungen zu erfüllen?

Wir erweitern erneut die Domain-Logik:

KLASS Lieferant 'Lieferant';
companyName = DATEN STRING[100] (Lieferant);

supplier = DATA Supplier (Product);

unitsInStock 'Lagerbestand' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Verkaufsnorm' = DATA NUMERIC[10,3] (Product);

Relational

Wählen Sie s.CompanyName, p.ProductName, sum(od.Quantity) + p.ReorderLevel — p.UnitsInStock als ToOrder
von Orders o
verbinden [Order Details] od, auf o.OrderID = od.OrderID
verbinden Products p, auf od.ProductID = p.ProductID
verbinden Suppliers s, auf p.SupplierID = s.SupplierID
wo o.ShippedDate ist null
gruppe nach s.CompanyName, p.ProductName, p.UnitsInStock, p.ReorderLevel
habe p.UnitsInStock < sum(od.Quantity) + p.ReorderLevel

Funktional

bestellt, aber nicht versandt (Produkt p) = 
    GRUPPENSUMME Menge(OrderDetail d) WENN produkt(d) = p;
zuBestellen 'Zu bestellen' (Produkt p) = bestelltNichtVersandt(p) + nachbestellMenge(p) - einheitenInLager(p);
WÄHLE unternehmensName(lieferant(Produkt p)), name(p), zuBestellen(p) WO zuBestellen(p) > 0;

Aufgabe mit Sternchen

Und das letzte Beispiel persönlich von mir. Es gibt die Logik eines sozialen Netzwerks. Menschen können miteinander befreundet sein und sich gegenseitig gefallen. Aus Sicht einer funktionalen Datenbank würde das folgendermaßen aussehen:

KLASSE Person;
likes = DATEN BOOLEAN (Person, Person);
friends = DATEN BOOLEAN (Person, Person);

Es ist notwendig, mögliche Kandidaten für Freundschaften zu finden. Formaler muss man alle Personen A, B und C finden, so dass A mit B befreundet ist, B mit C befreundet ist, A C mag, aber A nicht mit C befreundet ist.
Aus Sicht einer funktionalen Datenbank würde die Abfrage folgendermaßen aussehen:

WÄHLE Person a, Person b, Person c WOHER 
    mag(a, c) UND NICHT freund(a, c) UND 
    freund(a, b) UND freund(b, c);

Der Leser wird eingeladen, diese Aufgabe in SQL selbst zu lösen. Es wird angenommen, dass es viel weniger Freunde als Personen gibt, die einem gefallen. Daher sind sie in separaten Tabellen. Bei erfolgreicher Lösung gibt es auch eine Aufgabe mit zwei Sternchen. Hier ist die Freundschaft nicht symmetrisch. Aus Sicht einer funktionalen Datenbank würde das folgendermaßen aussehen:

WÄHLE Person a, Person b, Person c WOHER 
    mag(a, c) UND NICHT freund(a, c) UND 
    (freunde(a, b) ODER freunde(b, a) ) UND 
    (freunde(b, c) ODER freunde(c, b));

UPD: Lösung für die Aufgaben mit einem und zwei Sternchen von dss_kalika:

SELECT 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
FROM Persons                 AS p
--Gefällt mir                      
JOIN PersonRelationShip      AS pl ON pl.PersonAID = p.PersonID
                                  AND pl.Relation  = 'Gefällt mir'
--Freunde                     
JOIN PersonRelationShip      AS pf ON pf.PersonAID = p.PersonID 
                                  AND pf.Relation = 'Freund'
--Freunde von Freunden              
JOIN PersonRelationShip      AS pff ON pff.PersonAID = pf.PersonBID
                                   AND pff.PersonBID = pl.PersonBID
                                   AND pff.Relation = 'Freund'
--Noch keine Freunde         
LEFT JOIN PersonRelationShip AS pnf ON pnf.PersonAID = p.PersonID
                                   AND pnf.PersonBID = pff.PersonBID
                                   AND pnf.Relation = 'Freund'
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
--Likes                      
JOIN PersonRelationShipCollapsed  AS pl ON pl.PersonAID = p.PersonID
                                 AND pl.Relation  = 'Like'                                  
--Friends                          
JOIN PersonRelationShipCollapsed  AS pf ON pf.PersonAID = p.PersonID 
                                 AND pf.Relation = 'Friend'
--Friends of Friends                   
JOIN PersonRelationShipCollapsed  AS pff ON pff.PersonAID = pf.PersonBID
                                 AND pff.PersonBID = pl.PersonBID
                                 AND pff.Relation = 'Friend'
--Not yet friends                   
LEFT JOIN PersonRelationShipCollapsed AS pnf ON pnf.PersonAID = p.PersonID
                                   AND pnf.PersonBID = pff.PersonBID
                                   AND pnf.Relation = 'Friend'
WHERE pnf.[PersonAID] IS NULL 

Fazit

Es ist zu beachten, dass die angegebene Syntax lediglich eine von vielen Möglichkeiten ist, das dargestellte Konzept umzusetzen. Grundlage war SQL, und das Ziel war es, die maximale Ähnlichkeit zu bewahren. Natürlich könnten einigen die Bezeichnungen der Schlüsselwörter, die Schreibweise und ähnliches nicht gefallen. Hier steht jedoch das Konzept im Vordergrund. Bei Bedarf könnte auch eine Syntax ähnlich C++ oder Python erstellt werden.

Das beschriebene Konzept der Datenbank bietet meiner Meinung nach folgende Vorteile:

  • Einfachheit. Dies ist ein relativ subjektiver Indikator, der in einfachen Fällen nicht offensichtlich ist. Wenn man jedoch komplexere Fälle betrachtet (zum Beispiel Aufgaben mit Sternchen), wird meiner Ansicht nach deutlich, dass es wesentlich einfacher ist, solche Abfragen zu schreiben.
  • Kapselung. In einigen Beispielen habe ich Zwischenfunktionen deklariert (zum Beispiel, verkauft, gekauft usw.), von denen aus die folgenden Funktionen aufbauten. Das ermöglicht es, die Logik bestimmter Funktionen bei Bedarf zu ändern, ohne die Logik abhängiger Funktionen zu verändern. Zum Beispiel könnte man es so einrichten, dass Verkäufe verkauft von ganz anderen Objekten gerechnet werden, während die restliche Logik unverändert bleibt. Ja, in einer RDBMS kann man dies mit CREATE VIEW umsetzen. Wenn man jedoch die gesamte Logik auf diese Weise schreibt, wird sie nicht sehr leserlich sein.
  • Fehlen der semantischen Lücke. Eine solche Datenbank arbeitet mit Funktionen und Klassen (anstatt mit Tabellen und Feldern). Ähnlich wie in der klassischen Programmierung (wenn man annimmt, dass eine Methode eine Funktion mit dem ersten Parameter in Form der Klasse ist, zu der sie gehört). Entsprechend sollte es deutlich einfacher sein, diese mit universellen Programmiersprachen zu verbinden. Darüber hinaus ermöglicht dieses Konzept die Implementierung viel komplexerer Funktionen. Zum Beispiel können in die Datenbank Operatoren eingefügt werden wie:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Petya' MESSAGE 'Petya verkauft im Jahr 2019 zu viele von einem Produkt';

  • Vererbung und Polymorphismus. In einer funktionalen Datenbank kann durch Konstruktionen wie CLASS ClassP: Class1, Class2 eine Mehrfachvererbung eingeführt und polymorphe Mehrfachvererbung realisiert werden. Wie genau, werde ich eventuell in zukünftigen Artikeln schreiben.

Obwohl es sich lediglich um ein Konzept handelt, haben wir bereits eine Implementierung in Java, die die gesamte funktionale Logik in relationale Logik überträgt. Zudem wurde elegant die Logik der Präsentationen und vieles mehr integriert, was zu einem vollständigen Plattform. Im Grunde verwenden wir RDBMS (derzeit nur PostgreSQL) als „virtuelle Maschine“. Bei dieser Übertragung treten manchmal Probleme auf, da der Abfrageoptimierer des RDBMS bestimmte Statistiken nicht kennt, die das DBMS (Datenbankmanagementsystem) jedoch kennt. Theoretisch lässt sich ein Datenbankverwaltungssystem implementieren, das als Speicher eine Struktur nutzt, die speziell auf die funktionale Logik abgestimmt ist.

Quelle: habr.com

Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server 🔥 Kaufen Sie zuverlässiges Hosting für Websites mit DDoS-Schutz, VPS VDS-Server | ProHoster