Funktsionaalne andmebaas

Andmed, mis on salvestatud andmebaasides, on juba ammu domineeritud relatsiooniliste andmebaaside poolt, kus kasutatakse SQL keelt. Nii tugevasti, et tekkivad variandid nimetatakse NoSQL-iks. Neil õnnestus endale turul kindel koht saada, kuid relatsioonilised andmebaasid ei kavatse kaduda ja neid kasutatakse jätkuvalt aktiivselt oma eesmärkide saavutamiseks.

Selles artiklis tahan kirjeldada funktsionaalse andmebaasi kontseptsiooni. Parema arusaamise nimel teen seda, võrreldes seda klassikalise relatsioonilise mudeliga. Näidetena kasutatakse ülesandeid erinevatest SQL testidest, mida leidub internetis.

Sissejuhatus

Relatsioonilised andmebaasid opereerivad tabelite ja väljadega. Funktsionaalses andmebaasis kasutatakse nende asemel klasse ja funktsioone. Tabeli väli N võtmega on esindatud funktsioonina N parameetriga. Tabelitevaheliste ühenduste asemel kasutatakse funktsioone, mis tagastavad objekti klassist, millega side käib. JOIN asemel kasutatakse funktsioonide kooskompositsiooni.

Enne kui asume ülesannete juurde, kirjeldan domeeniloogika ülesannet. DDL-i jaoks kasutan PostgreSQL-i süntaksit. Funktsionaalset ma oma süntaksit.

Tabelid ja väljad

Lihtne objekt Sku, millel on väljad nime ja hind:

Suhetepõhine

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

Funktsionaalne

KLASS Sku;
nimi = ANDMED STRING[100] (Sku);
hind = ANDMED NUMERIK[10,5] (Sku);

Deame kaks funktsioonid, mis võtavad ühe parameetri Sku ja tagastavad primitiivse tüübi.

Eeldatakse, et funktsionaalses andmebaasis on igal objektil mingi sisemine kood, mis genereeritakse automaatset ja millele on vajadusel võimalik juurde pääseda.

Seame toote/poe/tootja hinna. See võib aja jooksul muutuda, seetõttu lisame tabelisse välja aeg. Jätan vahele tabelite deklaratsiooni viidete jaoks relatsioonilises andmebaasis, et koodi lühendada:

Suhetepõhine

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

Funktsionaalne

KLASS Sku;
KLASS Pood;
KLASS Tarnija;
kuupäevAeg = ANDMED KUUPÄEV (Sku, Pood, Tarnija);
hind = ANDMED NUMERIK[10,5] (Sku, Pood, Tarnija);

Indeksid

Viimase näite jaoks loome indeksi kõigi võtmete ja kuupäeva järgi, et oleks kiire ja mugav leida teatud aja hind.

Suhetepõhine

LOOBA INDICE prices_date
    KÜLLÜS
    (skuId, storeId, supplierId, dateTime)

Funktsionaalne

INDeks Sku sk, Kauplus st, Tarnija sp, kuupäevAeg(sk, st, sp);

Ülesanded

Alustame suhteliselt lihtsate ülesannetega, mis on võetud vastavast artikkel Habr's.

Esiteks kuulutame välja domeeniloogika (relatsiooniliste andmebaaside puhul on see tehtud otse antud artiklis).

Klass Osakond;
nimi = ANDMED STRING[100] (Osakond);

Klass Employee;
osakond = ANDMETE Osakond (Employee);
juht = ANDMETE Employee (Employee);
nimi = ANDMETE STRING[100] (Employee);
palk = ANDMETE NUMERIC[14,2] (Employee);

Ülesanne 1.1

Kuvatakse töötajate nimekiri, kelle palk on suurem kui nende vahetu juhi palk.

Suhetepõhine

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

Funktsionaalne

VALI nimi(Töötaja a) KUS palk(a) > palk(juht(a));

Ülesanne 1.2

Kuvatakse töötajate nimekiri, kellel on oma osakonnas kõrgeim palk

Suhetepõhine

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

Funktsionaalne

maxSalary 'Maksimaalne palk' (osakond s) = 
    GRUPPE MAX palk (Töötaja e) KUI osakond(e) = s;
VALI nimi(Töötaja a) KUS palk(a) = maxSalary(osakond(a));

// или если "заинлайнить"
VALI name(Employee a) KUS 
    salary(a) = maxSalary(RÜHMA MAX salary(Employee e) KUI department(e) = department(a));

Mõlemad rakendused on ekvivalentne. Esimese juhtumi puhul saab relatsioonilises andmebaasis kasutada CREATE VIEW, mis arvutab samuti esmalt konkreetse osakonna maksimaalse palga. Edaspidi kasutan visuaalsuse huvides esimest juhtumit, kuna see peegeldab paremini lahendust.

Ülesanne 1.3

Kuva osakondade ID-de loend, kus töötajate arv ei ületata 3.

Suhetepõhine

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

Funktsionaalne

countEmployees 'Töötajate arv' (Osakond d) = 
    GRUPP SUM 1 KUI osakond (Töötaja e) = d;
VALI Osakond d KUS countEmployees(d) <= 3;

Ülesanne 1.4

Kuva töötajate nimekiri, kellel ei ole määratud juhti, kes töötab samas osakonnas.

Suhetepõhine

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

Funktsionaalne

VALI nimet(Employee a) KUSKIL ( mitte (department(chief(a)) = department(a));

Ülesanne 1.5

Leia osakondade ID-de loend maksimaalse kogupalgaga töötajate jaoks.

Suhetepõhine

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 )

Funktsionaalne

palkasumma 'Maksimaalne palk' (Osakond d) = 
    GRUPP SUM palk (Töötaja e) KUI osakond (e) = d;
maxPalkSum 'Maksimaalne palk osakondadele' () = 
    GRUPI MAX palkasumma (Osakond d);
VALI Osakond d KUS palkasumma(d) = maxPalkSum();

Liigume keerukamate ülesannete juurde teiselt artikkel. Siin on detailsed juhised selle ülesande rakendamiseks MS SQL-is.

Ülesanne 2.1

Millised müüjad müüsid 1997. aastal üle 30 ühiku toodet nr 1?

Domeeniloogika (nagu alati, jätame RDBMS-is välja kuulutamise):

Klass Töötaja 'Kaupmees';
perekonnanimi 'Perekond' = ANDMED STRING[100] (Töötaja);

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

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

CLASS Detail 'Tellimuse rida';

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

Suhetepõhine

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

Funktsionaalne

müüdud (Töötaja e, INTEGER tooteId, INTEGER aastaarv) = 
    GRUPP SUMMA kogus(OrderDetail d) KUI 
        töötaja(order(d)) = e JA 
        id(toode(d)) = tooteId JA 
        eritaasta(date(order(d))) = aastaarv;
VALI perekonnanimi(Töötaja e) KUS müüdud(e, 1, 1997) > 30;

Ülesanne 2.2

Iga kliendi (nimi, perekonnanimi) kohta leidke kaks toodet (nimetus), millele klient kulutas kõige rohkem raha 1997. aastal.

Laiendame eelnevast näitest pärit domeeniloogikat:

CLASS Customer 'Kliendi';
contactName 'Nimi' = DATA STRING[100] (Kliendi);

customer = DATA Customer (Order);

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

Suhetepõhine

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

Funktsionaalne

sum (Detail d) = quantity(d) * unitPrice(d) * (1 - discount(d));
ostetud 'Ostetud' (Kliendid c, Toode p, KOHANE y) = 
    GRUPI SUMMA sum(Detail d) KUI 
        klient(order(d)) = c JA 
        toode(d) = p JA 
        extractYear(date(order(d))) = y;
hinnang 'Hinnang' (Kliendid c, Toode p, KOHANE y) = 
    PARTITION SUM 1 ORDER DESC bought(c, p, y), p BY c, y;
VALI kontaktNimi(Kliendid c), nimi(Toode p) KUS hinnang(c, p, 1997) < 3;

PARTITION operaator töötab järgmiselt: ta summeerib pärast SUM (siin 1) märgitud väljendi määratud gruppides (siin Kliendi ja Aasta, kuid see võib olla ükskõik milline väljend), sorteerides gruppide sees vastavalt ORDER (siin ostetud, ja kui need on võrdsed, siis toote sisekoodile).

Ülesanne 2.3

Kui palju tooteid tuleks tarnijatelt tellida, et täita praeguseid tellimusi.

Laiendame taas domeeniloogikat:

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

supplier = DATA Supplier (Product);

unitsInStock 'Laoseis' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Müüginorm' = DATA NUMERIC[10,3] (Product);

Suhetepõhine

vali 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

Funktsionaalne

tellimuse korraldatud 'Tellitud, kuid mitte tarnitud' (Toode p) = 
    GROUP SUM kogus(OrderDetail d) IF product(d) = p;
tellida 'Tellimusele' (Toode p) = orderedNotShipped(p) + reorderLevel(p) - unitsInStock(p);
SELECT companyName(supplier(Product p)), name(p), toOrder(p) WHERE toOrder(p) > 0;

Täpne ülesanne

Ja viimane näide isiklikult minult. On sotsiaalvõrgustiku loogika. Inimesed võivad olla sõbrad ja meeldida üksteisele. Funktsionaalsete andmebaaside seisukohalt näeb see välja järgmiselt:

KLASS Inimene;
meeldib = ANDMED VAHELIKK (Inimene, Inimene);
sõbrad = ANDMED VAHELIKK (Inimene, Inimene);

On vaja leida võimalikke sõbrakandidaate. Täpsemalt, tuleb leida kõik inimesed A, B, C, selliselt, et A on sõber B-ga, B on sõber C-ga, A meeldib C-le, kuid A ei ole sõber C-ga.
Funktsionaalsete andmebaaside seisukohalt näeb päring välja järgmiselt:

VALI Isik a, Isik b, Isik c KUS MUU 
    meeldib(a, c) JA EI SÕBRAD(a, c) JA 
    sõbrad(a, b) JA sõbrad(b, c);

Lugejale pakutakse iseseisvalt selle SQL ülesande lahendamist. Eeldatakse, et sõpru on oluliselt vähem kui neid, kellele meeldib. Seetõttu asuvad nad erinevates tabelites. Edukal lahendamisel on ka ülesanne kahe tähega. Selles ei ole sõprus sümmeetriline. Funktsionaalses andmebaasis näeb see välja nii:

VALI Isik a, Isik b, Isik c KUS MUU 
    meeldib(a, c) JA EI SÕBRAD(a, c) JA 
    (sõbrad(a, b) VÕI sõbrad(b, a)) JA 
    (sõbrad(b, c) VÕI sõbrad(c, b));

UPD: esimese ja teise tähega ülesande lahenduse autor dss_kalika:

SELECT 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
FROM Persons                 AS p
--Meeldimised                      
JOIN PersonRelationShip      AS pl ON pl.PersonAID = p.PersonID
                                  AND pl.Relation  = 'Like'
--Sõbrad                     
JOIN PersonRelationShip      AS pf ON pf.PersonAID = p.PersonID 
                                  AND pf.Relation = 'Friend'
--Sõprade sõbrad              
JOIN PersonRelationShip      AS pff ON pff.PersonAID = pf.PersonBID
                                   AND pff.PersonBID = pl.PersonBID
                                   AND pff.Relation = 'Friend'
--Ei ole veel sõbrad         
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
--Meeldimised                      
JOIN PersonRelationShipCollapsed  AS pl ON pl.PersonAID = p.PersonID
                                 AND pl.Relation  = 'Like'                                  
--Sõbrad                          
JOIN PersonRelationShipCollapsed  AS pf ON pf.PersonAID = p.PersonID 
                                 AND pf.Relation = 'Friend'
--Sõprade Sõbrad                   
JOIN PersonRelationShipCollapsed  AS pff ON pff.PersonAID = pf.PersonBID
                                 AND pff.PersonBID = pl.PersonBID
                                 AND pff.Relation = 'Friend'
--Jätkuvalt mitte sõbrad                   
LEFT JOIN PersonRelationShipCollapsed AS pnf ON pnf.PersonAID = p.PersonID
                                   AND pnf.PersonBID = pff.PersonBID
                                   AND pnf.Relation = 'Friend'
WHERE pnf.[PersonAID] IS NULL 

Kokkuvõte

Oluline on märkida, et antud keele süntaks on vaid üks võimalus ladustatud kontseptsiooni elluviimiseks. Aluseks on võetud SQL, ja eesmärk oli, et see oleks sellele võimalikult lähedane. Loomulikult võivad kellegile mitte meeldida võtmesõnade nimed, sõnade suurfirmad jpm. Siin on oluline just kontseptsioon ise. Soovi korral võib C++-i ja Pythonit sarnase süntaksiga teha.

Kirjeldatud andmebaasi kontseptsioonil on minu arvates järgmised eelised:

  • Lihtsus. See on suhteliselt subjektiivne näitaja, mis ei ole üldiselt ilmne lihtsates juhtumites. Kuid kui vaadata keerulisemaid juhtumeid (näiteks täheülesandeid), siis minu arvates on selliste päringute kirjutamine oluliselt lihtsam.
  • Encapsulation. Mõnedes näidetes olen kuulutanud vahefunktsioone (näiteks müüa, osta jne), millest ehitati üles järgmised funktsioonid. See võimaldab vajadusel teatud funktsioonide loogikat muuta, ilma et see mõjutaks nendest sõltuvaid loogikaid. Näiteks võib müük müüa tulema täiesti teistest objektidest, samas kui ülejäänud loogika jääb muutumatuks. Jah, RDBMS-is saab seda rakendada CREATE VIEW abil. Kuid kui kogu loogikat kirjutada sedasi, siis näeb see välja mitte eriti loetav.
  • Semantilise lõhe puudumine. Selline andmebaas opereerib funktsioonide ja klasside abil (lauad ja väljad asemel). Just nagu klassikalises programmeerimises (kui arvata, et meetod on funktsioon, mille esimene parameeter on klass, kuhu see kuulub). Seega peaks „sõbrunemine“ universaalsete programmeerimiskeeltega olema palju lihtsam. Lisaks võimaldab see kontseptsioon rakendada palju keerukamaid funktsioone. Näiteks on võimalik andmebaasi integreerida järgmiste tüüpi operaatoreid:

    PIIRANG sold(Employee e, 1, 2019) > 100 IF name(e) = 'Petya' MESSAGE 'Petya müüb 2019. aastal liiga palju ühte kaupa';

  • Pärimine ja polümorfism. Funktsionaalses andmebaasis on võimalik rakendada mitme pärimisega konstruktsioone, nagu CLASS ClassP: Class1, Class2, ning teostada mitme polümorfismi. Kuidas täpselt, võib-olla kirjutan järgnevates artiklites.

Kuigi see on ainult kontseptsioon, on meil juba teatud rakendus Java-s, mis tõlgendab kogu funktsionaalset loogikat relationaalseks loogikaks. Lisaks on sellele kenasti lisatud esitluse loogika ja palju muud, mistõttu on tulemuseks täies mahus platvorm. Sisuliselt kasutame RDBMS-i (moment väidame, et ainult PostgreSQL) kui „virtuaalmasinat“. Sellise üleviimise korral tekivad mõnikord probleemid, kuna RDBMS-i päringute optimeerija ei tea teatud statistikat, mida FSUBD teab. Teoorias on võimalik rakendada andmebaasihaldussüsteem, mis kasutab salvestusena struktuuri, mis on kohandatud just funktsionaalsele loogikale.

Allikas: habr.com

Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid 🔥 Osta usaldusväärne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster