Функционалдық ДҚБЖ

Мәліметтер қоры әлемінде бұрыннан 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 тапсырмасы

Қызметкерлер саны 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();

Басқасынан күрделірек тапсырмаларға көшейік мақалалар. Онда бұл тапсырманы MS SQL жүйесінде жүзеге асыру жолының егжей-тегжейлі талдауы бар.

2.1 тапсырмасы

1997 жылы No 30 өнім 1 бірліктен артық қандай сатушылар сатты?

Домен логикасы (бұрынғы RDBMS-тегідей біз декларацияны өткізіп жібереміз):

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) кейін көрсетілген өрнекті, көрсетілген топтар ішінде (мұнда Тұтынушы және Жыл, бірақ кез келген өрнек болуы мүмкін), ORDER параметрінде көрсетілген өрнектер бойынша топтар ішінде сұрыптайды (мұнда XNUMX). мұнда сатып алынды, ал егер тең болса, онда ішкі өнім кодына сәйкес).

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

Оқырман бұл мәселені 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 көмегімен RDBMS жүйесінде жүзеге асырылуы мүмкін. Бірақ егер барлық логика осылай жазылса, ол өте оқылатын болып көрінбейді.
  • Семантикалық алшақтық жоқ. Мұндай деректер базасы функциялар мен класстарда жұмыс істейді (кестелер мен өрістердің орнына). Классикалық бағдарламалаудағы сияқты (егер әдіс ол жататын класс түріндегі бірінші параметрі бар функция деп есептесек). Тиісінше, әмбебап бағдарламалау тілдерімен «дос табу» әлдеқайда оңай болуы керек. Бұған қоса, бұл тұжырымдама әлдеқайда күрделі функцияларды жүзеге асыруға мүмкіндік береді. Мысалы, келесі операторларды ендіре аласыз:

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

  • Тұқым қуалаушылық және полиморфизм. Функционалды дерекқорда CLASS ClassP: Class1, Class2 конструкциялары арқылы бірнеше мұраны енгізуге және бірнеше полиморфизмді жүзеге асыруға болады. Нақты қалай болатынын алдағы мақалаларда жазатын шығармын.

Бұл жай ғана тұжырымдама болса да, бізде Java-да барлық функционалдық логиканы реляциялық логикаға аударатын кейбір енгізулер бар. Оған қоса, өкілдіктердің логикасы және басқа да көптеген нәрселер оған әдемі бекітілген, соның арқасында біз тұтас аламыз платформасы. Негізінде, біз RDBMS (әзірше тек PostgreSQL) «виртуалды машина» ретінде қолданамыз. Кейде бұл аудармада мәселелер туындайды, себебі RDBMS сұрау оңтайландырғышы FDBMS білетін белгілі бір статистиканы білмейді. Теориялық тұрғыдан функционалдық логика үшін арнайы бейімделген белгілі бір құрылымды сақтау ретінде пайдаланатын деректер қорын басқару жүйесін енгізуге болады.

Ақпарат көзі: www.habr.com

пікір қалдыру