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 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 . 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 :
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 . 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
