Функционални ДБМС

Светом база података већ дуго доминирају релациони ДБМС, који користе СКЛ језик. Толико да се нове варијанте називају НоСКЛ. Успели су да изборе за себе одређено место на овом тржишту, али релациони ДБМС-ови неће умрети, и настављају да се активно користе у своје сврхе.

У овом чланку желим да опишем концепт функционалне базе података. Ради бољег разумевања, урадићу ово тако што ћу га упоредити са класичним релационим моделом. Као примери ће бити коришћени проблеми из различитих СКЛ тестова пронађених на Интернету.

Увод

Релационе базе података раде на табелама и пољима. У функционалној бази података, класе и функције ће се користити уместо њих. Поље у табели са Н кључева биће представљено као функција од Н параметара. Уместо релација између табела, користиће се функције које враћају објекте класе са којом се успоставља веза. Композиција функције ће се користити уместо ЈОИН.

Пре него што пређем директно на задатке, описаћу задатак доменске логике. За ДДЛ користићу ПостгреСКЛ синтаксу. За функционално има своју синтаксу.

Табеле и поља

Једноставан Ску објекат са пољима имена и цене:

Релациона

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 конструкције и имплементирати вишеструки полиморфизам. Вероватно ћу писати како тачно у будућим чланцима.

Иако је ово само концепт, већ имамо неку имплементацију у Јави која преводи сву функционалну логику у релациону логику. Осим тога, логика репрезентација и многе друге ствари су лепо везане за то, захваљујући чему добијамо целину платформа. У суштини, користимо РДБМС (за сада само ПостгреСКЛ) као „виртуелну машину“. Понекад се јављају проблеми са овим преводом јер РДБМС оптимизатор упита не зна одређене статистике које ФДБМС познаје. У теорији, могуће је имплементирати систем управљања базом података који ће користити одређену структуру као складиште, прилагођену посебно за функционалну логику.

Извор: ввв.хабр.цом

Додај коментар