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

Светът на базите данни отдавна е завладян от релационни СУБД, в които се използва езикът 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)
)

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

КЛАС Sku;
име = ДАННИ СТРУНГ[100] (Sku);
цена = ДАННИ ЧИСЛО[10,5] (Sku);

Обявяваме две функции, които приемат един параметър 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;
КЛАС Магазин;
КЛАС Доставчик;
датаЧас = ДАННИ ДАТА И ЧАС (Sku, Магазин, Доставчик);
цена = ДАННИ ЧИСЛО[10,5] (Sku, Магазин, Доставчик);

Индекси

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

Релационна

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

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

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

Задачи

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

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

КЛАС Департамент;
име = ДАННИ НИЗОВЕ[100] (Департамент);

КЛАС Служител;
отдел = ДАННИ Отдел (Служител);
началник = ДАННИ Служител (Служител);
име = ДАННИ НИЗ[100] (Служител);
заплата = ДАННИ ЧИСЛО[14,2] (Служител);

Задача 1.1

Изведете списък на служителите, които получават заплата по-висока от непосредствения ръководител.

Релационна

изберете a.*
от   служител a, служител b
където  b.id = a.chief_id
и    a.salary > b.salary

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

ИЗБЕРЕТЕ име(Служител a) КЪДЕ заплата(a) > заплата(ръководител(a));

Задача 1.2

Изведете списък на служителите, получаващи максимална заплата в своя отдел

Релационна

изберете a.*
от   служител a
където  a.salary = ( изберете max(salary) от служител b
                    където  b.department_id = a.department_id )

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

максимална заплата 'Максимална заплата' (отдел s) = 
    ГРУПИРАЙ макс заплата(Служител e) АКО отдел(e) = s;
ИЗБЕРИ име(Служител a) КЪДЕТО заплата(a) = максималнаЗаплата(отдел(a));

// или если "заинлайнить"
ИЗБЕРИ име(Служител a) КЪДЕ 
    заплата(a) = maxЗаплата(ГРУПИРАЙ MAX заплата(Служител e) АКО отдел(e) = отдел(a));

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

Задача 1.3

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

Релационна

изберете department_id
от   служител
group  по department_id
having count(*) <= 3

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

бройСлужители 'Брой служители' (Отдел d) = 
    GROUP SUM 1 IF department(Employee e) = d;
SELECT Отдел d WHERE бройСлужители(d) <= 3;

Задача 1.4

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

Релационна

изберете a.*
от   служител a
ляво   join служител b on (b.id = a.chief_id и b.department_id = a.department_id)
където  b.id е null

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

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

Задача 1.5

Намерете списък на ID на отдели с максималната сумарна заплата на служителите.

Релационна

с обща заплата като
  ( изберете department_id, sum(salary) salary
    от   служител
    group  по department_id )
изберете department_id
от   обща заплата a       
където  a.salary = ( изберете max(salary) от обща заплата )

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

заплатаСума 'Максимална заплата' (Отдел д) = 
    GROUP SUM salary(Employee e) IF department(e) = d;
максЗаплатаСума 'Максимална заплата отдели' () = 
    ГРУПА МАКС заплатаСума(Отдел д);
ИЗБЕРИ Отдел д КЪДЕТО заплатаСума(d) = максЗаплатаСума();

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

Задача 2.1

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

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

КЛАС Служител 'Продавач';
lastName 'Фамилия' = DATA STRING[100] (Служител);

КЛАС Продукт 'Продукт';
id = ДАННИ ЦЯЛО (Продукт);
име = ДАННИ НИЗ[100] (Продукт);

CLASS Order 'Поръчка';
дата = ДАННИ ДАТА (Поръчка);
служител = ДАННИ Служител (Поръчка);

КЛАС Детайл 'Строка поръчка';

поръчка = ДАННИ Поръчка (Детайл);
продукт = ДАННИ Продукт (Детайл);
количество = ДАННИ ЧИСЛО[10,5] (Детайл);

Релационна

изберете Фамилия
от Служители като e
където (
  изберете sum(od.Quantity)
  от [Детайли на поръчки] като od
  където od.ProductID = 1 и od.OrderID в (
    изберете o.OrderID
    от Поръчки като o
    където година(o.OrderDate) = 1997 и e.EmployeeID = o.EmployeeID)
) > 30

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

продаден (Служител e, ЦЯЛО productId, ЦЯЛО year) = 
    ГРУПОВА СУМА количество(Детайли на поръчката d) АКО 
        служител(поръчка(d)) = e И 
        id(продукт(d)) = productId И 
        извлечиГодина(дата(поръчка(d))) = year;
ИЗБЕРЕТЕ фамилия(Служител 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.ИмеНаКонтакт
    НАРЕДИ ПО СУМА(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) * цена на единица(d) * (1 - отстъпка(d));
купил 'Купил' (Клиент c, Продукт p, ЦЕЛОЧИСЛЕНО y) = 
    ГРУПА СУМ sum(Детайл d) АКО 
        клиент(поръчка(d)) = c И 
        продукт(d) = p И 
        извлечиГодина(дата(поръчка(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

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

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

CLASS Доставчик 'Поставщик';
companyName = DATA STRING[100] (Доставчик);

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

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

Релационна

избери s.ИмеНаКомпания, p.ИмеНаПродукт, сума(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.ОстатъчниЕдиници < сума(od.Количеще) + p.НивоНаПоръчка

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

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

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

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

КЛАС Човек;
харесвания = ДАННИ ЛОГИЧЕСКИ (Човек, Човек);
приятели = ДАННИ ЛОГИЧЕСКИ (Човек, Човек);

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

ИЗБЕРИ Лице а, Лице б, Лице в КЪДЕ 
    обича(а, в) И НЕ приятели(а, в) И 
    приятели(а, б) И приятели(б, в);

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

ИЗБЕРИ Лице а, Лице б, Лице в КЪДЕ 
    обича(а, в) И НЕ приятели(а, в) И 
    (friends(a, b) OR friends(b, a)) И 
    (friends(b, c) OR friends(c, b));

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

ИЗБЕРЕТЕ 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
ОТ Лица                 AS p
--Лайкове                      
СЛЕЙТЕ ЛицеОтношение      AS pl ON pl.PersonAID = p.PersonID
                                  И pl.Relation  = 'Харесвам'
--Приятели                     
СЛЕЙТЕ ЛицеОтношение      AS pf ON pf.PersonAID = p.PersonID 
                                  И pf.Relation = 'Приятел'
--Приятели на приятели              
СЛЕЙТЕ ЛицеОтношение      AS pff ON pff.PersonAID = pf.PersonBID
                                   И pff.PersonBID = pl.PersonBID
                                   И pff.Relation = 'Приятел'
--Още не са приятели         
ЛЕВИ СЛЕЙТЕ ЛицеОтношение AS pnf ON pnf.PersonAID = p.PersonID
                                   И pnf.PersonBID = pff.PersonBID
                                   И pnf.Relation = 'Приятел'
КЪДЕ pnf.PersonAID Е NULL 

;С СЛОВА PersonRelationShipCollapsed AS (
  ИЗБЕРЕТЕ pl.PersonAID
        ,pl.PersonBID
        ,pl.Relation 
  ОТ #ЛицеОтношение      AS pl 
  
  СЪЕДИНИ 

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

Заключение

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

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

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

    ОГРАНИЧЕНИЕ sold(Employee e, 1, 2019) > 100 АКО name(e) = 'Петя' СЪОБЩЕНИЕ 'Нещо Петя продава твърде много от един продукт през 2019 година';

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

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

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

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