Functionele DBMS

De wereld van databases is al lang gedomineerd door relationele DBMS die SQL gebruiken. Zozeer zelfs dat de nieuwere varianten NoSQL worden genoemd. Ze hebben een zekere plaats op deze markt weten te veroveren, maar relationele DBMS zijn nog lang niet dood en worden nog steeds actief voor hun doeleinden gebruikt.

In dit artikel wil ik het concept van een functionele database beschrijven. Voor een beter begrip zal ik dit doen door een vergelijking te maken met het klassieke relationele model. Voorbeelden zullen taken zijn uit verschillende SQL-tests die online zijn te vinden.

Inleiding

Relationele databases werken met tabellen en velden. In een functionele database zullen in plaats daarvan klassen en functies worden gebruikt. Een veld in een tabel met N sleutels zal worden weergegeven als een functie met N parameters. In plaats van relaties tussen tabellen zullen functies worden gebruikt die objecten van de verwante klasse retourneren. In plaats van JOIN zal functiecompositie worden gebruikt.

Voordat ik direct naar de taken ga, beschrijf ik de domeinlogica. Voor DDL zal ik de syntaxis van PostgreSQL gebruiken. Voor functioneel gebruik ik mijn eigen syntaxis.

Tabellen en velden

Een eenvoudig object Sku met de velden naam en prijs:

Relationeel

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

Functioneel

CLASS Sku;
naam = DATA STRING[100] (Sku);
prijs = DATA NUMERIC[10,5] (Sku);

We declareren twee functies, die één parameter Sku accepteren en een primitief type retourneren.

Het wordt verondersteld dat elke object in een functionele DBMS een interne code heeft die automatisch wordt gegenereerd en waartoe men indien nodig kan verwijzen.

We stellen de prijs in voor een product / winkel / leverancier. Deze kan in de loop van de tijd veranderen, dus we voegen een veld voor tijd aan de tabel toe. De declaratie van tabellen voor verwijzingen in de relationele database laat ik achterwege om de code te verkorten:

Relationeel

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

Functioneel

CLASS Sku;
KLASSE Winkel;
KLASSE Leverancier;
datumTijd = GEGEVENS DATUMTIJD (Sku, Winkel, Leverancier);
prijs = GEGEVENS NUMERIEK[10,5] (Sku, Winkel, Leverancier);

Indexen

Voor het laatste voorbeeld bouwen we een index op alle sleutels en de datum, zodat we snel de prijs op een bepaald tijdstip kunnen vinden.

Relationeel

CREATE INDEX prices_date
    ON prices
    (skuId, storeId, supplierId, dateTime)

Functioneel

INDEX Sku sk, Winkel st, Leverancier sp, dateTime(sk, st, sp);

Taken

Laten we beginnen met relatief eenvoudige taken, gehaald uit het relevante artikel op Habr.

Laten we eerst de domeinlogica definiëren (voor relationele databases is dit direct in het artikel gedaan).

KLAS Departement;
naam = GEGEVENS STRING[100] (Departement);

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

Taak 1.1

Geef een lijst weer van medewerkers die een salaris ontvangen dat hoger is dan dat van hun directe leidinggevende.

Relationeel

select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary

Functioneel

SELECT name(Employee a) WHERE salary(a) > salary(chief(a));

Taak 1.2

Geef een lijst weer van medewerkers die het hoogste salaris in hun afdeling ontvangen.

Relationeel

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

Functioneel

maxSalary 'Maximaal salaris' (Afdeling s) = 
    GROUP MAX salaris (Werknemer e) IF afdeling(e) = s;
SELECT naam (Werknemer a) WHERE salaris(a) = maxSalary(afdeling(a));

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

Beide implementaties zijn gelijkwaardig. In het eerste geval kan in een relationele database CREATE VIEW worden gebruikt, dat op dezelfde wijze eerst het maximale salaris voor een specifieke afdeling berekent. Voor de duidelijkheid zal ik in het vervolg het eerste geval gebruiken, omdat dit de oplossing beter weerspiegelt.

Taak 1.3

Geef een lijst weer van afdelings-ID's waarbij het aantal medewerkers niet meer dan 3 is.

Relationeel

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

Functioneel

aantalWerknemers 'Aantal werknemers' (Afdeling d) = 
    GROEP SOM 1 ALS afdeling(Werknemer e) = d;
SELECTEER Afdeling d WAAR aantalWerknemers(d) <= 3;

Taak 1.4

Geef een lijst weer van medewerkers zonder aangewezen leidinggevende die zich in dezelfde afdeling bevinden.

Relationeel

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

Functioneel

SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));

Taak 1.5

Vind een lijst van afdelings-ID's met het hoogste totale salaris van medewerkers.

Relationeel

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 )

Functioneel

salarisSom 'Maximale salaris' (Afdeling d) = 
    GROEP SOM salaris(werknemer e) ALS afdeling(e) = d;
maxSalarisSom 'Maximale salarissen van afdelingen' () = 
    GROEP MAX salarisSom(Afdeling d);
SELECT Afdeling d WAAR salarisSom(d) = maxSalarisSom();

Laten we overgaan naar meer complexe taken uit een andere artikel. Hier is een gedetailleerde uitleg over hoe deze taak op MS SQL kan worden geïmplementeerd.

Taak 2.1

Welke verkopers hebben in 1997 meer dan 30 stuks productnummer 1 verkocht?

Domeinlogica (zoals eerder, in RSUBD overslaan we de verklaring):

CLASS Employee 'Verkoper';
lastName 'Achternaam' = DATA STRING[100] (Employee);

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

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

CLASS Detail 'Bestellijn';

order = DATA Order (Detail);
product = DATA Product (Detail);
quantity = DATA NUMERIC[10,5] (Detail);

Relationeel

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

Functioneel

verkocht (Medewerker e, INTEGER productId, INTEGER jaar) = 
    GROEP SOM hoeveelheid(OrderDetail d) ALS 
        medewerker(order(d)) = e EN 
        id(product(d)) = productId EN 
        extractYear(date(order(d))) = jaar;
SELECT lastName(Medewerker e) WAAR verkocht(e, 1, 1997) > 30;

Taak 2.2

Voor elke klant (voornaam, achternaam) de twee producten (namen) vinden waarop de klant in 1997 het meeste geld heeft uitgegeven.

We breiden de domeinlogica uit met het vorige voorbeeld:

CLASS Klant 'Klant';
contactName 'Volledige naam' = DATA STRING[100] (Klant);

klant = DATA Klant (Bestelling);

eenheidsprijs = DATA NUMERIC[14,2] (Detail);
korting = DATA NUMERIC[6,2] (Detail);

Relationeel

SELECT ContactNaam, ProductNaam FROM (
SELECT c.ContactNaam, p.ProductNaam
, ROW_NUMBER() OVER (
    PARTITION BY c.ContactNaam
    ORDER BY SUM(od.Aantal * od.Eenheidsprijs * (1 - od.Korting)) DESC
) AS RatingByAmt
FROM Klanten c
JOIN Bestellingen o ON o.KlantID = c.KlantID
JOIN [Bestelling Details] od ON od.BestellingID = o.BestellingID
JOIN Producten p ON p.ProductID = od.ProductID
WHERE YEAR(o.BestelDatum) = 1997
GROUP BY c.ContactNaam, p.ProductNaam
) t
WHERE RatingByAmt < 3

Functioneel

som (Detail d) = hoeveelheid(d) * eenheidsprijs(d) * (1 - korting(d));
gekocht 'Gekocht' (Klant c, Product p, INTEGER y) = 
    GROEP SOM som(Detail d) ALS 
        klant(bestelling(d)) = c EN 
        product(d) = p EN 
        extractYear(datum(bestelling(d))) = y;
beoordeling 'Beoordeling' (Klant c, Product p, INTEGER y) = 
    PARTITION SOM 1 VOLGORDE DESC gekocht(c, p, y), p GEGAAN c, y;
SELECT contactNaam(Klant c), naam(Product p) WAAR beoordeling(c, p, 1997) < 3;

De PARTITION-operator werkt als volgt: hij somt de expressie op die na SUM is opgegeven (hier 1), binnen de opgegeven groepen (hier Klant en Jaar, maar het kan elke expressie zijn), gesorteerd binnen de groepen volgens de expressies die in ORDER zijn aangegeven (hier gekocht, en als deze gelijk zijn, op de interne productcode).

Taak 2.3

Hoeveel artikelen moeten er bij leveranciers worden besteld om aan de huidige bestellingen te voldoen.

We breiden de domeinlogica opnieuw uit:

CLASS Leverancier 'Leverancier';
companyName = DATA STRING[100] (Leverancier);

leverancier = DATA Leverancier (Product);

eenhedenOpVoorraad 'Voorraad' = DATA NUMERIC[10,3] (Product);
herbestelniveau 'Verkoopnorm' = DATA NUMERIC[10,3] (Product);

Relationeel

select s.Bedrijfsnaam, p.ProductNaam, sum(od.Aantal) + p.Herbestelniveau - p.EenhedenOpVoorraad as TeBestellen
from Bestellingen o
join [Bestelling Details] od on o.BestellingID = od.BestellingID
join Producten p on od.ProductID = p.ProductID
join Leveranciers s on p.LeverancierID = s.LeverancierID
where o.VerzendDatum is null
group by s.Bedrijfsnaam, p.ProductNaam, p.EenhedenOpVoorraad, p.Herbestelniveau
having p.EenhedenOpVoorraad < sum(od.Aantal) + p.Herbestelniveau

Functioneel

besteldNietVerzonden 'Besteld, maar niet verzonden' (Product p) = 
    GROUP SUM hoeveelheid(OrderDetail d) IF product(d) = p;
teBestellen 'Te bestellen' (Product p) = besteldNietVerzonden(p) + reorderLevel(p) - unitsInStock(p);
SELECT companyName(supplier(Product p)), name(p), teBestellen(p) WHERE teBestellen(p) > 0;

Taak met een ster

En het laatste voorbeeld is persoonlijk van mij. Er is een sociale netwerklogica. Mensen kunnen vriendschap sluiten en elkaar leuk vinden. Vanuit het oogpunt van de functionele database zou dit er als volgt uitzien:

KLASSE Persoon;
houdt van = GEGEVENS BOOLEAN (Persoon, Persoon);
vrienden = GEGEVENS BOOLEAN (Persoon, Persoon);

Het is nodig om mogelijke kandidaten voor vriendschap te vinden. Formeel moeten we alle mensen A, B, C vinden, zodat A bevriend is met B, B is bevriend met C, A vindt C leuk, maar A is niet bevriend met C.
Vanuit het perspectief van de functionele database zou de query er als volgt uitzien:

SELECT Persoon a, Persoon b, Persoon c WAAR 
    houdt van(a, c) EN NIET vrienden(a, c) EN 
    vrienden(a, b) EN vrienden(b, c);

De lezer wordt voorgesteld om deze taak zelf op te lossen in SQL. Er wordt verondersteld dat vrienden veel minder zijn dan degenen die leuk gevonden worden. Daarom staan ze in aparte tabellen. In het geval van een succesvolle oplossing is er ook een taak met twee sterren. Hier is vriendschap niet symmetrisch. In de functionele database zou het er als volgt uitzien:

SELECT Persoon a, Persoon b, Persoon c WAAR 
    houdt van(a, c) EN NIET vrienden(a, c) EN 
    (vrienden(a, b) OF vrienden(b, a)) EN 
    (vrienden(b, c) OF vrienden(c, b));

UPD: oplossing van de taak met de eerste en de tweede ster van dss_kalika:

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

Conclusie

Het is belangrijk op te merken dat de gegeven syntaxis van de taal slechts één van de mogelijke manieren is om het concept uit te voeren. De basis is SQL genomen, en het doel was om het zoveel mogelijk daarop te laten lijken. Natuurlijk kan iemand het niet eens zijn met de namen van de sleutelwoorden, de registerinstellingen van de woorden en dergelijke. De essentie is namelijk het concept zelf. Indien gewenst kan vergelijkbare syntaxis in C++ en Python worden gemaakt.

Volgens mij heeft het beschreven databankconcept de volgende voordelen:

  • Eenvoud. Dit is een relatief subjectieve maatstaf die niet duidelijk is in eenvoudige gevallen. Maar als we naar complexere situaties kijken (bijvoorbeeld star-taken), dan vind ik het schrijven van dergelijke aanvragen aanzienlijk eenvoudiger.
  • Encapsulatie. In sommige voorbeelden heb ik tussentijdse functies gedeclareerd (zoals verkocht, aangekocht enz.), waarop de daaropvolgende functies zijn gebaseerd. Dit maakt het mogelijk om de logica van bepaalde functies te wijzigen zonder de logica van afhankelijke functies aan te passen. Bijvoorbeeld, het is mogelijk om de verkoop verkocht waren afkomstig van totaal andere objecten, terwijl de rest van de logica ongewijzigd blijft. Ja, in een RDBMS kan dit worden gerealiseerd met behulp van CREATE VIEW. Maar als je de hele logica zo schrijft, zal het er niet erg leesbaar uitzien.
  • Afwezigheid van semantische breuken. Zo'n database werkt met functies en klassen (in plaats van tabellen en velden). Net zoals in klassieke programmering (als je ervan uitgaat dat een methode een functie is met de eerste parameter als de klasse waartoe deze behoort). Bijgevolg zou het „verzoenen” met universele programmeertalen aanzienlijk eenvoudiger moeten zijn. Bovendien maakt dit concept het mogelijk om veel complexere functies te implementeren. Bijvoorbeeld, je kunt in de database operators van het type inbouwen:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Petya' MESSAGE 'Petya verkoopt te veel van hetzelfde product in 2019.';

  • Overerving en polymorfisme. In een functionele database kan meervoudige overerving worden geïntroduceerd via constructies zoals CLASS ClassP: Class1, Class2 en meervoudig polymorfisme worden gerealiseerd. Hoe precies, wellicht schrijf ik dat in volgende artikelen.

Hoewel het slechts een concept is, hebben we al een bepaalde implementatie in Java, die alle functionele logica omzet naar relationele logica. Bovendien is de logica van weergaven mooi geïntegreerd, samen met veel andere dingen, waardoor we een hele platform. In essentie gebruiken we RDBMS (tot nu toe alleen PostgreSQL) als een „virtuele machine”. Bij deze transformatie ontstaan soms problemen, omdat de query-optimizer van het RDBMS bepaalde statistieken niet kent, die de FSDB wel kent. In theorie zou het mogelijk zijn om een databasesysteem te creëren dat een structuur gebruikt die specifiek is aangepast aan de functionele logica.

Bron: habr.com

Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers 🔥 Koop betrouwbare webhosting met bescherming tegen DDoS, VPS VDS servers | ProHoster