Функционалдык DBMS

Берилиштер базаларынын дүйнөсүндө көптөн бери SQL тилин колдонгон реляциялык DBMS үстөмдүк кылып келген. Ошентип, пайда болгон варианттар NoSQL деп аталат. Алар бул рынокто өздөрүнө белгилүү орунду ээлешти, бирок реляциялык DBMS өлбөйт жана өз максаттары үчүн жигердүү колдонула берет.

Бул макалада мен функционалдык маалымат базасы түшүнүгүн сүрөттөгүм келет. Жакшыраак түшүнүү үчүн, мен муну классикалык реляциялык модель менен салыштырып көрөм. Мисал катары Интернетте табылган ар кандай 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-жылы 30 даанадан ашык No 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де көрсөтүлгөн туюнтмалар боюнча топтордун ичинде сорттоо ( бул жерде сатылып алынган, эгерде бирдей болсо, анда ички продукт кодуна ылайык).

тапшырма 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 билген белгилүү статистиканы билбейт. Теориялык жактан алганда, функционалдык логикага ылайыкташтырылган белгилүү бир структураны сактоочу катары колдоно турган маалымат базасын башкаруу системасын ишке ашырууга болот.

Source: www.habr.com

Комментарий кошуу