Funktionell DBMS

Databasvärlden har länge dominerats av relationella DBMS, som använder SQL-språket. Så mycket att nya varianter kallas NoSQL. De lyckades skapa sig en viss plats på denna marknad, men relationella DBMS kommer inte att dö och fortsätter att användas aktivt för sina syften.

I den här artikeln vill jag beskriva konceptet med en funktionell databas. För en bättre förståelse kommer jag att göra detta genom att jämföra det med den klassiska relationsmodellen. Problem från olika SQL-tester som hittats på Internet kommer att användas som exempel.

Inledning

Relationsdatabaser fungerar på tabeller och fält. I en funktionsdatabas kommer klasser respektive funktioner att användas istället. Ett fält i en tabell med N nycklar kommer att representeras som en funktion av N parametrar. Istället för relationer mellan tabeller kommer funktioner att användas som returnerar objekt av den klass som kopplingen görs till. Funktionssammansättning kommer att användas istället för JOIN.

Innan jag går direkt till uppgifterna kommer jag att beskriva uppgiften med domänlogik. För DDL kommer jag att använda PostgreSQL-syntax. För funktionell har den sin egen syntax.

Tabeller och fält

Ett enkelt Sku-objekt med namn- och prisfält:

Relationellt

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

funktionell

CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);

Vi tillkännager två funktioner, som tar en parameter Sku som indata och returnerar en primitiv typ.

Det antas att i en funktionell DBMS kommer varje objekt att ha någon intern kod som genereras automatiskt och som kan nås vid behov.

Låt oss sätta priset för produkten/butiken/leverantören. Det kan ändras med tiden, så låt oss lägga till ett tidsfält i tabellen. Jag hoppar över att deklarera tabeller för kataloger i en relationsdatabas för att förkorta koden:

Relationellt

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

funktionell

CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);

Index

För det sista exemplet kommer vi att bygga ett index på alla nycklar och datum så att vi snabbt kan hitta priset för en specifik tid.

Relationellt

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

funktionell

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

uppgifter

Låt oss börja med relativt enkla problem hämtade från motsvarande Artikel på Habr.

Låt oss först deklarera domänlogiken (för relationsdatabasen görs detta direkt i artikeln ovan).

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

Uppgift 1.1

Visa en lista över anställda som får en högre lön än deras närmaste chef.

Relationellt

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

funktionell

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

Uppgift 1.2

Lista de anställda som får maxlön på sin avdelning

Relationellt

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

funktionell

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

Båda implementeringarna är likvärdiga. För det första fallet kan du i en relationsdatabas använda CREATE VIEW, som på samma sätt först kommer att beräkna maxlönen för en specifik avdelning i den. I det följande kommer jag för tydlighetens skull att använda det första fallet, eftersom det bättre speglar lösningen.

Uppgift 1.3

Visa en lista över avdelnings-ID:n där antalet anställda inte överstiger 3 personer.

Relationellt

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

funktionell

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

Uppgift 1.4

Visa en lista över anställda som inte har en utsedd chef som arbetar på samma avdelning.

Relationellt

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

funktionell

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

Uppgift 1.5

Hitta en lista över avdelnings-ID:n med den maximala totala lönen.

Relationellt

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 )

funktionell

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

Låt oss gå vidare till mer komplexa uppgifter från en annan Artikel. Den innehåller en detaljerad analys av hur man implementerar denna uppgift i MS SQL.

Uppgift 2.1

Vilka säljare sålde mer än 1997 enheter av produkt nr 30 1?

Domänlogik (som tidigare på RDBMS hoppar vi över deklarationen):

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

Relationellt

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

funktionell

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;

Uppgift 2.2

För varje köpare (namn, efternamn), hitta de två varor (namn) som köparen spenderade mest pengar på 1997.

Vi utökar domänlogiken från föregående exempel:

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

customer = DATA Customer (Order);

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

Relationellt

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

funktionell

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;

PARTITION-operatorn fungerar enligt följande princip: den summerar uttrycket som anges efter SUM (här 1), inom de angivna grupperna (här Kund och År, men kan vara vilket uttryck som helst), sorterar inom grupperna efter uttrycken som anges i ORDER ( här köpt, och om lika, då enligt den interna produktkoden).

Uppgift 2.3

Hur många varor behöver beställas från leverantörer för att uppfylla aktuella beställningar.

Låt oss utöka domänlogiken igen:

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

supplier = DATA Supplier (Product);

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

Relationellt

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

funktionell

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;

Problem med en asterisk

Och det sista exemplet är från mig personligen. Det finns logiken i ett socialt nätverk. Människor kan vara vänner med varandra och tycka om varandra. Ur ett funktionellt databasperspektiv skulle det se ut så här:

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

Det är nödvändigt att hitta möjliga kandidater för vänskap. Mer formellt måste du hitta alla personer A, B, C så att A är vän med B och B är vän med C, A gillar C, men A är inte vän med C.
Ur ett funktionellt databasperspektiv skulle frågan se ut så här:

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

Läsaren uppmuntras att lösa detta problem i SQL på egen hand. Det antas att det finns mycket färre vänner än människor du gillar. Därför finns de i separata tabeller. Om det lyckas finns det också en uppgift med två stjärnor. I den är vänskap inte symmetrisk. På en funktionell databas skulle det se ut så här:

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ösning på problemet med den första och andra asterisken från 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 

Slutsats

Det bör noteras att den givna språksyntaxen bara är ett av alternativen för att implementera det givna konceptet. SQL togs som grund och målet var att den skulle vara så lik den som möjligt. Vissa kanske inte gillar namnen på nyckelord, ordregister osv. Huvudsaken här är själva konceptet. Om så önskas kan du göra både C++ och Python liknande syntax.

Det beskrivna databaskonceptet har enligt min mening följande fördelar:

  • Enkelhet. Detta är en relativt subjektiv indikator som inte är uppenbar i enkla fall. Men om du tittar på mer komplexa fall (till exempel problem med asterisker), så är det enligt min mening mycket lättare att skriva sådana frågor.
  • Инкапсуляция. I några exempel deklarerade jag mellanfunktioner (t.ex. säljs, köpt etc.), varifrån efterföljande funktioner byggdes. Detta gör att du kan ändra logiken för vissa funktioner, om det behövs, utan att ändra logiken för de som är beroende av dem. Du kan till exempel göra försäljning säljs beräknades från helt andra objekt, medan resten av logiken inte kommer att förändras. Ja, detta kan implementeras i ett RDBMS med CREATE VIEW. Men om all logik är skriven på det här sättet kommer den inte att se särskilt läsbar ut.
  • Inget semantiskt gap. En sådan databas fungerar på funktioner och klasser (istället för tabeller och fält). Precis som i klassisk programmering (om vi antar att en metod är en funktion med den första parametern i form av den klass den tillhör). Följaktligen borde det vara mycket lättare att "bli vänner" med universella programmeringsspråk. Dessutom tillåter detta koncept att mycket mer komplex funktionalitet kan implementeras. Du kan till exempel bädda in operatorer som:

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

  • Arv och polymorfism. I en funktionell databas kan du introducera multipelt arv genom CLASS ClassP: Class1, Class2-konstruktionerna och implementera multipel polymorfism. Jag kommer förmodligen att skriva exakt hur i framtida artiklar.

Även om detta bara är ett koncept har vi redan en viss implementering i Java som översätter all funktionell logik till relationslogik. Plus, logiken i representationer och många andra saker är vackert fästa vid den, tack vare vilken vi får en helhet plattform. I huvudsak använder vi RDBMS (endast PostgreSQL för närvarande) som en "virtuell maskin". Problem uppstår ibland med denna översättning eftersom RDBMS-frågeoptimeraren inte känner till viss statistik som FDBMS känner till. I teorin är det möjligt att implementera ett databashanteringssystem som kommer att använda en viss struktur som lagring, anpassat specifikt för funktionell logik.

Källa: will.com

Lägg en kommentar