Funksionele DBMS

Die databasiswêreld is lank reeds oorgeneem deur relasionele DBBS'e wat die SQL-taal gebruik. Soveel so dat opkomende variëteite NoSQL genoem word. Hulle het daarin geslaag om 'n sekere plek vir hulself in hierdie mark te wen, maar relasionele DBMS gaan nie doodgaan nie, en gaan voort om aktief vir hul eie doeleindes gebruik te word.

In hierdie artikel wil ek die konsep van 'n funksionele databasis beskryf. Vir 'n beter begrip sal ek dit doen deur met die klassieke relasionele model te vergelyk. As voorbeelde sal take van verskeie SQL-toetse wat op die internet gevind word, gebruik word.

Inleiding

Relasionele databasisse werk op tabelle en velde. In 'n funksionele databasis sal klasse en funksies onderskeidelik in plaas daarvan gebruik word. 'n Veld in 'n tabel met N sleutels sal as 'n funksie van N parameters voorgestel word. In plaas van skakels tussen tabelle, sal funksies gebruik word wat voorwerpe van die klas waarheen die skakel gaan terugstuur. Funksie samestelling sal gebruik word in plaas van JOIN.

Voordat ek direk na die take voortgaan, sal ek die taak van domeinlogika beskryf. Vir DDL sal ek PostgreSQL-sintaksis gebruik. Vir funksionele sy eie sintaksis.

Tabelle en velde

'n Eenvoudige Sku-voorwerp met naam- en prysvelde:

relasionele

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

Funksioneel

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

Ons kondig twee aan funksies, wat een Sku-parameter as invoer neem en 'n primitiewe tipe terugstuur.

Daar word aanvaar dat in 'n funksionele DBBS elke voorwerp 'n interne kode sal hê wat outomaties gegenereer word en toegang kan verkry word indien nodig.

Kom ons stel die prys vir die produk / winkel / verskaffer vas. Dit kan met verloop van tyd verander, so kom ons voeg 'n tydveld by die tabel. Ek sal die verklaring van tabelle vir gidse in 'n relasionele databasis oorslaan om die kode te verkort:

relasionele

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

Funksioneel

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

Indekse

Vir die laaste voorbeeld, kom ons bou 'n indeks op alle sleutels en datum sodat ons vinnig die prys vir 'n sekere tyd kan vind.

relasionele

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

Funksioneel

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

take

Kom ons begin met relatief eenvoudige probleme geneem uit die ooreenstemmende Artikel op Habr.

Kom ons verklaar eers die domeinlogika (vir 'n relasionele databasis word dit direk in die bogenoemde artikel gedoen).

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

1.1 uitdaging

Vertoon 'n lys van werknemers wat lone ontvang wat groter is as dié van die onmiddellike toesighouer.

relasionele

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

Funksioneel

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

1.2 uitdaging

Vertoon 'n lys van werknemers wat die hoogste salaris in hul departement verdien

relasionele

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

Funksioneel

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 implementerings is gelykstaande. Vir die eerste geval in die relasionele databasis kan jy CREATE VIEW gebruik, wat op dieselfde manier eers die maksimum salaris vir 'n spesifieke departement daarin sal bereken. In die toekoms, vir duidelikheid, sal ek die eerste geval gebruik, aangesien dit die oplossing beter weerspieël.

1.3 uitdaging

Vertoon 'n lys van departement-ID's, die aantal werknemers waarin nie 3 mense oorskry nie.

relasionele

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

Funksioneel

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

1.4 uitdaging

Vertoon 'n lys van werknemers wat nie 'n toegewese bestuurder het wat in dieselfde departement werk nie.

relasionele

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

Funksioneel

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

1.5 uitdaging

Vind die lys van departement-ID's met die maksimum totale werknemersalaris.

relasionele

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 )

Funksioneel

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

Kom ons gaan oor na meer komplekse take van 'n ander Artikel. Dit bevat 'n gedetailleerde ontleding van hoe om hierdie taak in MS SQL te implementeer.

2.1 uitdaging

Watter verkopers het meer as 1997 stukke item #30 in 1 verkoop?

Domeinlogika (soos voorheen, slaan ons die verklaring op die RDBMS oor):

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

relasionele

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

Funksioneel

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;

2.2 uitdaging

Vir elke kliënt (voornaam, van), vind die twee items (naam) waaraan die kliënt die meeste geld in 1997 bestee het.

Die uitbreiding van die domeinlogika van die vorige voorbeeld:

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

customer = DATA Customer (Order);

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

relasionele

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

Funksioneel

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;

Die PARTITION-operateur werk volgens die volgende beginsel: dit som die uitdrukking gespesifiseer na SUM (hier 1) binne die gespesifiseerde groepe (hier Klant en Jaar, maar kan enige uitdrukking wees), sorteer binne die groepe volgens die uitdrukkings gespesifiseer in ORDER ( hier gekoop, en as dit gelyk is, dan volgens die interne produkkode).

2.3 uitdaging

Hoeveel goedere moet van verskaffers bestel word om huidige bestellings te vervul.

Kom ons brei die domeinlogika weer uit:

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

supplier = DATA Supplier (Product);

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

relasionele

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

Funksioneel

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;

Taak met 'n asterisk

En die laaste voorbeeld is van my persoonlik. Daar is die logika van 'n sosiale netwerk. Mense kan vriende met mekaar wees en van mekaar hou. Vanuit 'n funksionele databasisperspektief sou dit soos volg lyk:

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

Dit is nodig om moontlike kandidate vir vriendskap te vind. Meer formeel moet jy alle mense A, B, C so vind dat A vriende is met B, en B vriende is met C, A hou van C, maar A is nie vriende met C nie.
Vanuit 'n funksionele databasis oogpunt sal die navraag soos volg lyk:

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

Die leser word uitgenooi om hierdie probleem onafhanklik in SQL op te los. Daar word aanvaar dat daar baie minder vriende is as diegene wat daarvan hou. Daarom is hulle in aparte tabelle. In die geval van 'n suksesvolle oplossing is daar ook 'n probleem met twee sterretjies. Haar vriendskap is nie simmetries nie. Op 'n funksionele databasis sal dit soos volg lyk:

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 van die probleem met die eerste en tweede sterretjie van 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 

Gevolgtrekking

Daar moet kennis geneem word dat die bogenoemde sintaksis van die taal net een van die opsies is vir die implementering van bogenoemde konsep. Dit was SQL wat as die basis geneem is, en die doel was om dit so soortgelyk as moontlik daaraan te maak. Natuurlik hou iemand dalk nie van die name van sleutelwoorde, hoofletters van woorde, ensovoorts nie. Die belangrikste ding hier is die konsep self. As jy wil, kan jy beide C ++ en Python soortgelyke sintaksis maak.

Die beskryfde databasiskonsep het na my mening die volgende voordele:

  • Eenvoud. Dit is 'n relatief subjektiewe aanwyser wat nie voor die hand liggend is in eenvoudige gevalle nie. Maar as jy na meer komplekse gevalle kyk (byvoorbeeld take met sterretjies), dan is dit na my mening baie makliker om sulke navrae te skryf.
  • Инкапсуляция. In sommige voorbeelde het ek intermediêre funksies verklaar (byvoorbeeld, verkoop, gekoop ens.), waaruit daaropvolgende funksies gebou is. Dit laat jou toe om die logika van sekere funksies te verander, indien nodig, sonder om die logika van diegene wat daarvan afhanklik is, te verander. Byvoorbeeld, jy kan verkope maak verkoop is uit heeltemal verskillende voorwerpe bereken, terwyl die res van die logika nie sal verander nie. Ja, in RDBMS kan dit gedoen word met CREATE VIEW. Maar as jy al die logika op hierdie manier skryf, dan sal dit nie baie leesbaar lyk nie.
  • Geen semantiese gaping nie. So 'n databasis werk met funksies en klasse (in plaas van tabelle en velde). Op dieselfde manier as in klassieke programmering (met die veronderstelling dat 'n metode 'n funksie is met die eerste parameter in die vorm van 'n klas waaraan dit behoort). Gevolglik behoort dit baie makliker te wees om "vriende te maak" met universele programmeertale. Daarbenewens laat hierdie konsep jou toe om baie meer komplekse funksies te implementeer. Byvoorbeeld, jy kan stellings soos hierdie in die databasis insluit:

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

  • Oorerwing en polimorfisme. In 'n funksionele databasis kan jy meervoudige oorerwing instel deur die KLAS KlasP: Klas1, Klas2-konstrukte en meervoudige polimorfisme implementeer. Hoe presies, miskien sal ek in die volgende artikels skryf.

Alhoewel dit net 'n konsep is, het ons reeds 'n mate van implementering in Java wat alle funksionele logika in relasionele logika vertaal. Boonop is die logika van voorstellings en baie ander dinge pragtig daaraan vasgeskroef, waardeur ons 'n geheel kry platform. In wese gebruik ons ​​'n RDBMS (slegs PostgreSQL tot dusver) as 'n "virtuele masjien". Hierdie vertaling veroorsaak soms probleme omdat die RDBMS-navraagoptimeerder nie sekere statistieke ken wat die FDBMS ken nie. In teorie is dit moontlik om 'n databasisbestuurstelsel te implementeer wat 'n sekere struktuur as berging sal gebruik, spesifiek aangepas vir funksionele logika.

Bron: will.com

Voeg 'n opmerking