Andmebaasimaailma on pikka aega üle võtnud relatsioonilised DBMS-id, mis kasutavad SQL-keelt. Nii palju, et esilekerkivaid sorte nimetatakse NoSQL-iks. Neil õnnestus sellel turul endale kindel koht võita, kuid relatsiooniline DBMS ei sure ja seda kasutatakse jätkuvalt aktiivselt oma eesmärkidel.
Selles artiklis tahan kirjeldada funktsionaalse andmebaasi kontseptsiooni. Parema mõistmise huvides teen seda klassikalise suhtemudeliga võrreldes. Näidetena kasutame ülesandeid erinevatest Internetist leitud SQL-testidest.
Sissejuhatus
Relatsiooniandmebaasid töötavad tabelitel ja väljadel. Funktsionaalses andmebaasis kasutatakse selle asemel klasse ja funktsioone. N võtmega tabeli väli esitatakse N parameetri funktsioonina. Tabelitevaheliste linkide asemel kasutatakse funktsioone, mis tagastavad selle klassi objekte, kuhu link läheb. Funktsioonide koostist kasutatakse asemel JOIN.
Enne otse ülesannete juurde asumist kirjeldan domeeniloogika ülesannet. DDL-i jaoks kasutan PostgreSQL-i süntaksit. Funktsionaalsuse jaoks oma süntaks.
Lauad ja väljad
Lihtne Sku objekt nime- ja hinnaväljadega:
suhteline
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
Funktsionaalne
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
Anname teada kaks funktsioonid, mis võtavad sisendiks ühe Sku parameetri ja tagastavad primitiivse tüübi.
Eeldatakse, et funktsionaalses DBMS-is on igal objektil mingi sisemine kood, mis genereeritakse automaatselt ja millele saab vajadusel juurde pääseda.
Määrame tootele/poele/tarnijale hinna. See võib aja jooksul muutuda, seega lisame tabelisse ajavälja. Koodi lühendamiseks jätan vahele relatsiooniandmebaasi kataloogide tabelite deklareerimise:
suhteline
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
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
Indeksid
Viimase näite puhul koostame kõikidele võtmetele ja kuupäevale indeksi, et saaksime kiiresti leida teatud aja hinna.
suhteline
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Funktsionaalne
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
ülesanded
Alustame suhteliselt lihtsatest ülesannetest, mis on võetud vastavast
Esmalt deklareerime domeeniloogika (relatsiooniandmebaasi puhul tehakse seda otse ülaltoodud artiklis).
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. ülesanne
Kuvage nimekiri töötajatest, kes saavad suuremat palka kui vahetu juhi oma.
suhteline
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Funktsionaalne
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
1.2. ülesanne
Kuvage oma osakonna kõrgeimat palka teenivate töötajate loend
suhteline
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
Funktsionaalne
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));
Mõlemad teostused on samaväärsed. Esimesel juhul saab relatsiooniandmebaasis kasutada CREATE VIEW-i, mis samamoodi arvutab esmalt konkreetse osakonna maksimumpalga. Edaspidi kasutan selguse huvides esimest juhtumit, kuna see kajastab lahendust paremini.
1.3. ülesanne
Kuvage osakonna ID-de loend, mille töötajate arv ei ületa 3 inimest.
suhteline
select department_id
from employee
group by department_id
having count(*) <= 3
Funktsionaalne
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
1.4. ülesanne
Kuvage nimekiri töötajatest, kellel ei ole samas osakonnas töötavat juhti.
suhteline
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
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
1.5. ülesanne
Leidke osakonna ID-de loend, millel on maksimaalne töötaja kogupalk.
suhteline
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
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();
Liigume teisest keerulisemate ülesannete juurde
2.1. ülesanne
Millised müüjad müüsid 1997. aastal rohkem kui 30 tükki kaupa nr 1?
Domeeniloogika (nagu varem, jätame RDBMS-i deklaratsiooni vahele):
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);
suhteline
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
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. ülesanne
Leidke iga kliendi (eesnimi, perekonnanimi) kohta kaks eset (nimi), millele klient 1997. aastal kõige rohkem raha kulutas.
Domeeniloogika laiendamine eelmisest näitest:
CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
suhteline
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));
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;
Operaator PARTITSIOON töötab järgmisel põhimõttel: summeerib SUM (siin 1) järel määratud avaldise määratud rühmade piires (siin Klient ja Aasta, kuid võib olla mis tahes avaldis), sorteerides rühmade sees vastavalt ORDER määratud avaldistele ( siit ostetud ja kui need on võrdsed, siis sisemise tootekoodi järgi).
2.3. ülesanne
Kui palju kaupu tuleb tarnijatelt tellida jooksvate tellimuste täitmiseks.
Laiendame uuesti domeeniloogikat:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
suhteline
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
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;
Ülesanne tärniga
Ja viimane näide on minult isiklikult. Seal on sotsiaalvõrgustiku loogika. Inimesed võivad olla üksteisega sõbrad ja üksteisele meeldida. Funktsionaalse andmebaasi vaatenurgast näeks see välja järgmine:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
Sõpruse jaoks on vaja leida võimalikud kandidaadid. Ametlikumalt peate leidma kõik inimesed A, B ja C nii, et A oleks B-ga sõber ja B oleks C-ga sõber, A-le meeldib C, kuid A-le ei meeldi C.
Funktsionaalse andmebaasi seisukohast näeks päring välja järgmine:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
Lugejal palutakse seda ülesannet SQL-is iseseisvalt lahendada. Eeldatakse, et sõpru on palju vähem kui neid, kellele meeldib. Seetõttu on need eraldi tabelites. Eduka lahenduse korral on probleem ka kahe tärniga. Tema sõprus ei ole sümmeetriline. Funktsionaalses andmebaasis näeks see välja järgmine:
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: ülesande lahendus esimese ja teise tärniga alates
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
Järeldus
Tuleb märkida, et ülaltoodud keele süntaks on vaid üks ülaltoodud kontseptsiooni rakendamise võimalustest. Aluseks võeti just SQL ja eesmärk oli muuta see sellega võimalikult sarnaseks. Muidugi ei pruugi kellelegi meeldida märksõnade nimed, sõnade suurtähted ja nii edasi. Peamine on siin kontseptsioon ise. Soovi korral saab nii C ++ kui ka Pythoni süntaksi teha sarnaseks.
Kirjeldatud andmebaasi kontseptsioonil on minu arvates järgmised eelised:
- Lihtsus. See on suhteliselt subjektiivne näitaja, mis pole lihtsatel juhtudel ilmne. Aga kui vaadata keerulisemaid juhtumeid (näiteks tärnidega ülesandeid), siis minu arvates on selliste päringute kirjutamine palju lihtsam.
- Инкапсуляция. Mõnes näites deklareerisin vahefunktsioone (näiteks müüdud, ostnud jne), millest ehitati üles järgnevad funktsioonid. See võimaldab vajadusel muuta teatud funktsioonide loogikat, muutmata nendest sõltuvate funktsioonide loogikat. Näiteks saate teha müüki müüdud arvutati täiesti erinevate objektide põhjal, samas kui ülejäänud loogika ei muutu. Jah, RDBMS-is saab seda teha kasutades CREATE VIEW. Aga kui kogu loogika niimoodi kirja panna, siis ei näe see eriti loetav välja.
- Semantilist lõhet pole. Selline andmebaas töötab funktsioonide ja klassidega (tabelite ja väljade asemel). Samamoodi nagu klassikalises programmeerimises (eeldusel, et meetod on funktsioon, mille esimene parameeter on selle klassi kujul, kuhu ta kuulub). Sellest lähtuvalt peaks universaalsete programmeerimiskeeltega “sõpru looma” olema palju lihtsam. Lisaks võimaldab see kontseptsioon rakendada palju keerukamaid funktsioone. Näiteks saate andmebaasi manustada selliseid avaldusi:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Pärand ja polümorfism. Funktsionaalses andmebaasis saate CLASS ClassP: Class1, Class2 konstruktsioonide kaudu juurutada mitut pärandit ja rakendada mitut polümorfismi. Kuidas täpselt, kirjutan võib-olla järgmistes artiklites.
Kuigi see on vaid kontseptsioon, on meil Java-s juba mõni rakendus, mis muudab kogu funktsionaalse loogika relatsiooniloogikaks. Lisaks on esinduste loogika ja palju muud ilusasti külge keeratud, tänu millele saame terviku
Allikas: www.habr.com