Функциональная СУБД

Светът на базите данни отдавна е завладян от релационни СУБД, в които се използва езикът SQL. Толкова е силно, че новите разновидности се наричат NoSQL. Успяха да си проправят определено място на този пазар, но релационните СУБД не възнамеряват да изчезнат и продължават активно да се използват за своите цели.

В тази статия искам да опиша концепцията за функционална база данни. За по-добро разбиране ще направя сравнение с класическата релационна модел. Като примери ще се използват задачи от различни тестове по SQL, намерени в интернет.

Въведение

Релационните бази данни оперират с таблици и полета. В функционалната база данни вместо тях ще се използват класове и функции съответно. Поле в таблица с N ключа ще бъде представено като функция с N параметри. Вместо връзки между таблиците ще се използват функции, които връщат обекти от класа, с който е връзката. Вместо JOIN ще се използва композиция на функции.

Преди да преминем към самите задачи, ще опиша задаването на домейнната логика. За DDL ще използвам синтаксиса на PostgreSQL. За функционална ще използвам своя синтаксис.

Таблици и полета

Прост обект Sku с полета наименование и цена:

Релационен

CREATE TABLE Sku
(
    id bigint NOT NULL,
    name character varying(100),
    price numeric(10,5),
    CONSTRAINT id_pkey PRIMARY KEY (id)
)

Функционална

КЛАС Ску;
име = ДАННИ НИЗА[100] (Ску);
цена = ДАННИ ЧИСЛО[10,5] (Ску);

Декларираме две функции, които приемат на вход един параметър Sku, и връщат примитивен тип.

Предполага се, че във функционалната СУБД всеки обект ще има някакъв вътрешен код, който автоматично се генерира и към който при необходимост може да се обърнем.

Задаваме цена за продукта / магазина / доставчика. Тя може да се променя с времето, затова добавяме в таблицата поле за време. Декларирането на таблици за справочници в релационната база данни пропускам, за да съкрати кода:

Релационен

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)
)

Функционална

КЛАС Ску;
КЛАС Магазин;
КЛАС Доставчик;
датаВреме = ДАННИ ДАТА И ЧАС (Sku, Магазин, Доставчик);
цена = ДАННИ ЧИСЛОВИ[10,5] (Sku, Магазин, Доставчик);

Индекси

За последния пример ще изградим индекс по всички ключове и дата, за да можем бързо да намираме цена за определено време.

Релационен

CREATE INDEX prices_date
    ON prices
    (skuId, storeId, supplierId, dateTime)

Функционална

ИНДЕКС Sku sk, Магазин st, Доставчик sp, dateTime(sk, st, sp);

Задачи

Нека започнем с относително прости задачи, взети от съответната на статията на Хабра.

Първо, ще обявим доменната логика (за релационна база, това е направено директно в предоставената статия).

ДЪРЖАВЕН КЛАС
име = ДАННИ СТРОКА[100] (Клас)

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

Функционална

ИЗБЕРИ име(Служител a) КЪДЕТО заплата(a) > заплата(началник(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 'Максимална заплата' (отдели) = 
    GROUP MAX заплата (служител e) IF department(e) = s;
SELECT name(служител 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));

И двете реализации са еквивалентни. За първия случай в релационната база може да се използва CREATE VIEW, който по същия начин първо ще изчисли максималната заплата за конкретния отдел. В по-нататъшните примери ще използвам първия случай, тъй като той по-добре илюстрира решението.

Задача 1.3

Изведете списък с ID на отделите, в които броят на служителите не надвишава 3.

Релационен

select department_id
from employee
group by department_id
having count(*) <= 3

Функционална

countEmployees 'Брой служители' (Отдел 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

Функционална

ИЗБЕРЕТЕ име(Служител a) КЪДЕТО НЕ (отдел(началник(a)) = отдел(a));

Задача 1.5

Намерете списък с ID на отделите с максимална обща заплата на работниците.

Релационен

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 'Максимална заплата' (Отдел d) = 
    GROUP SUM salary(Employee e) IF department(e) = d;
maxSalarySum 'Максимална заплата на отделите' () = 
    GROUP MAX salarySum(Отдел d);
SELECT Отдел d WHERE salarySum(d) = maxSalarySum();

Преминаваме към по-сложни задачи от друга на статията. В нея има подробно разглеждане на това как да се реализира тази задача на MS SQL.

Задача 2.1

Кои продавачи са продали над 30 броя от продукт №1 през 1997 година?

Доменна логика (както преди на РСУБД пропускаме обявлението):

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

Функционална

продаден(Employee e, INTEGER productId, INTEGER year) = 
    ГРУПИРАЙ СУМАТА количество(OrderDetail d) АКО 
        служител(order(d)) = e И 
        id(product(d)) = productId И 
        извлечиГодина(date(order(d))) = year;
ИЗБЕРИ lastName(Employee e) КЪДЕТО продаден(e, 1, 1997) > 30;

Задача 2.2

За всеки клиент (име, фамилия) намерете два продукта (наименование), за които клиентът е похарчил най-много пари през 1997 година.

Разширяваме доменната логика от предишния пример:

CLASS Customer 'Клиент';
contactName 'Име на контакт' = DATA STRING[100] (Customer);

клиент = DATA Клиент (Поръчка);

единичнаЦена = DATA ЧИСЛО[14,2] (Детайл);
намаление = DATA ЧИСЛО[6,2] (Детайл);

Релационен

ИЗБЕРИ Име за контакт, Име на продукта ОТ (
ИЗБЕРИ c.Име за контакт, p.Име на продукта
, НОМЕР_НА_РЕДА() НАД (
    РАЗДЕЛЯНЕ ОТ c.Име за контакт
    НАРЕДИ ПО SUM(od.Количество * od.ЕдиничнаЦена * (1 - od.Намаление)) НАСАМ
) КАТО РейтингПоСума
ОТ Клиенти c
СВЪЗКИ Поръчки o ON o.IDКлиент = c.IDКлиент
СВЪЗКИ [Детайли на поръчката] od ON od.IDПоръчка = o.IDПоръчка
СВЪЗКИ Продукти p ON p.IDПродукт = od.IDПродукт
КЪДЕ ГОДИНА(o.ДатаНаПоръчка) = 1997
ГРУПИРАЙ ПО c.Име за контакт, p.Име на продукта
) t
КЪДЕ РейтингПоСума < 3

Функционална

сума (Детайл д) = количество(d) * цена на единица(d) * (1 - отстъпка(d));
Купил 'Купил' (Клиент c, Продукт p, ЦЕЛО число y) = 
    ГРУПА СУМ sum(Детайл d) IF 
        клиент(поръчка(d)) = c И 
        продукт(d) = p И 
        извлечиГодина(date(поръчка(d))) = y;
рейтинг 'Рейтинг' (Клиент c, Продукт p, ЦЕЛО число y) = 
    ПАРТИТИОН СУМА 1 НАРЕЖДАЙ DESC закупени(c, p, y), p ПО c, y;
ИЗБЕРЕТЕ контактноИме(Клиент c), име(Продукт p) КЪДЕТО рейтинг(c, p, 1997) < 3;

Операторът PARTITION работи по следния начин: той сумира израза, посочен след SUM (тук 1), в рамките на зададените групи (тук Клиент и Година, но може да бъде всеки израз), сортиране в рамките на групите по изразите, посочени в ORDER (тук закупени, а ако са равни, по вътрешния код на продукта).

Задача 2.3

Колко артикула трябва да бъдат поръчани от доставчиците, за да се изпълнят текущите поръчки.

Отново разширяваме домейн логиката:

КЛАС Доставчик 'Доставчик';
companyName = DATA STRING[100] (Доставчик);

доставчик = DATA Доставчик (Продукт);

единициНаСклад 'Остатък на склада' = DATA ЧИСЛО[10,3] (Продукт);
нормативНаПоръчка 'Норма продажба' = DATA ЧИСЛО[10,3] (Продукт);

Релационен

избери s.Наименование на компанията, p.Име на продукта, sum(od.Количество) + p.НормативНаПоръчка — p.ЕдинициНаСклад както ДаПоръчам
от Поръчки o
свързване [Детайли на поръчката] od on o.IDПоръчка = od.IDПоръчка
свързване Продукти p on od.IDПродукт = p.IDПродукт
свързване Доставчици s on p.IDДоставчик = s.IDДоставчик
където o.ДатаНаИзпращане е null
групирай по s.Наименование на компанията, p.Име на продукта, p.ЕдинициНаСклад, p.НормативНаПоръчка
имайки p.ЕдинициНаСклад < sum(od.Количество) + p.НормативНаПоръчка

Функционална

поръчаноНоНеИзпратено 'Заказано, но не отгружено' (Продукт p) = 
    ГРУПА СУМАРЕН брой(Детайл на поръчка d) IF продукт(d) = p;
къмПоръчка 'К поръчку' (Продукт p) = поръчаноНоНеИзпратено(p) + повторноНиво(p) - единициНаСклад(p);
ИЗБЕРИ имеНаКомпания(доставчик(Продукт p)), име(p), къмПоръчка(p) КЪДЕ къмПоръчка(p) > 0;

Задача със звезда

И последен пример лично от мен. Има логика на социална мрежа. Хората могат да дружат помежду си и да се харесват. От гледна точка на функционалната база данни това ще изглежда по следния начин:

КЛАС Person;
харесвания = ДАННИ BOOLEAN (Person, Person);
приятели = ДАННИ BOOLEAN (Person, Person);

Необходимо е да се намерят възможни кандидати за приятелство. По-формализирано трябва да се намерят всички хора A, B, C, така че A дружи с B, B дружи с C, A харесва C, но A не дружи с C.
От гледна точка на функционалната база данни заявката ще изглежда по следния начин:

ИЗБЕРИ Човек a, Човек b, Човек c КЪДЕ 
    харесва(a, c) И НЕ приятели(a, c) И 
    приятели(a, b) И приятели(b, c);

На читателя се предлага самостоятелно да реши тази задача на SQL. Предполага се, че приятелите са много по-малко от тези, които харесват. Следователно те лежат в отделни таблици. В случай на успешно решаване, има също защита с две звезди. В него приятелството не е симетрично. На функционалната база данни това ще изглежда така:

ИЗБЕРИ Човек a, Човек b, Човек c КЪДЕ 
    харесва(a, c) И НЕ приятели(a, c) И 
    (приятели(a, b) ИЛИ приятели(b, a)) И 
    (приятели(b, c) ИЛИ приятели(c, b));

UPD: решение на задачата с първа и втора звезда от dss_kalika:

ИЗБЕРИ 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
ОТ Лица                  КАТО p
--Лайкове                      
ПРИСОЕДИНИ Към ЛичниВръзки     КАТО pl ON pl.PersonAID = p.PersonID
                                  И pl.Отношение  = 'Like'
--Приятели                     
ПРИСОЕДИНИ Към ЛичниВръзки     КАТО pf ON pf.PersonAID = p.PersonID 
                                  И pf.Отношение = 'Friend'
--Приятели на Приятели              
ПРИСОЕДИНИ Към ЛичниВръзки     КАТО pff ON pff.PersonAID = pf.PersonBID
                                   И pff.PersonBID = pl.PersonBID
                                   И pff.Отношение = 'Friend'
--Още не са приятели         
ЛЯВ ПРИСОЕДИНИ Към ЛичниВръзки КАТО pnf ON pnf.PersonAID = p.PersonID
                                   И pnf.PersonBID = pff.PersonBID
                                   И pnf.Отношение = 'Friend'
КЪДЕ pnf.PersonAID Е NULL 

;С С В личностинаВръзкаСити КАТО (
  ИЗБЕРИ pl.PersonAID
        ,pl.PersonBID
        ,pl.Отношение 
  ОТ #ЛичниВръзки      КАТО pl 
  
  СЪЮЗ 

  ИЗБЕРИ pl.PersonBID КАТО PersonAID
        ,pl.PersonAID КАТО PersonBID
        ,pl.Отношение
  ОТ #ЛичниВръзки      КАТО pl 
)
ИЗБЕРИ 
   pl.PersonAID
  ,pf.PersonBID
  ,pff.PersonBID
ОТ #Лица                     КАТО p
--Лайкове                      
ПРИСОЕДИНИ Към личниВръзкиСити  КАТО pl ON pl.PersonAID = p.PersonID
                                 И pl.Отношение  = 'Like'                                  
--Приятели                          
ПРИСОЕДИНИ Към личниВръзкиСити  КАТО pf ON pf.PersonAID = p.PersonID 
                                 И pf.Отношение = 'Friend'
--Приятели на Приятели                   
ПРИСОЕДИНИ Към личниВръзкиСити  КАТО pff ON pff.PersonAID = pf.PersonBID
                                 И pff.PersonBID = pl.PersonBID
                                 И pff.Отношение = 'Friend'
--Още не са приятели                   
ЛЯВ ПРИСОЕДИНИ Към личниВръзкиСити AS pnf ON pnf.PersonAID = p.PersonID
                                   И pnf.PersonBID = pff.PersonBID
                                   И pnf.Отношение = 'Friend'
КЪДЕ pnf.[PersonAID] Е NULL 

Заключение

Следва да се отбележи, че представеният синтаксис на езика е само един от вариантите за реализация на представената концепция. Основата е взета от SQL, и целта е той максимално да бъде подобен на него. Разбира се, на някого може да не му харесат наименованията на ключовите думи, регистъра на думите и други. Тук основното е именно самата концепция. При желание може да се направи и C++, и Python подобен синтаксис.

Описаната концепция на базата данни, по мое мнение, притежава следните предимства:

  • Лесна употреба. Това е относително субективен показател, който не е очевиден при простите случаи. Но ако погледнем по-сложни случаи (например задачите със звездичките), то, по мое мнение, писането на такива заявки е значително по-лесно.
  • Инкапсулация. В някои примери обявявах междинни функции (например, продадено, купено и т.н.), от които се изграждаха последващите функции. Това позволява, при необходимост, да се променя логиката на определени функции без да се изменя логиката на зависимите от тях. Например, може да се направи, така че продажбите продадено се считат от напълно различни обекти, като логиката остава непроменена. Да, в РСУБД това може да бъде реализирано с помощта на CREATE VIEW. Но ако цялата логика се пише по този начин, ще изглежда не особено четлива.
  • Липса на семантична пропаст. Тази база данни оперира с функции и класове (вместо таблици и полета). По същия начин, както в класическото програмиране (считано, че методът е функция с първия параметър под формата на клас, към който принадлежи). Съответно, „сдружаването“ с универсалните езици за програмиране трябва да бъде значително по-лесно. Освен това, тази концепция позволява реализирането на много по-сложни функции. Например, може да се вграждат в базата данни оператори от вида:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Нещо Петя продава прекалено много от един продукт през 2019 година';

  • Наследяване и полиморфизъм. В функционалната база данни може да се въведе множество наследяване чрез конструкции CLASS ClassP: Class1, Class2 и да се реализира множество полиморфизъм. Как точно, може би ще напиша в следващите статии.

Въпреки че това е само концепция, вече имаме известна реализация на Java, която превежда цялата функционална логика в релационна логика. Освен това е красиво свързана логиката на представянията и много други неща, благодарение на които се получава цяла платформа. По същество, използваме РСУБД (засега само PostgreSQL) като „виртуална машина“. При такова транслации понякога възникват проблеми, тъй като оптимизаторът на заявки в РСУБД не знае определена статистика, която знае ФСУБД. В теорията, може да се реализира система за управление на бази данни, която да използва като хранилище определена структура, адаптирана именно за функционалната логика.

Източник: habr.com

Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри 🔥 Купете надежден хостинг за сайтове със защита от DDoS, VPS и VDS сървъри | ProHoster