Funkcionalen DBMS

V svetu baz podatkov že dolgo prevladujejo relacijski DBMS-ji, ki uporabljajo jezik SQL. Tako zelo, da se nastajajoče različice imenujejo NoSQL. Uspeli so si izboriti določeno mesto na tem trgu, vendar relacijski DBMS ne bodo umrli in se bodo še naprej aktivno uporabljali za svoje namene.

V tem članku želim opisati koncept funkcionalne baze podatkov. Za boljše razumevanje bom to naredil tako, da ga bom primerjal s klasičnim relacijskim modelom. Kot primeri bodo uporabljeni problemi iz različnih testov SQL, ki jih najdemo na internetu.

Predstavitev

Relacijske baze podatkov delujejo na tabelah in poljih. V funkcionalni bazi podatkov bodo namesto tega uporabljeni razredi oziroma funkcije. Polje v tabeli z N ključi bo predstavljeno kot funkcija N parametrov. Namesto relacij med tabelami bodo uporabljene funkcije, ki vračajo objekte razreda, s katerim je vzpostavljena povezava. Namesto JOIN bo uporabljena sestava funkcij.

Preden preidem neposredno na naloge, bom opisal nalogo domenske logike. Za DDL bom uporabil sintakso PostgreSQL. Za funkcionalnost ima svojo sintakso.

Tabele in polja

Preprost predmet Sku s polji za ime in ceno:

Relacijska

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

Funkcionalna

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

Objavljamo dva funkcije, ki sprejmejo en parameter Sku kot vhod in vrnejo primitivni tip.

Predpostavlja se, da bo v funkcionalnem DBMS vsak objekt imel neko notranjo kodo, ki je samodejno ustvarjena in do katere lahko po potrebi dostopate.

Določimo ceno za izdelek/trgovino/dobavitelja. Sčasoma se lahko spremeni, zato v tabelo dodamo časovno polje. Preskočil bom deklaracijo tabel za imenike v relacijski bazi podatkov, da bi skrajšal kodo:

Relacijska

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

Funkcionalna

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

Indeksi

Za zadnji primer bomo zgradili indeks na vseh ključih in datumu, da bomo lahko hitro našli ceno za določen čas.

Relacijska

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

Funkcionalna

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

naloge

Začnimo z razmeroma preprostimi problemi, vzetimi iz ustreznih Člen na Habru.

Najprej deklarirajmo logiko domene (za relacijsko bazo podatkov je to storjeno neposredno v zgornjem članku).

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

Izziv 1.1

Prikažite seznam zaposlenih, ki prejemajo višjo plačo od neposrednega nadrejenega.

Relacijska

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

Funkcionalna

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

Izziv 1.2

Navedite zaposlene, ki prejemajo najvišjo plačo v svojem oddelku

Relacijska

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

Funkcionalna

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

Obe izvedbi sta enakovredni. Za prvi primer lahko v relacijski bazi podatkov uporabite CREATE VIEW, ki bo na enak način najprej izračunal maksimalno plačo za določen oddelek v njej. V nadaljevanju bom zaradi jasnosti uporabil prvi primer, saj bolje odraža rešitev.

Izziv 1.3

Prikažite seznam ID-jev oddelkov, v katerih število zaposlenih ne presega 3 ljudi.

Relacijska

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

Funkcionalna

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

Izziv 1.4

Prikažite seznam zaposlenih, ki nimajo imenovanega vodje, ki dela v istem oddelku.

Relacijska

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

Funkcionalna

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

Izziv 1.5

Poiščite seznam ID-jev oddelkov z najvišjo skupno plačo zaposlenega.

Relacijska

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 )

Funkcionalna

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

Preidimo na bolj zapletene naloge z drugega Člen. Vsebuje podrobno analizo izvajanja te naloge v MS SQL.

Izziv 2.1

Kateri prodajalci so leta 1997 prodali več kot 30 enot izdelka št. 1?

Logika domene (kot prej na RDBMS preskočimo deklaracijo):

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

Relacijska

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

Funkcionalna

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;

Izziv 2.2

Za vsakega kupca (ime, priimek) poiščite dve dobrini (ime), za kateri je kupec leta 1997 zapravil največ denarja.

Razširimo logiko domene iz prejšnjega primera:

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

customer = DATA Customer (Order);

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

Relacijska

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

Funkcionalna

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;

Operator PARTITION deluje po naslednjem principu: sešteje izraz, določen za SUM (tukaj 1), znotraj navedenih skupin (tukaj Stranka in Leto, vendar je lahko kateri koli izraz), razvrščanje znotraj skupin po izrazih, navedenih v ORDER ( tukaj kupljeno, in če je enako, potem v skladu z interno kodo izdelka).

Izziv 2.3

Koliko blaga je treba naročiti pri dobaviteljih za izpolnitev trenutnih naročil.

Ponovno razširimo domensko logiko:

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

supplier = DATA Supplier (Product);

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

Relacijska

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

Funkcionalna

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;

Težava z zvezdico

In zadnji primer je od mene osebno. Obstaja logika družbenega omrežja. Ljudje smo lahko med seboj prijatelji in se imamo radi. Z vidika funkcionalne baze podatkov bi to izgledalo takole:

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

Treba je najti možne kandidate za prijateljstvo. Bolj formalno, najti morate vse ljudi A, B, C, tako da je A prijatelj z B in B prijatelj s C, A je všeč C, vendar A ni prijatelj s C.
Z vidika funkcionalne baze podatkov bi bila poizvedba videti takole:

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

Bralca spodbujamo, da to težavo reši v SQL sam. Predpostavlja se, da je prijateljev veliko manj kot ljudi, ki so vam všeč. Zato so v ločenih tabelah. V primeru uspeha je na voljo tudi naloga z dvema zvezdicama. V njej prijateljstvo ni simetrično. V funkcionalni bazi podatkov bi bilo videti takole:

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: rešitev težave s prvo in drugo zvezdico iz 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 

Zaključek

Opozoriti je treba, da je podana sintaksa jezika le ena od možnosti za implementacijo danega koncepta. Za osnovo je bil vzet SQL, cilj pa je bil, da bi mu bil čim bolj podoben. Seveda nekaterim morda ne bodo všeč imena ključnih besed, besedni registri itd. Tu je glavna stvar sam koncept. Če želite, lahko naredite C++ in Python podobno sintakso.

Opisani koncept baze podatkov ima po mojem mnenju naslednje prednosti:

  • preprostost. To je relativno subjektiven indikator, ki v preprostih primerih ni očiten. Če pa pogledate bolj zapletene primere (na primer težave z zvezdicami), potem je po mojem mnenju pisanje takšnih poizvedb veliko lažje.
  • Инкапсуляция. V nekaterih primerih sem deklariral vmesne funkcije (npr. prodaja, Kupil itd.), iz katerih so bile zgrajene naslednje funkcije. To vam omogoča, da po potrebi spremenite logiko določenih funkcij, ne da bi spremenili logiko tistih, ki so od njih odvisne. Na primer, lahko prodajate prodaja so bili izračunani iz popolnoma različnih objektov, medtem ko se ostala logika ne bo spremenila. Da, to je mogoče implementirati v RDBMS z uporabo CREATE VIEW. Toda če je vsa logika zapisana na ta način, ne bo videti zelo berljivo.
  • Brez pomenske vrzeli. Takšna zbirka podatkov deluje na funkcijah in razredih (namesto na tabelah in poljih). Tako kot pri klasičnem programiranju (če predpostavimo, da je metoda funkcija s prvim parametrom v obliki razreda, ki mu pripada). V skladu s tem bi moralo biti veliko lažje "sprijateljiti se" z univerzalnimi programskimi jeziki. Poleg tega ta koncept omogoča implementacijo veliko bolj kompleksne funkcionalnosti. Vdelate lahko na primer operatorje, kot so:

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

  • Dedovanje in polimorfizem. V funkcionalni bazi podatkov lahko uvedete večkratno dedovanje prek konstruktov CLASS ClassP: Class1, Class2 in implementirate večkratni polimorfizem. Verjetno bom v naslednjih člankih napisal, kako natančno.

Čeprav je to samo koncept, že imamo nekaj implementacije v Javi, ki prevede vso funkcionalno logiko v relacijsko logiko. Poleg tega je nanj lepo pripeta logika predstav in veliko drugih stvari, zaradi česar dobimo celoto Platforma. V bistvu uporabljamo RDBMS (za zdaj samo PostgreSQL) kot "virtualni stroj". S tem prevodom se včasih pojavijo težave, ker optimizator poizvedb RDBMS ne pozna določenih statističnih podatkov, ki jih pozna FDBMS. Teoretično je možno implementirati sistem za upravljanje baz podatkov, ki bo kot shrambo uporabljal določeno strukturo, prilagojeno posebej za funkcionalno logiko.

Vir: www.habr.com

Dodaj komentar