Funktionale DBMS

Die Welt der Datenbanken wird seit langem von relationalen DBMS beherrscht, die die SQL-Sprache verwenden. So stark, dass die neu auftretenden Varianten als NoSQL bezeichnet werden. Es ist ihnen gelungen, sich einen bestimmten Platz auf diesem Markt zu erkämpfen, aber relationale DBMS haben nicht vor zu verschwinden und werden weiterhin aktiv für ihre Zwecke genutzt.

In diesem Artikel möchte ich das Konzept der funktionalen Datenbank beschreiben. Um das Verständnis zu erleichtern, werde ich dies durch den Vergleich mit dem klassischen relationalen Modell tun. Beispiele stammen aus verschiedenen SQL-Tests, die im Internet gefunden wurden.

Einführung

Relationale Datenbanken arbeiten mit Tabellen und Feldern. In einer funktionalen Datenbank werden stattdessen Klassen und Funktionen verwendet. Ein Feld in einer Tabelle mit N Schlüsseln wird als Funktion von N Parametern dargestellt. Anstelle von Beziehungen zwischen Tabellen werden Funktionen verwendet, die Objekte der Klasse zurückgeben, auf die die Beziehung verweist. Anstelle von JOIN wird die Kombination von Funktionen verwendet.

Bevor wir direkt zu den Aufgaben übergehen, werde ich die Aufgabe der Domänenlogik beschreiben. Für DDL werde ich die Syntax von PostgreSQL verwenden. Für funktionale Datenbanken meine eigene Syntax.

Tabellen und Felder

Ein einfaches Objekt Sku mit den Feldern Name 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

CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[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 einem funktionalen DBMS einen internen Code hat, der automatisch generiert wird und auf den bei Bedarf zugegriffen werden kann.

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

CLASS Sku;
KLASSE Geschäft;
KLASSE Lieferant;
datumUhrzeit = DATEN DATETIME (Sku, Geschäft, Lieferant);
preis = DATEN NUMERISCH[10,5] (Sku, Geschäft, Lieferant);

Indizes

Für das letzte Beispiel erstellen wir einen Index über alle Schlüssel und das Datum, damit wir den Preis zu einem bestimmten Zeitpunkt 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

Wir beginnen mit relativ einfachen Aufgaben, die aus dem entsprechenden des Artikels auf Habré.

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

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

KLASSE Employee;
department = DATEN Department (Employee);
chief = DATEN Employee (Employee);
name = DATEN STRING[100] (Employee);
salary = DATEN NUMERIC[14,2] (Employee);

Aufgabe 1.1

Geben Sie eine Liste von Mitarbeitern aus, die ein Gehalt erhalten, das höher ist als das ihres direkten 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

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

Relational

select a.*
from employee a
where a.salary = ( select max(salary) from employee b
                    where b.department_id = a.department_id )

Funktional

maxSalary 'Maximalgehalt' (Abteilung s) = 
    GROUP MAX Gehalt (Mitarbeiter e) WENN abteilung(e) = s;
SELECT name(Mitarbeiter a) WOHER salary(a) = maxSalary(abteilung(a));

// или если "заинлайнить"
SELECT name(Employee a) WHERE 
    salary(a) = maxSalary(GRUPPE MAX salary(Employee e) WENN department(e) = department(a));

Beide Implementierungen sind äquivalent. Im ersten Fall kann man in einer relationalen Datenbank CREATE VIEW verwenden, das ebenso zuerst das maximale Gehalt für eine bestimmte Abteilung berechnet. Im Folgenden werde ich zur Veranschaulichung den ersten Fall verwenden, da er die Lösung besser widerspiegelt.

Aufgabe 1.3

Geben Sie eine Liste von Abteilungs-IDs aus, deren Anzahl an Mitarbeitern nicht mehr als 3 Personen beträgt.

Relational

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

Funktional

Anzahl der Mitarbeiter 'Anzahl der Mitarbeiter' (Abteilung d) = 
    GRUPPE SUMME 1 WENN abteilung(Mitarbeiter e) = d;
SELECT Abteilung d WOHER anzahlDerMitarbeiter(d) <= 3;

Aufgabe 1.4

Geben Sie eine Liste von Mitarbeitern 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ÄHLE name(Mitarbeiter a) WOHER NICHT (abteilung(oberster(a)) = abteilung(a));

Aufgabe 1.5

Finden Sie eine Liste von Abteilungs-IDs mit dem höchsten Gesamtsaldo der Gehälter 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

Gehaltssumme 'Maximales Gehalt' (Abteilung d) = 
    GRUPPE SUMME Gehalt (Mitarbeiter e), WENN abteilung(e) = d;
maxGehaltssumme 'Maximales Gehalt der Abteilungen' () = 
    GRUPPE MAX Gehaltssumme (Abteilung d);
SELECT Abteilung d WHERE gehaltssumme(d) = maxGehaltssumme();

Lassen Sie uns zu komplexeren Aufgaben aus einem anderen Bereich übergehen. des Artikels. Darin ist eine detaillierte Analyse enthalten, wie man diese Aufgabe in MS SQL implementiert.

Aufgabe 2.1

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

Die Domänenlogik (wie zuvor bei RSDB, das deklarative Element bleibt aus):

KLASSE Employee 'Verkäufer';
Nachname 'Familienname' = DATEN STRING[100] (Employee);

KLASSE Product 'Produkt';
id = DATEN INTEGER (Product);
name = DATEN STRING[100] (Product);

KLASSE Order 'Bestellung';
date = DATEN DATE (Order);
employee = DATEN Employee (Order);

KLASSE Detail 'Bestellzeile';

order = DATEN Order (Detail);
product = DATEN Product (Detail);
quantity = DATEN 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 SUMMEN menge(OrderDetail d) WENN 
        mitarbeiter(order(d)) = e UND 
        id(product(d)) = produktId UND 
        extrahiereJahr(datum(order(d))) = jahr;
WÄHLE nachname(Mitarbeiter e) WO verkauft(e, 1, 1997) > 30;

Aufgabe 2.2

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

Erweitern wir die Geschäftslogik aus dem vorherigen Beispiel:

CLASS Customer 'Kunde';
contactName 'Vollständiger 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

Summe (Detail d) = Menge(d) * Einzelpreis(d) * (1 - Rabatt(d));
gekauft 'Gekauft' (Kunde c, Produkt p, GANZZAHL y) = 
    GRUPPE SUMME 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 SUMME 1 ORDNE DESC gekauft(c, p, y), p NACH c, y;
WÄHLE kontaktName(Kunde c), name(Produkt p) WHERE bewertung(c, p, 1997) < 3;

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

Aufgabe 2.3

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

Erneut erweitern wir die Geschäftslogik:

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

supplier = DATA Supplier (Product);

unitsInStock 'Lagerbestand' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Verkaufsschwelle' = 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

'Bestellt, aber nicht versandt' (Produkt p) = 
    GRUPPENSUMME menge(Bestellposition d) WENN produkt(d) = p;
'Zu bestellen' (Produkt p) = orderedNotShipped(p) + reorderLevel(p) - unitsInStock(p);
WÄHLEN Sie companyName(lieferant(Produkt p)), name(p), toOrder(p) WO toOrder(p) > 0;

Sternchen-Aufgabe

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

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

Es müssen potenzielle Freundschaftskandidaten gefunden werden. Formaler ausgedrückt: Es sind alle Personen A, B, C zu finden, sodass A mit B befreundet ist, B mit C befreundet ist, A C gefällt, aber A nicht mit C befreundet ist.
Aus der Sicht einer funktionalen Datenbank würde die Abfrage folgendermaßen aussehen:

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

Der Leser wird aufgefordert, diese Aufgabe selbstständig in SQL zu lösen. Es wird angenommen, dass es deutlich weniger Freunde als Leute gibt, die gefallen. Daher befinden sie sich in separaten Tabellen. Bei erfolgreicher Lösung gibt es auch eine Aufgabe mit zwei Sternchen. In ihr ist die Freundschaft nicht symmetrisch. In einer funktionalen Datenbank würde dies folgendermaßen aussehen:

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

UPD: Lösung der Aufgabe mit einem und zwei Sternchen von dss_kalika:

WÄHLEN 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
VON Personen                 AS p
--Gefällt mir                     
JOIN PersonRelationShip      AS pl ON pl.PersonAID = p.PersonID
                                  UND pl.Relation  = 'Gefällt mir'
--Freunde                     
JOIN PersonRelationShip      AS pf ON pf.PersonAID = p.PersonID 
                                  UND pf.Relation = 'Freund'
--Freunde von Freunden              
JOIN PersonRelationShip      AS pff ON pff.PersonAID = pf.PersonBID
                                   UND pff.PersonBID = pl.PersonBID
                                   UND pff.Relation = 'Freund'
--Noch keine Freunde         
LEFT JOIN PersonRelationShip AS pnf ON pnf.PersonAID = p.PersonID
                                   UND pnf.PersonBID = pff.PersonBID
                                   UND pnf.Relation = 'Freund'
WO pnf.PersonAID IST NULL 

;MIT PersonRelationShipCollapsed ALS (
  WÄHLEN pl.PersonAID
        ,pl.PersonBID
        ,pl.Relation 
  VON #PersonRelationShip      AS pl 
  
  UNION 

  WÄHLEN pl.PersonBID AS PersonAID
        ,pl.PersonAID AS PersonBID
        ,pl.Relation
  VON #PersonRelationShip      AS pl 
)
WÄHLEN 
   pl.PersonAID
  ,pf.PersonBID
  ,pff.PersonBID
VON #Persons                      AS p
--Gefällt mir                      
JOIN PersonRelationShipCollapsed  AS pl ON pl.PersonAID = p.PersonID
                                 UND pl.Relation  = 'Gefällt mir'                                  
--Freunde                          
JOIN PersonRelationShipCollapsed  AS pf ON pf.PersonAID = p.PersonID 
                                 UND pf.Relation = 'Freund'
--Freunde von Freunden                   
JOIN PersonRelationShipCollapsed  AS pff ON pff.PersonAID = pf.PersonBID
                                 UND pff.PersonBID = pl.PersonBID
                                 UND pff.Relation = 'Freund'
--Noch keine Freunde                   
LEFT JOIN PersonRelationShipCollapsed AS pnf ON pnf.PersonAID = p.PersonID
                                   UND pnf.PersonBID = pff.PersonBID
                                   UND pnf.Relation = 'Freund'
WO pnf.[PersonAID] IST NULL 

Fazit

Es sollte erwähnt werden, dass die angegebene Syntax der Sprache nur eine von vielen Möglichkeiten zur Umsetzung des genannten Konzepts ist. SQL wurde als Grundlage gewählt, und das Ziel war es, ihn so nah wie möglich daran zu halten. Natürlich können einige die Benennungen der Schlüsselwörter, die Groß- und Kleinschreibung und andere Aspekte als unpassend empfinden. Hier ist das Wesentliche — das Konzept selbst. Bei Bedarf könnte man eine ähnliche Syntax auch in C++ oder Python erstellen.

Das beschriebene Datenbankkonzept besitzt meiner Meinung nach folgende Vorteile:

  • Einfachheit. Dies ist ein relativ subjektiver Indikator, der bei einfachen Fällen nicht offensichtlich ist. Wenn man jedoch komplexere Fälle betrachtet (zum Beispiel Aufgaben mit Sternen), so ist es meiner Ansicht nach deutlich einfacher, solche Abfragen zu schreiben.
  • Kapselung. In einigen Beispielen habe ich Zwischenschritte in Form von Funktionen deklariert (zum Beispiel, verkauft, gekauft usw.), auf denen die nachfolgenden Funktionen basieren. Dies ermöglicht es, bei Bedarf die Logik bestimmter Funktionen zu ändern, ohne die Logik der abhängigen Funktionen zu verändern. Man könnte beispielsweise die Verkaufsprozesse verkauft Man ging von völlig anderen Objekten aus, aber die restliche Logik bleibt unverändert. Ja, das kann in einem RDBMS mit CREATE VIEW umgesetzt werden. Aber wenn man die gesamte Logik auf diese Weise schreibt, wirkt sie nicht sehr lesbar.
  • Fehlen einer semantischen Trennung. Diese Datenbank arbeitet mit Funktionen und Klassen (statt mit Tabellen und Feldern). Genau wie in der klassischen Programmierung (wenn man davon ausgeht, dass eine Methode eine Funktion mit dem ersten Parameter als Klasse ist, zu der sie gehört). Daher sollte es deutlich einfacher sein, sie mit universellen Programmiersprachen zu verbinden. Darüber hinaus ermöglicht dieses Konzept die Umsetzung viel komplexerer Funktionen. Beispielsweise können Operatoren der Art in die Datenbank eingebaut werden:

    EINSCHRÄNKUNG verkauft(Mitarbeiter e, 1, 2019) > 100 WENN name(e) = 'Petja' NACHRICHT 'Petja verkauft zu viel von einem Produkt im Jahr 2019';

  • Vererbung und Polymorphismus. In einer funktionalen Datenbank kann Mehrfachvererbung durch Konstrukte wie CLASS ClassP: Class1, Class2 eingeführt werden, und polymorphe Mehrfachvererbung kann realisiert werden. Wie genau, werde ich vielleicht in den nächsten Artikeln schreiben.

Obwohl dies nur ein Konzept ist, haben wir bereits eine gewisse Implementierung in Java, die die gesamte funktionale Logik in relationale Logik übersetzt. Dazu kommt eine schön integrierte Logik für Views und vieles mehr, was ein ganzes die Plattform. Im Grunde verwenden wir RDBMS (momentan nur PostgreSQL) als „virtuelle Maschine“. Bei dieser Übersetzung treten manchmal Probleme auf, da der Abfrageoptimierer des RDBMS keine spezifischen Statistiken kennt, die das FDBMS hat. Theoretisch könnte man ein Datenbankmanagementsystem implementieren, das eine Struktur als Speicher verwendet, die speziell an die funktionale Logik angepasst ist.

Quelle: habr.com

Zuverlässiges Hosting für Websites mit DDoS-Schutz kaufen, VPS VDS Server 🔥 Zuverlässiges Hosting für Websites mit DDoS-Schutz kaufen, VPS VDS Server - ProHoster