Функцыянальная СКБД

Свет баз дадзеных даўно захоплены рэляцыйнымі СКБД, у якіх выкарыстоўваецца мова 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)
)

Функцыянальная

CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[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)
)

Функцыянальная

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

Абедзве рэалізацыі эквівалентныя. Для першага выпадку ў рэляцыйнай базе можна выкарыстоўваць CREATE VIEW, які такім жа чынам спачатку палічыць для канкрэтнага аддзела максімальную зарплату ў ім. У далейшым я для нагляднасці буду выкарыстоўваць першы выпадак, бо ён лепш адлюстроўвае рашэнне.

задача 1.3

Вывесці спіс ID аддзелаў, колькасць супрацоўнікаў у якіх не перавышае 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

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

Пяройдзем да больш складаных задач з іншай артыкулы. У ёй ёсць падрабязны разбор таго, як рэалізоўваць гэтую задачу на MS SQL.

задача 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;

Аператар PARTITION працуе па наступным прынцыпе: ён сумуе выраз, паказаны пасля SUM (тут 1), усярэдзіне паказаных груп (тут Customer і Year, але можа быць любы выраз), сартуючы ўсярэдзіне груп па выразах, паказаным у ORDER (тут bought, а калі роўныя, то па ўнутраным кодзе прадукта).

задача 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);

Неабходна знайсці магчымых кандыдатаў на сяброўства. Больш фармалізавана трэба знайсці ўсіх людзей A, B, C такіх, што A сябруе з B, а B сябруе з C, A падабаецца C, але A не сябруе з C.
З пункту гледжання функцыянальнай базы дадзеных запыт будзе выглядаць наступным чынам:

SELECT Person a, Person b, Person c WHERE 
    likes(a, c) AND NOT friends(a, c) AND 
    friends(a, b) AND friends(b, c);

Чытачу прапануецца самастойна вырашыць гэтую задачу на SQL. Мяркуецца, што сяброў значна менш, чым тых, хто падабаецца. Таму яны ляжаць у асобных табліцах. У выпадку паспяховага рашэння ёсць таксама задача з дзвюма зорачкамі. У ёй сяброўства не сіметрычна. На функцыянальнай базе дадзеных гэта будзе выглядаць так:

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: рашэнне задачы з першай і другой зорачкай ад dss_kalika:

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 

Заключэнне

Варта адзначыць, што прыведзены сінтаксіс мовы - гэта ўсяго толькі адзін з варыянтаў рэалізацыі прыведзенай канцэпцыі. За аснову быў узяты менавіта 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

Дадаць каментар