Funktsional DBMS

Ma'lumotlar bazalari dunyosida uzoq vaqtdan beri SQL tilidan foydalanadigan relyatsion DBMSlar hukmronlik qiladi. Shunday qilib, paydo bo'lgan variantlar NoSQL deb ataladi. Ular ushbu bozorda o'zlari uchun ma'lum o'rinni egallashga muvaffaq bo'lishdi, ammo relyatsion DBMS o'lmaydi va o'z maqsadlari uchun faol foydalanishda davom etadi.

Ushbu maqolada men funktsional ma'lumotlar bazasi tushunchasini tasvirlamoqchiman. Yaxshiroq tushunish uchun men buni klassik munosabatlar modeli bilan taqqoslash orqali qilaman. Misol tariqasida Internetda topilgan turli xil SQL testlaridagi muammolardan foydalaniladi.

kirish

Relyatsion ma'lumotlar bazalari jadvallar va maydonlarda ishlaydi. Funktsional ma'lumotlar bazasida o'rniga mos ravishda sinflar va funktsiyalar qo'llaniladi. N tugmali jadvaldagi maydon N parametr funksiyasi sifatida taqdim etiladi. Jadvallar o'rtasidagi munosabatlar o'rniga ulanish amalga oshirilgan sinf ob'ektlarini qaytaradigan funktsiyalar qo'llaniladi. JOIN o‘rniga funksiya tarkibi ishlatiladi.

To'g'ridan-to'g'ri vazifalarga o'tishdan oldin men domen mantig'ining vazifasini tasvirlab beraman. DDL uchun men PostgreSQL sintaksisidan foydalanaman. Funktsionallik uchun u o'z sintaksisiga ega.

Jadvallar va maydonlar

Nom va narx maydonlariga ega oddiy Sku obyekti:

Aloqaviy

CREATE TABLE Sku
(
    id bigint NOT NULL,
    name character varying(100),
    price numeric(10,5),
    CONSTRAINT id_pkey PRIMARY KEY (id)
)

funktsional

CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);

Biz ikkitasini e'lon qilamiz vazifalari, ular bitta Sku parametrini kirish sifatida qabul qiladi va ibtidoiy turni qaytaradi.

Funktsional ma'lumotlar bazasida har bir ob'ekt avtomatik ravishda yaratiladigan va kerak bo'lganda kirish mumkin bo'lgan ichki kodga ega bo'lishi taxmin qilinadi.

Keling, mahsulot/do'kon/yetkazib beruvchi uchun narxni belgilaylik. Vaqt o'tishi bilan u o'zgarishi mumkin, shuning uchun jadvalga vaqt maydonini qo'shamiz. Kodni qisqartirish uchun relyatsion ma'lumotlar bazasidagi kataloglar uchun jadvallarni e'lon qilishni o'tkazib yuboraman:

Aloqaviy

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

funktsional

CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);

Ko'rsatkichlar

Oxirgi misol uchun biz ma'lum bir vaqt uchun narxni tezda topishimiz uchun barcha kalitlar va sana bo'yicha indeks yaratamiz.

Aloqaviy

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

funktsional

INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);

vazifalar

Keling, mos keladiganlardan olingan nisbatan oddiy masalalardan boshlaylik maqolalar Habrda.

Birinchidan, domen mantig'ini e'lon qilaylik (relatsion ma'lumotlar bazasi uchun bu to'g'ridan-to'g'ri yuqoridagi maqolada amalga oshiriladi).

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

Vazifa 1.1

To'g'ridan-to'g'ri rahbaridan ko'proq maosh oladigan xodimlar ro'yxatini ko'rsating.

Aloqaviy

select a.*
from   employee a, employee b
where  b.id = a.chief_id
and    a.salary > b.salary

funktsional

SELECT name(Employee a) WHERE salary(a) > salary(chief(a));

Vazifa 1.2

O'z bo'limida maksimal ish haqi oladigan xodimlarni sanab o'ting

Aloqaviy

select a.*
from   employee a
where  a.salary = ( select max(salary) from employee b
                    where  b.department_id = a.department_id )

funktsional

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

Ikkala dastur ham bir xil. Birinchi holda, relyatsion ma'lumotlar bazasida siz CREATE VIEW-dan foydalanishingiz mumkin, u xuddi shu tarzda birinchi navbatda undagi ma'lum bir bo'lim uchun maksimal ish haqini hisoblab chiqadi. Keyinchalik aniqlik uchun men birinchi holatdan foydalanaman, chunki u yechimni yaxshiroq aks ettiradi.

Vazifa 1.3

Xodimlar soni 3 kishidan oshmaydigan bo'lim identifikatorlari ro'yxatini ko'rsating.

Aloqaviy

select department_id
from   employee
group  by department_id
having count(*) <= 3

funktsional

countEmployees 'Количество сотрудников' (Department d) = 
    GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;

Vazifa 1.4

Xuddi shu bo'limda ishlaydigan tayinlangan menejeri bo'lmagan xodimlar ro'yxatini ko'rsating.

Aloqaviy

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

funktsional

SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));

Vazifa 1.5

Xodimlarning maksimal ish haqiga ega bo'lim identifikatorlari ro'yxatini toping.

Aloqaviy

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 )

funktsional

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

Keling, boshqasidan murakkabroq vazifalarga o'tamiz maqolalar. Unda MS SQL da ushbu vazifani qanday amalga oshirish kerakligi batafsil tahlil qilingan.

Vazifa 2.1

Qaysi sotuvchilar 1997 yilda 30 donadan ortiq mahsulot No1 sotgan?

Domen mantig'i (oldingi RDBMS da bo'lgani kabi biz deklaratsiyani o'tkazib yuboramiz):

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

Aloqaviy

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

funktsional

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;

Vazifa 2.2

Har bir xaridor (ismi, familiyasi) uchun 1997 yilda xaridor eng ko'p pul sarflagan ikkita tovarni (ism) toping.

Biz oldingi misoldan domen mantig'ini kengaytiramiz:

CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);

customer = DATA Customer (Order);

unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);

Aloqaviy

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

funktsional

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 operatori quyidagi printsip bo'yicha ishlaydi: u SUMdan keyin ko'rsatilgan ifodani (bu erda 1), ko'rsatilgan guruhlar ichida (bu erda Mijoz va Yil, lekin har qanday ifoda bo'lishi mumkin) jamlaydi, ORDER (bu erda ko'rsatilgan ifodalar bo'yicha guruhlar ichida saralanadi) bu erda sotib olingan va agar teng bo'lsa, ichki mahsulot kodiga muvofiq).

Vazifa 2.3

Joriy buyurtmalarni bajarish uchun etkazib beruvchilardan qancha tovarlarga buyurtma berish kerak.

Domen mantig'ini yana kengaytiramiz:

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

supplier = DATA Supplier (Product);

unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);

Aloqaviy

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

funktsional

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;

Yulduzcha bilan muammo

Va oxirgi misol shaxsan mendan. Ijtimoiy tarmoqning mantig'i bor. Odamlar bir-birlari bilan do'st bo'lishlari va bir-birlarini yoqtirishlari mumkin. Funktsional ma'lumotlar bazasi nuqtai nazaridan u quyidagicha ko'rinadi:

CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);

Do'stlik uchun mumkin bo'lgan nomzodlarni topish kerak. Rasmiyroq aytganda, siz A, B, C barcha odamlarni shunday topishingiz kerakki, A B bilan do'st bo'lsin va B C bilan do'st, A C bilan do'st bo'lsin, lekin A C bilan do'st emas.
Funktsional ma'lumotlar bazasi nuqtai nazaridan so'rov quyidagicha ko'rinadi:

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

O'quvchi ushbu muammoni SQLda mustaqil ravishda hal qilishga taklif qilinadi. Do'stlar sizga yoqadigan odamlarga qaraganda ancha kam deb taxmin qilinadi. Shuning uchun ular alohida jadvallarda joylashgan. Muvaffaqiyatli bo'lsa, ikkita yulduzli vazifa ham bor. Unda do'stlik nosimmetrik emas. Funktsional ma'lumotlar bazasida u quyidagicha ko'rinadi:

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: birinchi va ikkinchi yulduzcha bilan muammoni hal qilish 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 

xulosa

Shuni ta'kidlash kerakki, berilgan til sintaksisi berilgan kontseptsiyani amalga oshirish variantlaridan biridir. SQL asos qilib olindi va maqsad unga imkon qadar o'xshash bo'lishi edi. Albatta, ba'zilarga kalit so'zlarning nomlari, so'z registrlari va boshqalar yoqmasligi mumkin. Bu erda asosiy narsa kontseptsiyaning o'zi. Agar xohlasangiz, siz C++ va Python-ni o'xshash sintaksis qilishingiz mumkin.

Ta'riflangan ma'lumotlar bazasi kontseptsiyasi, mening fikrimcha, quyidagi afzalliklarga ega:

  • sukunat. Bu oddiy holatlarda aniq bo'lmagan nisbatan sub'ektiv ko'rsatkich. Ammo agar siz murakkabroq holatlarga qarasangiz (masalan, yulduzcha bilan bog'liq muammolar), menimcha, bunday so'rovlarni yozish ancha oson.
  • Inkapsulyatsiya. Ba'zi misollarda men oraliq funktsiyalarni e'lon qildim (masalan, sotilgan, sotib oldi va boshqalar), undan keyingi funktsiyalar qurilgan. Bu, agar kerak bo'lsa, ularga bog'liq bo'lganlarning mantig'ini o'zgartirmasdan, ma'lum funktsiyalarning mantiqini o'zgartirishga imkon beradi. Masalan, siz savdo qilishingiz mumkin sotilgan butunlay boshqa ob'ektlardan hisoblangan, qolgan mantiq esa o'zgarmaydi. Ha, buni RDBMS da CREATE VIEW yordamida amalga oshirish mumkin. Ammo agar barcha mantiq shu tarzda yozilgan bo'lsa, u juda o'qilishi mumkin bo'lmaydi.
  • Semantik bo'shliq yo'q. Bunday ma'lumotlar bazasi funksiyalar va sinflar (jadvallar va maydonlar o'rniga) ustida ishlaydi. Klassik dasturlashda bo'lgani kabi (agar metod u mansub bo'lgan sinf ko'rinishidagi birinchi parametrga ega bo'lgan funksiya deb faraz qilsak). Shunga ko'ra, universal dasturlash tillari bilan "do'stlashish" ancha oson bo'lishi kerak. Bundan tashqari, ushbu kontseptsiya yanada murakkab funktsiyalarni amalga oshirishga imkon beradi. Masalan, siz quyidagi kabi operatorlarni joylashtirishingiz mumkin:

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

  • Irsiyat va polimorfizm. Funktsional ma'lumotlar bazasida siz CLASS ClassP: Class1, Class2 konstruktsiyalari orqali bir nechta merosni kiritishingiz va bir nechta polimorfizmni amalga oshirishingiz mumkin. Kelgusi maqolalarda aniq qanday qilib yozaman.

Bu shunchaki tushuncha bo'lsa ham, bizda Java-da barcha funktsional mantiqni relyatsion mantiqqa aylantiruvchi ba'zi ilovalar mavjud. Bundan tashqari, vakillik mantig'i va boshqa ko'plab narsalar unga juda yaxshi biriktirilgan, buning natijasida biz butunlikni olamiz platforma. Aslida, biz RDBMS dan (hozircha faqat PostgreSQL) “virtual mashina” sifatida foydalanamiz. Ba'zida ushbu tarjima bilan bog'liq muammolar paydo bo'ladi, chunki RDBMS so'rovlarini optimallashtiruvchi FDBMS biladigan ma'lum statistik ma'lumotlarni bilmaydi. Nazariy jihatdan, ma'lum bir tuzilmani funktsional mantiq uchun moslashtirilgan saqlash sifatida ishlatadigan ma'lumotlar bazasini boshqarish tizimini amalga oshirish mumkin.

Manba: www.habr.com

a Izoh qo'shish