Duomenų bazių pasaulyje jau seniai dominavo reliacinės DBVS, kurios naudoja SQL kalbą. Tiek, kad atsirandantys variantai vadinami NoSQL. Jie sugebėjo išsiskirti tam tikrą vietą šioje rinkoje, tačiau reliacinės DBVS nemirs ir toliau bus aktyviai naudojamos savo tikslams.
Šiame straipsnyje noriu apibūdinti funkcinės duomenų bazės sąvoką. Kad geriau suprasčiau, tai padarysiu palygindamas su klasikiniu reliaciniu modeliu. Kaip pavyzdžiai bus panaudotos problemos iš įvairių internete rastų SQL testų.
įvedimas
Reliacinės duomenų bazės veikia lentelėse ir laukuose. Funkcinėje duomenų bazėje bus naudojamos atitinkamai klasės ir funkcijos. Lentelės laukas su N klavišais bus pavaizduotas kaip N parametrų funkcija. Vietoj ryšių tarp lentelių bus naudojamos funkcijos, kurios grąžina klasės, su kuria užmezgamas ryšys, objektus. Vietoj JOIN bus naudojama funkcijos sudėtis.
Prieš pereinant tiesiai prie užduočių, aprašysiu domeno logikos užduotį. DDL naudosiu PostgreSQL sintaksę. Funkcionaliems jis turi savo sintaksę.
Lentelės ir laukai
Paprastas Sku objektas su pavadinimo ir kainos laukais:
Santykinis
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
Funkcionalus
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
Skelbiame du функции, kurie ima vieną parametrą Sku kaip įvestį ir grąžina primityvų tipą.
Daroma prielaida, kad funkcinėje DBVS kiekvienas objektas turės tam tikrą vidinį kodą, kuris yra automatiškai generuojamas ir gali būti pasiekiamas, jei reikia.
Nustatykime prekės/parduotuvės/tiekėjo kainą. Laikui bėgant jis gali keistis, todėl į lentelę įtraukime laiko lauką. Praleisiu katalogų lentelių deklaravimą reliacinėje duomenų bazėje, kad sutrumpinčiau kodą:
Santykinis
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)
)
Funkcionalus
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
Indeksai
Paskutiniame pavyzdyje mes sukursime visų raktų ir datos indeksą, kad galėtume greitai rasti kainą konkrečiam laikui.
Santykinis
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Funkcionalus
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
užduotys
Pradėkime nuo gana paprastų problemų, paimtų iš atitinkamų
Pirmiausia deklaruokime domeno logiką (reliacinės duomenų bazės atveju tai daroma tiesiogiai aukščiau esančiame straipsnyje).
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 užduotis
Parodykite sąrašą darbuotojų, kurie gauna didesnį atlyginimą nei jų tiesioginis vadovas.
Santykinis
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Funkcionalus
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
1.2 užduotis
Išvardykite darbuotojus, kurie savo skyriuje gauna maksimalų atlyginimą
Santykinis
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
Funkcionalus
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));
Abu įgyvendinimai yra lygiaverčiai. Pirmuoju atveju reliacinėje duomenų bazėje galite naudoti CREATE VIEW, kuri tuo pačiu būdu pirmiausia apskaičiuos maksimalų atlyginimą konkrečiam joje esančiam padaliniui. Toliau, siekiant aiškumo, naudosiu pirmąjį atvejį, nes jis geriau atspindi sprendimą.
1.3 užduotis
Rodyti skyrių ID sąrašą, kuriame darbuotojų skaičius neviršija 3 žmonių.
Santykinis
select department_id
from employee
group by department_id
having count(*) <= 3
Funkcionalus
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
1.4 užduotis
Rodyti darbuotojų, kurie neturi paskirto vadovo, dirbančio tame pačiame skyriuje, sąrašą.
Santykinis
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
Funkcionalus
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
1.5 užduotis
Raskite skyrių ID sąrašą su maksimaliu bendru darbuotojo atlyginimu.
Santykinis
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 )
Funkcionalus
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();
Pereikime prie sudėtingesnių užduočių nuo kitos
2.1 užduotis
Kurie pardavėjai 1997 m. pardavė daugiau nei 30 vienetų prekės Nr. 1?
Domeno logika (kaip ir anksčiau RDBMS, mes praleidžiame deklaraciją):
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);
Santykinis
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
Funkcionalus
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 užduotis
Kiekvienam pirkėjui (vardas, pavardė) suraskite dvi prekes (pavadinimą), kurioms 1997 metais pirkėjas išleido daugiausia pinigų.
Išplečiame domeno logiką iš ankstesnio pavyzdžio:
CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
Santykinis
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
Funkcionalus
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;
PARTITION operatorius veikia tokiu principu: susumuoja po SUM nurodytą išraišką (čia 1), nurodytose grupėse (čia Klientas ir Metai, bet gali būti bet kokia išraiška), rūšiuoja grupėse pagal UŽSAKYME nurodytas išraiškas ( čia pirktas, o jei lygus, tai pagal vidinį prekės kodą).
2.3 užduotis
Kiek prekių reikia užsakyti iš tiekėjų, kad būtų įvykdyti esami užsakymai.
Dar kartą išplėskime domeno logiką:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
Santykinis
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
Funkcionalus
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;
Problema su žvaigždute
Ir paskutinis pavyzdys yra iš manęs asmeniškai. Yra socialinio tinklo logika. Žmonės gali draugauti vieni su kitais ir patikti vienas kitam. Iš funkcinės duomenų bazės perspektyvos tai atrodytų taip:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
Būtina rasti galimus kandidatus į draugystę. Kalbant formaliau, reikia surasti visus žmones A, B, C taip, kad A draugautų su B, o B draugautų su C, A mėgtų C, bet A nedraugautų su C.
Iš funkcinės duomenų bazės perspektyvos užklausa atrodytų taip:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
Skaitytojas raginamas savarankiškai išspręsti šią problemą SQL. Manoma, kad draugų yra daug mažiau nei jums patinkančių žmonių. Todėl jie yra atskirose lentelėse. Jei pavyks, taip pat yra užduotis su dviem žvaigždutėmis. Joje draugystė nėra simetriška. Funkcinėje duomenų bazėje tai atrodytų taip:
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: problemos sprendimas su pirmąja ir antra žvaigždute nuo
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
išvada
Pažymėtina, kad pateikta kalbos sintaksė yra tik vienas iš variantų, kaip įgyvendinti pateiktą koncepciją. SQL buvo laikomas pagrindu ir buvo siekiama, kad jis būtų kuo panašesnis į jį. Žinoma, kai kam gali nepatikti raktinių žodžių pavadinimai, žodžių registrai ir pan. Svarbiausia čia yra pati koncepcija. Jei norite, galite sukurti panašią C++ ir Python sintaksę.
Mano nuomone, aprašyta duomenų bazės koncepcija turi šiuos privalumus:
- palengvinti. Tai gana subjektyvus rodiklis, kuris paprastais atvejais nėra akivaizdus. Bet jei pažvelgsite į sudėtingesnius atvejus (pavyzdžiui, problemas su žvaigždutėmis), tada, mano nuomone, rašyti tokias užklausas yra daug lengviau.
- Inkubacija. Kai kuriuose pavyzdžiuose deklaravau tarpines funkcijas (pvz. parduodami, nusipirkau ir kt.), iš kurių buvo kuriamos tolesnės funkcijos. Tai leidžia, jei reikia, keisti tam tikrų funkcijų logiką, nekeičiant nuo jų priklausančių funkcijų logikos. Pavyzdžiui, galite parduoti parduodami buvo skaičiuojami iš visiškai skirtingų objektų, o likusi logika nepasikeis. Taip, tai galima įdiegti RDBMS naudojant CREATE VIEW. Bet jei visa logika taip parašyta, tai atrodys nelabai įskaitoma.
- Nėra semantinio atotrūkio. Tokia duomenų bazė veikia pagal funkcijas ir klases (vietoj lentelių ir laukų). Kaip ir klasikiniame programavime (jeigu metodas yra funkcija su pirmuoju parametru klasės, kuriai jis priklauso, forma). Atitinkamai, „susidraugauti“ su universaliomis programavimo kalbomis turėtų būti daug lengviau. Be to, ši koncepcija leidžia įgyvendinti daug sudėtingesnes funkcijas. Pavyzdžiui, galite įterpti tokius operatorius:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Paveldėjimas ir polimorfizmas. Funkcinėje duomenų bazėje galite įvesti daugialypį paveldėjimą per CLASS ClassP: Class1, Class2 konstrukcijas ir įgyvendinti daugialypį polimorfizmą. Kaip tiksliai, tikriausiai parašysiu kituose straipsniuose.
Nors tai tik koncepcija, mes jau turime tam tikrą „Java“ diegimą, kuris visą funkcinę logiką paverčia reliacine logika. Be to, prie jo gražiai pritvirtinta reprezentacijų logika ir daug kitų dalykų, kurių dėka gauname visumą
Šaltinis: www.habr.com