Светом база података већ дуго доминирају релациони ДБМС, који користе СКЛ језик. Толико да се нове варијанте називају НоСКЛ. Успели су да изборе за себе одређено место на овом тржишту, али релациони ДБМС-ови неће умрети, и настављају да се активно користе у своје сврхе.
У овом чланку желим да опишем концепт функционалне базе података. Ради бољег разумевања, урадићу ово тако што ћу га упоредити са класичним релационим моделом. Као примери ће бити коришћени проблеми из различитих СКЛ тестова пронађених на Интернету.
Увод
Релационе базе података раде на табелама и пољима. У функционалној бази података, класе и функције ће се користити уместо њих. Поље у табели са Н кључева биће представљено као функција од Н параметара. Уместо релација између табела, користиће се функције које враћају објекте класе са којом се успоставља веза. Композиција функције ће се користити уместо ЈОИН.
Пре него што пређем директно на задатке, описаћу задатак доменске логике. За ДДЛ користићу ПостгреСКЛ синтаксу. За функционално има своју синтаксу.
Табеле и поља
Једноставан Ску објекат са пољима имена и цене:
Релациона
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
Функционални
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
Најављујемо два funkcije, који узимају један параметар Ску као улаз и враћају примитивни тип.
Претпоставља се да ће у функционалном ДБМС-у сваки објекат имати неки интерни код који се аутоматски генерише и може му се приступити ако је потребно.
Хајде да одредимо цену за производ/продавницу/добављача. Може се променити током времена, па хајде да додамо временско поље у табелу. Прескочићу декларисање табела за директоријуме у релационој бази података да бих скратио код:
Релациона
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)
)
Функционални
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
Индекси
За последњи пример, направићемо индекс за све кључеве и датум тако да можемо брзо да пронађемо цену за одређено време.
Релациона
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Функционални
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
задаци
Почнимо са релативно једноставним проблемима преузетим из одговарајућих
Прво, хајде да декларишемо логику домена (за релациону базу података то се ради директно у горњем чланку).
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
Прикажите листу запослених који примају плату већу од плате њиховог непосредног претпостављеног.
Релациона
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Функционални
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
Задатак 1.2
Наведите запослене који примају максималну плату у свом одељењу
Релациона
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
Функционални
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));
Обе имплементације су еквивалентне. За први случај, у релационој бази података можете користити ЦРЕАТЕ ВИЕВ, који ће на исти начин прво израчунати максималну плату за одређено одељење у њој. У даљем тексту, ради јасноће, користићу први случај, пошто боље одражава решење.
Задатак 1.3
Прикажите листу ИД-ова одељења, број запослених у којима не прелази 3 особе.
Релациона
select department_id
from employee
group by department_id
having count(*) <= 3
Функционални
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
Задатак 1.4
Прикажите листу запослених који немају одређеног менаџера који ради у истом одељењу.
Релациона
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
Функционални
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
Задатак 1.5
Пронађите листу ИД-ова одељења са максималном укупном платом запослених.
Релациона
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 )
Функционални
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();
Пређимо на сложеније задатке из другог
Задатак 2.1
Који продавци су продали више од 1997 јединица производа бр. 30 1. године?
Логика домена (као и раније на РДБМС-у прескачемо декларацију):
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);
Релациона
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
Функционални
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
За сваког купца (име, презиме) пронађите два добра (име) на које је купац потрошио највише новца 1997. године.
Проширујемо логику домена из претходног примера:
CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
Релациона
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
Функционални
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;
Оператор ПАРТИТИОН ради на следећем принципу: сабира израз који је наведен после СУМ (овде 1), у оквиру наведених група (овде Купац и Година, али може бити било који израз), сортирајући унутар група према изразима наведеним у НАРУЏБИ ( овде купљен, а ако је једнак, онда према интерном коду производа).
Задатак 2.3
Колико робе треба наручити од добављача да би се испуниле текуће поруџбине.
Хајде да поново проширимо логику домена:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
Релациона
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
Функционални
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;
Проблем са звездицом
И последњи пример је од мене лично. Постоји логика друштвене мреже. Људи могу бити пријатељи једни са другима и волети једни друге. Из перспективе функционалне базе података то би изгледало овако:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
Неопходно је пронаћи могуће кандидате за пријатељство. Формалније, морате пронаћи све људе А, Б, Ц тако да је А пријатељ са Б, а Б пријатељ са Ц, А воли Ц, али А није пријатељ са Ц.
Из перспективе функционалне базе података, упит би изгледао овако:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
Читалац се подстиче да сам реши овај проблем у СКЛ-у. Претпоставља се да има много мање пријатеља него људи које волите. Стога су у посебним табелама. Ако успе, ту је и задатак са две звездице. У њему пријатељство није симетрично. У функционалној бази података то би изгледало овако:
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));
УПД: решење проблема са првом и другом звездицом од
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
Закључак
Треба напоменути да је дата синтакса језика само једна од опција за имплементацију датог концепта. За основу је узет СКЛ, а циљ је био да му буде што сличнији. Наравно, некима се можда неће допасти називи кључних речи, регистри речи итд. Главна ствар овде је сам концепт. Ако желите, можете направити сличну синтаксу за Ц++ и Питхон.
Описани концепт базе података, по мом мишљењу, има следеће предности:
- ублажити. Ово је релативно субјективан показатељ који није очигледан у једноставним случајевима. Али ако погледате сложеније случајеве (на пример, проблеме са звездицама), онда је, по мом мишљењу, писање таквих упита много лакше.
- Инкапсулациа. У неким примерима сам декларисао средње функције (нпр. продато, купио итд.), од којих су изграђене накнадне функције. Ово вам омогућава да промените логику одређених функција, ако је потребно, без промене логике оних које зависе од њих. На пример, можете остварити продају продато израчунате су из потпуно различитих објеката, док се остатак логике неће променити. Да, ово се може имплементирати у РДБМС помоћу ЦРЕАТЕ ВИЕВ. Али ако је сва логика овако написана, неће изгледати баш читљиво.
- Нема семантичке празнине. Таква база података ради на функцијама и класама (уместо табела и поља). Баш као у класичном програмирању (ако претпоставимо да је метода функција са првим параметром у облику класе којој припада). Сходно томе, требало би да буде много лакше „спријатељити се“ са универзалним програмским језицима. Поред тога, овај концепт омогућава имплементацију много сложеније функционалности. На пример, можете да уградите операторе као што су:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Наслеђивање и полиморфизам. У функционалној бази података можете увести вишеструко наслеђивање кроз ЦЛАСС ЦлассП: Цласс1, Цласс2 конструкције и имплементирати вишеструки полиморфизам. Вероватно ћу писати како тачно у будућим чланцима.
Иако је ово само концепт, већ имамо неку имплементацију у Јави која преводи сву функционалну логику у релациону логику. Осим тога, логика репрезентација и многе друге ствари су лепо везане за то, захваљујући чему добијамо целину
Извор: ввв.хабр.цом