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