Functioneel DBMS

De wereld van databases wordt lange tijd gedomineerd door relationele DBMS'en, die de SQL-taal gebruiken. Zozeer zelfs dat opkomende varianten NoSQL worden genoemd. Ze zijn erin geslaagd een bepaalde plaats voor zichzelf te veroveren op deze markt, maar relationele DBMS'en zullen niet uitsterven en actief voor hun doeleinden worden gebruikt.

In dit artikel wil ik het concept van een functionele database beschrijven. Voor een beter begrip zal ik dit doen door het te vergelijken met het klassieke relationele model. Problemen uit verschillende SQL-tests die op internet te vinden zijn, zullen als voorbeeld worden gebruikt.

Introductie

Relationele databases werken met tabellen en velden. In een functionele database zullen in plaats daarvan respectievelijk klassen en functies worden gebruikt. Een veld in een tabel met N sleutels zal worden weergegeven als een functie van N parameters. In plaats van relaties tussen tabellen zullen functies worden gebruikt die objecten retourneren van de klasse waarmee de verbinding is gemaakt. Er zal gebruik worden gemaakt van functiesamenstelling in plaats van JOIN.

Voordat ik direct naar de taken ga, zal ik de taak van domeinlogica beschrijven. Voor DDL gebruik ik de PostgreSQL-syntaxis. Voor functioneel heeft het zijn eigen syntaxis.

Tabellen en velden

Een eenvoudig Sku-object met naam- en prijsvelden:

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;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);

Wij kondigen er twee aan functies, die één parameter Sku als invoer gebruiken en een primitief type retourneren.

Er wordt aangenomen dat in een functioneel DBMS elk object een interne code zal hebben die automatisch wordt gegenereerd en waartoe indien nodig toegang kan worden verkregen.

Laten we de prijs voor het product/winkel/leverancier instellen. Dit kan in de loop van de tijd veranderen, dus laten we een tijdveld aan de tabel toevoegen. Ik sla het declareren van tabellen voor mappen in een relationele database over om de code in te korten:

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;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);

Index

Voor het laatste voorbeeld bouwen we een index op alle sleutels en de datum zodat we snel de prijs voor een bepaalde tijd kunnen vinden.

relationeel

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

functioneel

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

taken

Laten we beginnen met relatief eenvoudige problemen uit de overeenkomstige Artikel op Habr.

Laten we eerst de domeinlogica declareren (voor de relationele database gebeurt dit rechtstreeks in het bovenstaande 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);

Taak 1.1

Geef een lijst weer van werknemers 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

Maak een lijst van de werknemers die op hun afdeling het maximale salaris 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 'Максимальная зарплата' (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 implementaties zijn gelijkwaardig. Voor het eerste geval kunt u in een relationele database CREATE VIEW gebruiken, die op dezelfde manier eerst het maximale salaris voor een specifieke afdeling daarin berekent. In wat volgt, zal ik voor de duidelijkheid het eerste geval gebruiken, omdat dit de oplossing beter weergeeft.

Taak 1.3

Geef een lijst met afdelings-ID's weer, waarbij het aantal werknemers niet groter is dan 3 personen.

relationeel

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

functioneel

countEmployees 'Количество сотрудников' (Department d) = 
    GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;

Taak 1.4

Geef een lijst weer van medewerkers die geen toegewezen manager hebben die op dezelfde afdeling werkt.

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

Zoek een lijst met afdelings-ID's met het maximale totale werknemerssalaris.

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

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

Laten we verder gaan met complexere taken van een andere Artikel. Het bevat een gedetailleerde analyse van hoe deze taak in MS SQL kan worden geïmplementeerd.

Taak 2.1

Welke verkopers verkochten in 1997 meer dan 30 eenheden van product nr. 1?

Domeinlogica (zoals eerder op RDBMS slaan we de declaratie over):

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

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

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;

Taak 2.2

Zoek voor elke koper (naam, achternaam) de twee goederen (naam) waaraan de koper in 1997 het meeste geld heeft uitgegeven.

We breiden de domeinlogica uit het vorige voorbeeld uit:

CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);

customer = DATA Customer (Order);

unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);

relationeel

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

functioneel

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;

De operator PARTITION werkt volgens het volgende principe: hij somt de expressie op die gespecificeerd is na SUM (hier 1), binnen de gespecificeerde groepen (hier Klant en Jaar, maar kan elke expressie zijn), en sorteert binnen de groepen op de expressies gespecificeerd in de ORDER ( hier gekocht, en indien gelijk, dan volgens de interne productcode).

Taak 2.3

Hoeveel goederen moeten er bij leveranciers worden besteld om lopende bestellingen uit te voeren.

Laten we de domeinlogica opnieuw uitbreiden:

CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);

supplier = DATA Supplier (Product);

unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);

relationeel

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

functioneel

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;

Probleem met een sterretje

En het laatste voorbeeld is van mij persoonlijk. Er is de logica van een sociaal netwerk. Mensen kunnen vrienden met elkaar zijn en elkaar aardig vinden. Vanuit een functioneel databaseperspectief zou het er als volgt uitzien:

CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);

Het is noodzakelijk om mogelijke kandidaten voor vriendschap te vinden. Formeeler moet je alle mensen A, B, C vinden, zodat A bevriend is met B, en B bevriend is met C, A houdt van C, maar A is geen vrienden met C.
Vanuit een functioneel databaseperspectief zou de query er als volgt uitzien:

SELECT Person a, Person b, Person c WHERE 
    likes(a, c) AND NOT friends(a, c) AND 
    friends(a, b) AND friends(b, c);

De lezer wordt aangemoedigd dit probleem zelf in SQL op te lossen. Er wordt aangenomen dat er veel minder vrienden zijn dan mensen die je leuk vindt. Daarom staan ​​ze in aparte tabellen. Bij succes is er ook een taak met twee sterren. Daarin is vriendschap niet symmetrisch. Op een functionele database zou het er als volgt uitzien:

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: oplossing voor het probleem met het eerste en tweede sterretje uit 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 

Conclusie

Opgemerkt moet worden dat de gegeven taalsyntaxis slechts een van de opties is om het gegeven concept te implementeren. SQL werd als basis genomen en het doel was dat het er zo veel mogelijk op zou lijken. Natuurlijk zullen sommigen de namen van trefwoorden, woordregisters, enz. misschien niet leuk vinden. Het belangrijkste hier is het concept zelf. Indien gewenst kunt u zowel C++ als Python een vergelijkbare syntaxis maken.

Het beschreven databaseconcept heeft naar mijn mening de volgende voordelen:

  • Gemak. Dit is een relatief subjectieve indicator die in eenvoudige gevallen niet duidelijk is. Maar als je naar complexere gevallen kijkt (bijvoorbeeld problemen met sterretjes), dan is het schrijven van dergelijke vragen naar mijn mening veel eenvoudiger.
  • Инкапсуляция. In sommige voorbeelden heb ik tussenfuncties gedeclareerd (bijvoorbeeld uitverkocht, gekocht enz.), waaruit daaropvolgende functies werden opgebouwd. Hierdoor kunt u indien nodig de logica van bepaalde functies wijzigen, zonder de logica te veranderen van de functies die ervan afhankelijk zijn. U kunt bijvoorbeeld verkopen doen uitverkocht werden berekend op basis van totaal verschillende objecten, terwijl de rest van de logica niet zal veranderen. Ja, dit kan worden geïmplementeerd in een RDBMS met behulp van CREATE VIEW. Maar als alle logica op deze manier is geschreven, zal het er niet erg leesbaar uitzien.
  • Geen semantische kloof. Zo'n database werkt op functies en klassen (in plaats van op tabellen en velden). Net als bij klassiek programmeren (als we aannemen dat een methode een functie is met als eerste parameter de vorm van de klasse waartoe deze behoort). Dienovereenkomstig zou het veel gemakkelijker moeten zijn om “vrienden te maken” met universele programmeertalen. Bovendien maakt dit concept het mogelijk om veel complexere functionaliteit te implementeren. U kunt bijvoorbeeld operators insluiten zoals:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE  'Что-то Петя продает слишком много одного товара в 2019 году';

  • Overerving en polymorfisme. In een functionele database kunt u meervoudige overerving introduceren via de CLASS ClassP: Class1, Class2-constructies en meervoudig polymorfisme implementeren. Ik zal waarschijnlijk in toekomstige artikelen schrijven hoe precies.

Hoewel dit slechts een concept is, hebben we al een implementatie in Java die alle functionele logica vertaalt in relationele logica. Bovendien is de logica van representaties en een heleboel andere dingen er prachtig aan verbonden, waardoor we een geheel krijgen platform. In wezen gebruiken we het RDBMS (voorlopig alleen PostgreSQL) als een “virtuele machine”. Er doen zich soms problemen voor bij deze vertaling omdat de RDBMS-queryoptimalisatie bepaalde statistieken niet kent die de FDBMS wel kent. In theorie is het mogelijk om een ​​databasebeheersysteem te implementeren dat een bepaalde structuur als opslag gebruikt, specifiek aangepast voor functionele logica.

Bron: www.habr.com

Voeg een reactie