Andmebaasid on kaua olnud relatsiooniliste andmebaaside käes, kus kasutatakse SQL-i keelt. Nii tugev on see mõju, et ilmnevad uued variandid, mida nimetatakse NoSQL-iks. Need on suutnud endale teatud koha turul kindlustada, kuid relatsioonilised andmebaasid ei plaani kaduda ja jätkavad aktiivset kasutamist oma eesmärkide jaoks.
Selles artiklis soovin ma tutvustada funktsionaalse andmebaasi kontseptsiooni. Parema arusaamise nimel teen seda, võrreldes seda klassikalise relatsioonilise mudeliga. Näidetena kasutatakse erinevate SQL-testide ülesandeid, mis leitud internetis.
Sissejuhatus
Relatsioonilised andmebaasid töötavad tabelite ja väljadega. Funktsionaalses andmebaasis kasutatakse nende asemel klasse ja funktsioone. Väli tabelis N võtmega esindatakse funktsioonina, millel on N parameetrit. Tabelite vaheliste seoste asemel kasutatakse funktsioone, mis tagastavad klassi objekti, millele seos osutab. JOINi asemel kasutatakse funktsioonide kompositsiooni.
Enne kui liikuda otse ülesannete juurde, kirjeldan domeeniloogika ülesannet. DDL-i jaoks kasutan PostgreSQL-i süntaksit. Funktsionaalse jaoks oma süntaksit.
Tabelid ja väljad
Lihtne objekt Sku väljadega nimetus ja hind:
Relatsiooniline
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 = ANDME STRING[100] (Sku);
hind = ANDME NUMERIC[10,5] (Sku);
Me kuulutame välja kaks funktsioone, mis võtavad ühe parameetri Sku ja tagastavad primitiivse tüübi.
Eeldatakse, et funktsionaalses andmebaasis on igal objektil mingi sisemine kood, mis genereeritakse automaatselt ja millele saab vajadusel viidata.
Seame toote / poe / tarnija hinna. See võib aja jooksul muutuda, seega lisame tabelisse väljad ajaks. Jätan relatsioonilise andmebaasi soovituslikud tabelite väljakuulutamised vahele koodi lühendamiseks:
Relatsiooniline
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 Kauplus;
KLASS Tarnija;
kuupäevAeg = ANDMED AEG (Sku, Kauplus, Tarnija);
hind = ANDMED NUMERIK[10,5] (Sku, Kauplus, Tarnija);
Indeksid
Viimase näite jaoks loome indeksi kõikide võtmete ja kuupäeva järgi, et saaksime kiiresti leida hinna teatud aja hetkel.
Relatsiooniline
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Funktsionaalne
INDEX Sku sk, Kauplus st, Tarnija sp, dateTime(sk, st, sp);
Ülesanded
Alustame suhteliselt lihtsatest ülesannetest, mis on võetud vastavast Habrist.
Esiteks kuulutame välja domeeniloogika (releeriva andmebaasi jaoks on see tehtud otse artikelis).
Klass Osakond;
nimi = ANDMED STRING[100] (Osakond);
Klass Employee;
osakond = ANDMED Department (Employee);
juht = ANDMED Employee (Employee);
nimi = ANDMED STRING[100] (Employee);
palk = ANDMED NUMERIC[14,2] (Employee);
Ülesanne 1.1
Väljastage töötajate nimekiri, kelle palk on suurem kui otseülemal.
Relatsiooniline
vali a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Funktsionaalne
VALI name(Töötaja a) KUS salary(a) > salary(pealik(a));
Ülesanne 1.2
Väljastage töötajate nimekiri, kellel on oma osakonnas kõrgeim palk.
Relatsiooniline
vali a.*
from employee a
where a.salary = ( vali max(salary) from employee b
where b.department_id = a.department_id )
Funktsionaalne
maxSalary 'Maksimaalne palk' (Osakond s) =
GROUP MAX palk(Töötaja e) KUI osakond(e) = s;
SELECT name(Töötaja a) KUS palk(a) = maxSalary(osakond(a));
// или если "заинлайнить"
VALI nimi(Employee a) KUS
palk(a) = maxSalary(GROUP MAX palk(Employee e) KUI osakond(e) = osakond(a));
Mõlemad teostused on ekvivalentsed. Esimese juhtumi jaoks saab releerivas andmebaasis kasutada CREATE VIEW, mis arvutab samamoodi esmalt konkreetse osakonna maksimaalse palga. Edasi lisan ma selguse huvides esimese juhtumi, kuna see peegeldab paremini lahendust.
Ülesanne 1.3
Väljastage osakondade ID-d, kus töötajate arv ei ületa 3 inimest.
Relatsiooniline
vali department_id
from employee
group by department_id
having count(*) <= 3
Funktsionaalne
countEmployees 'Töötajate arv' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
VALI Department d KUS countEmployees(d) <= 3;
Ülesanne 1.4
Väljastage töötajate nimekiri, kellel pole välja määratud juhti, kes töötab samas osakonnas.
Relatsiooniline
vali 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 nimi(Töötaja a) KUS EI ole (department(juhataja(a)) = department(a));
Ülesanne 1.5
Leidke osakondade ID-d, mille töötajate kogupalk on maksimaalne.
Relatsiooniline
with sum_salary as
( vali department_id, sum(salary) palk
from employee
group by department_id )
vali department_id
from sum_salary a
where a.salary = ( vali max(salary) from sum_salary )
Funktsionaalne
palkasumma 'Maksimaalne palk' (Osakond d) =
GROUP SUM salary(Employee e) IF department(e) = d;
maxPalkSum 'Maksimaalne palk osakondade kaupa' () =
GRUPI MAX palkasumma(Osakond d);
VALI Osakond d KUS palkasumma(d) = maxPalkSum();
Liigume keerukamate ülesannete juurde teisest . Seal on üksikasjalik ülevaade sellest, kuidas seda ülesannet MS SQL-is rakendada.
Ülesanne 2.1
Millised müüjad müüsid 1997. aastal rohkem kui 30 tükki toodet nr 1?
Domeeniloogika (nagu ennem RDBS-is, jätame kuulutuse vahele):
CLASS Employee 'Müüja';
lastName 'Perekonnanimi' = DATA STRING[100] (Employee);
Klass Product 'Toode';
id = ANDMED INTEGER (Product);
nimi = ANDMED STRING[100] (Product);
Klass Tellimus 'Tellimus';
kuupäev = ANDMED DATE (Order);
töötaja = ANDMED Employee (Order);
Klass Detail 'Tellimuse rida';
tellimus = ANDMED Order (Detail);
toode = ANDMED Product (Detail);
kogus = ANDMED NUMERIC[10,5] (Detail);
Relatsiooniline
vali Perekonnanimi
from Employees as e
where (
vali sum(od.Quantity)
from [Order Details] as od
where od.ProductID = 1 and od.OrderID in (
vali 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 aasta) =
RÜHMA SUMMA kogus(TellimusDetail d) KUI
töötaja(tellimus(d)) = e JA
id(toode(d)) = tooteId JA
aasta(käibedatum(tellimus(d))) = aasta;
VALI perekonnanimi(Töötaja e) KUS müüdud(e, 1, 1997) > 30;
Ülesanne 2.2
Iga kliendi (nimi, perekonnanimi) jaoks leidke kaks toodet (nimetust), millele klient kulutas 1997. aastal kõige rohkem raha.
Kohandame eelnevas näites domeeniloogikat:
CLASS Customer 'Klient';
contactName 'Eesnimi' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
Relatsiooniline
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 'Ostis' (Klient c, Toode p, KOGUS y) =
RÜHM SUM sum(Detaail d) KUI
klient(keerake(d)) = c JA
toode(d) = p JA
extractYear(date(order(d))) = y;
hinnang 'Hinnang' (Klient c, Toode p, KOGUS y) =
PARTITION SUM 1 ORDER DESC bought(c, p, y), p BY c, y;
VALI kontaktNimi(Klient c), nimi(Toode p) KUIDAS hinnang(c, p, 1997) < 3;
PARTITION operator works as follows: it sums the expression specified after SUM (here 1), within the specified groups (here Customer and Year, but it can be any expression), sorting within groups according to the expressions specified in ORDER (here bought, and if equal, by the internal product code).
Task 2.3
How many products need to be ordered from suppliers to fulfill current orders.
Again, we are expanding the domain logic:
KLASS Tarnija 'Tarnija';
companyName = ANDMED STRING[100] (Tarnija);
supplier = DATA Supplier (Product);
unitsInStock 'Stock remaining' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Sales norm' = DATA NUMERIC[10,3] (Product);
Relatsiooniline
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
Funktsionaalne
tellitud, kuid mitte saadetud (Toode p) =
GRUPI SUMMA kogus(OrderDetail d) KUI toode(d) = p;
tellida 'Tellimiseks' (Toode p) = tellitud(p) + uue tellimise tase(p) - laos olevad üksused(p);
VALI ettevõtte nimi(tootja(Toode p)), nimi(p), tellida(p) KUS tellida(p) > 0;
Star task
And finally a personal example from me. There is a social network logic. People can be friends with each other and can like each other. From the perspective of a functional database, it would look like this:
CLASS Inimene;
meeldib = ANDMED BOOLEAN (Inimene, Inimene);
sõbrad = ANDMED BOOLEAN (Inimene, Inimene);
It is necessary to find possible candidates for friendship. More formally, it is required to find all people A, B, C such that A is friends with B, and B is friends with C, A likes C, but A is not friends with C.
From the perspective of a functional database, the query would look like this:
VALI Inimene a, Inimene b, Inimene c KUS
meeldib(a, c) JA EI ole sõbrad(a, c) JA
sõbrad(a, b) JA sõbrad(b, c);
The reader is invited to solve this SQL task on their own. It is assumed that friends are much fewer than those who are liked. Therefore, they are placed in separate tables. In case of a successful solution, there is also a task with two stars. In it, friendship is not symmetrical. From the perspective of a functional database, it would look like this:
VALI Inimene a, Inimene b, Inimene c KUS
meeldib(a, c) JA EI ole 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: solution to the task with the first and second stars from :
VALI
pl.PersonAID
,pf.PersonAID
,pff.PersonAID
KUST VALITUD INIMESTE TABELIST AS p
--Meeldimised
LIITU InimeseSuhe AS pl ON pl.PersonAID = p.PersonID
JA pl.Suhe = 'Meeldimine'
--Sõbrad
LIITU InimeseSuhe AS pf ON pf.PersonAID = p.PersonID
JA pf.Suhe = 'Sõber'
--Sõprade Sõbrad
LIITU InimeseSuhe AS pff ON pff.PersonAID = pf.PersonBID
JA pff.PersonBID = pl.PersonBID
JA pff.Suhe = 'Sõber'
--Pole veel sõbrad
VASAKLIIT InimeseSuhe AS pnf ON pnf.PersonAID = p.PersonID
JA pnf.PersonBID = pff.PersonBID
JA pnf.Suhe = 'Sõber'
KUS pnf.PersonAID ON NULL
;KOOS PersonSuheKoondatud KUI (
VALI pl.PersonAID
,pl.PersonBID
,pl.Suhe
KUST #PersonSuhe AS pl
ÜHENDUS
VALI pl.PersonBID KUI PersonAID
,pl.PersonAID KUI PersonBID
,pl.Suhe
KUST #PersonSuhe AS pl
)
VALI
pl.PersonAID
,pf.PersonBID
,pff.PersonBID
KUST #Inimesed AS p
--Meeldimised
LIITU PersonSuheKoondatud AS pl ON pl.PersonAID = p.PersonID
JA pl.Suhe = 'Meeldimine'
--Sõbrad
LIITU PersonSuheKoondatud AS pf ON pf.PersonAID = p.PersonID
JA pf.Suhe = 'Sõber'
--Sõprade Sõbrad
LIITU PersonSuheKoondatud AS pff ON pff.PersonAID = pf.PersonBID
JA pff.PersonBID = pl.PersonBID
JA pff.Suhe = 'Sõber'
--Pole veel sõbrad
VASAKLIIT PersonSuheKoondatud AS pnf ON pnf.PersonAID = p.PersonID
JA pnf.PersonBID = pff.PersonBID
JA pnf.Suhe = 'Sõber'
KUS pnf.[PersonAID] ON NULL
Kokkuvõte
Tasub märkida, et antud keelestrimni süntaks on vaid üks võimalus antud kontseptsiooni rakendamiseks. Aluseks on võetud just SQL, et muuta see võimalikult sarnaseks. Loomulikult võivad mõned inimesed olla rahulolematu märksõnade nimetuse, sõnade registreerimise ja muu üle. Peamine on siin kontseptsioon. Soovi korral on võimalik luua ka C++, ja Python'i sarnane süntaks.
Kirjeldatud andmebaasi kontseptsioonil on minu arvates järgmised eelised:
- Lihtsus. See on suhteliselt subjektiivne näitaja, mis ei ole lihtsates olukordades ilmne. Kuid kui vaadata keerulisemaid olukordi (näiteks täheülesanne), siis on minu arvates selliste päringute kirjutamine oluliselt lihtsam.
- Kaapseldamine. Mõnedes näidetes olen kuulutanud vahefunktsioone (näiteks, müüdud, ostetud jne), millest järgnevad funktsioonid sõltuvad. See võimaldab vajadusel teatud funktsioonide loogikat muuta, mõjutamata sõltuvaid funktsioone. Näiteks on võimalik korraldada, et müügid müüdud peeti teistest objektidest, samal ajal kui ülejäänud loogika ei muutu. Jah, RDBMS-is on seda võimalik ellu viia kasutades CREATE VIEW. Kuid kui kogu loogika nii kirjutada, siis ei ole see väga loetav.
- Semantilise lõhe puudumine. Selline andmebaas opereerib funktsioonide ja klassidega (lauad ja väljad asemel). Just nagu klassikalises programmeerimises (kui arvestada, et meetod on funktsioon, mille esimeseks parameetriks on klass, mille juurde see kuulub). Seega, „sõbrustada“ universaalsete programmeerimiskeeltega peaks oluliselt lihtsam olema. Lisaks võimaldab see kontseptsioon rakendada palju keerukamaid funktsioone. Näiteks saab andmebaasi integreerida järgmisi tüüpi operaatorid:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Petja' MESSAGE 'Midagi Petja müüb 2019. aastal liiga palju ühte toodet;'}
- Pärimine ja polimorfism. Funktsionaalses andmebaasis saab mitmekordset pärimist tutvustada struktuuridega CLASS ClassP: Class1, Class2 ja rakendada mitmekordset polimorfismi. Kuidas täpselt, võib-olla kirjutan järgmistes artiklites.
Kuigi see on vaid kontseptsioon, on meil juba olemasolev teostus Java-s, mis tõlgib kogu funktsionaalse loogika suhtelise loogikani. Pluss sellele on kaunilt seotud esitluste loogika ja palju muud, mille tõttu saadakse terviklik . Põhimõtteliselt kasutame RDBMS-i (praegu ainult PostgreSQL) kui „virtuaalmasinat“. Sellise tõlke korral esinevad mõnikord probleemid, kuna RDBMS-i küsimuste optimeerija ei tunne teatud statistikat, mida tunneb FDBMS. Teoreetiliselt on võimalik ellu viia andmebaasisüsteem, mis kasutab salvestusena mingit struktuuri, mis on kohandatud just funktsionaalse loogika jaoks.
Allikas: habr.com
