Functional na DBMS

Ang mundo ng mga database ay matagal nang pinangungunahan ng mga relational na DBMS, na gumagamit ng wikang SQL. Kaya't ang mga umuusbong na variant ay tinatawag na NoSQL. Nagawa nilang mag-ukit ng isang partikular na lugar para sa kanilang sarili sa merkado na ito, ngunit ang mga relational na DBMS ay hindi mamamatay, at patuloy na aktibong ginagamit para sa kanilang mga layunin.

Sa artikulong ito gusto kong ilarawan ang konsepto ng isang functional database. Para sa isang mas mahusay na pag-unawa, gagawin ko ito sa pamamagitan ng paghahambing nito sa classical relational model. Gagamitin bilang mga halimbawa ang mga problema mula sa iba't ibang mga pagsubok sa SQL na matatagpuan sa Internet.

Pagpapakilala

Gumagana ang mga relational database sa mga talahanayan at field. Sa isang functional na database, ang mga klase at function ang gagamitin, ayon sa pagkakabanggit. Ang isang field sa isang talahanayan na may mga N key ay kakatawanin bilang isang function ng N parameter. Sa halip na mga ugnayan sa pagitan ng mga talahanayan, gagamitin ang mga function na nagbabalik ng mga bagay ng klase kung saan ginawa ang koneksyon. Gagamitin ang komposisyon ng function sa halip na SUMALI.

Bago lumipat nang direkta sa mga gawain, ilalarawan ko ang gawain ng lohika ng domain. Para sa DDL gagamit ako ng PostgreSQL syntax. Para sa functional mayroon itong sariling syntax.

Mga talahanayan at mga patlang

Isang simpleng object ng Sku na may mga field ng pangalan at presyo:

Relational

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

Magagamit

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

Inaanunsyo namin ang dalawa Ρ„ΡƒΠ½ΠΊΡ†ΠΈΠΈ, na kumukuha ng isang parameter na Sku bilang input at nagbabalik ng primitive na uri.

Ipinapalagay na sa isang functional na DBMS ang bawat bagay ay magkakaroon ng ilang panloob na code na awtomatikong nabuo at maaaring ma-access kung kinakailangan.

Itakda natin ang presyo para sa produkto/tindahan/supplier. Maaari itong magbago sa paglipas ng panahon, kaya magdagdag tayo ng field ng oras sa talahanayan. Laktawan ko ang pagdedeklara ng mga talahanayan para sa mga direktoryo sa isang relational database upang paikliin ang code:

Relational

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

Magagamit

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

Mga Index

Para sa huling halimbawa, bubuo kami ng index sa lahat ng key at petsa para mabilis naming mahanap ang presyo para sa isang partikular na oras.

Relational

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

Magagamit

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

gawain

Magsimula tayo sa medyo simpleng mga problema na kinuha mula sa kaukulang Artikulo sa Habr.

Una, ideklara natin ang lohika ng domain (para sa relational database ito ay direktang ginagawa sa artikulo sa itaas).

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

Gawain 1.1

Magpakita ng listahan ng mga empleyado na tumatanggap ng suweldo na mas malaki kaysa sa kanilang agarang superbisor.

Relational

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

Magagamit

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

Gawain 1.2

Ilista ang mga empleyado na tumatanggap ng pinakamataas na suweldo sa kanilang departamento

Relational

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

Magagamit

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

Ang parehong pagpapatupad ay katumbas. Para sa unang kaso, sa isang relational database maaari mong gamitin ang CREATE VIEW, na sa parehong paraan ay unang kalkulahin ang maximum na suweldo para sa isang partikular na departamento sa loob nito. Sa kung ano ang sumusunod, para sa kalinawan, gagamitin ko ang unang kaso, dahil ito ay mas mahusay na sumasalamin sa solusyon.

Gawain 1.3

Magpakita ng listahan ng mga department ID, ang bilang ng mga empleyado kung saan hindi lalampas sa 3 tao.

Relational

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

Magagamit

countEmployees 'ΠšΠΎΠ»ΠΈΡ‡Π΅ΡΡ‚Π²ΠΎ ΡΠΎΡ‚Ρ€ΡƒΠ΄Π½ΠΈΠΊΠΎΠ²' (Department d) = 
    GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;

Gawain 1.4

Magpakita ng listahan ng mga empleyado na walang itinalagang manager na nagtatrabaho sa parehong departamento.

Relational

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

Magagamit

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

Gawain 1.5

Maghanap ng listahan ng mga department ID na may pinakamataas na kabuuang suweldo ng empleyado.

Relational

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 )

Magagamit

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

Lumipat tayo sa mas kumplikadong mga gawain mula sa isa pa Artikulo. Naglalaman ito ng detalyadong pagsusuri kung paano ipatupad ang gawaing ito sa MS SQL.

Gawain 2.1

Aling mga nagbebenta ang nagbenta ng higit sa 1997 yunit ng produkto No. 30 noong 1?

Logic ng domain (tulad ng dati sa RDBMS nilaktawan namin ang deklarasyon):

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

Relational

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

Magagamit

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;

Gawain 2.2

Para sa bawat mamimili (pangalan, apelyido), hanapin ang dalawang kalakal (pangalan) kung saan ginastos ng mamimili ang pinakamaraming pera noong 1997.

Pinapalawak namin ang lohika ng domain mula sa nakaraang halimbawa:

CLASS Customer 'ΠšΠ»ΠΈΠ΅Π½Ρ‚';
contactName 'ЀИО' = DATA STRING[100] (Customer);

customer = DATA Customer (Order);

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

Relational

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

Magagamit

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;

Gumagana ang operator ng PARTITION sa sumusunod na prinsipyo: binubuo nito ang expression na tinukoy pagkatapos ng SUM (dito 1), sa loob ng tinukoy na mga grupo (dito Customer at Taon, ngunit maaaring anumang expression), pag-uuri sa loob ng mga grupo ayon sa mga expression na tinukoy sa ORDER ( dito binili, at kung pantay, pagkatapos ay ayon sa panloob na code ng produkto).

Gawain 2.3

Gaano karaming mga kalakal ang kailangang i-order mula sa mga supplier upang matupad ang mga kasalukuyang order.

Palawakin natin muli ang logic ng domain:

CLASS Supplier 'ΠŸΠΎΡΡ‚Π°Π²Ρ‰ΠΈΠΊ';
companyName = DATA STRING[100] (Supplier);

supplier = DATA Supplier (Product);

unitsInStock 'ΠžΡΡ‚Π°Ρ‚ΠΎΠΊ Π½Π° ΡΠΊΠ»Π°Π΄Π΅' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма ΠΏΡ€ΠΎΠ΄Π°ΠΆΠΈ' = DATA NUMERIC[10,3] (Product);

Relational

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

Magagamit

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;

Problema sa isang asterisk

At ang huling halimbawa ay mula sa akin nang personal. Mayroong lohika ng isang social network. Ang mga tao ay maaaring maging magkaibigan sa isa't isa at magkagusto sa isa't isa. Mula sa isang functional na pananaw sa database, magiging ganito ang hitsura:

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

Ito ay kinakailangan upang makahanap ng mga posibleng kandidato para sa pagkakaibigan. Sa mas pormal na paraan, kailangan mong hanapin ang lahat ng tao A, B, C na si A ay kaibigan ni B, at si B ay kaibigan ni C, gusto ni A si C, ngunit si A ay hindi kaibigan ni C.
Mula sa isang functional na pananaw sa database, ang query ay magiging ganito:

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

Hinihikayat ang mambabasa na lutasin ang problemang ito sa SQL sa kanyang sarili. Ipinapalagay na mas kaunti ang mga kaibigan kaysa sa mga taong gusto mo. Samakatuwid sila ay nasa magkahiwalay na mga talahanayan. Kung matagumpay, mayroon ding gawain na may dalawang bituin. Sa loob nito, ang pagkakaibigan ay hindi simetriko. Sa isang functional na database ay magiging ganito ang hitsura:

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: solusyon sa problema sa una at pangalawang asterisk mula sa 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 

Konklusyon

Dapat tandaan na ang ibinigay na syntax ng wika ay isa lamang sa mga opsyon para sa pagpapatupad ng ibinigay na konsepto. Ang SQL ay kinuha bilang batayan, at ang layunin ay para ito ay maging katulad nito hangga't maaari. Siyempre, maaaring hindi gusto ng ilan ang mga pangalan ng mga keyword, mga rehistro ng salita, atbp. Ang pangunahing bagay dito ay ang konsepto mismo. Kung ninanais, maaari mong gawin ang parehong C++ at Python na magkatulad na syntax.

Ang inilarawan na konsepto ng database, sa palagay ko, ay may mga sumusunod na pakinabang:

  • Ang pagiging simple. Ito ay isang medyo subjective indicator na hindi halata sa mga simpleng kaso. Ngunit kung titingnan mo ang mas kumplikadong mga kaso (halimbawa, mga problema sa mga asterisk), kung gayon, sa palagay ko, ang pagsusulat ng mga naturang query ay mas madali.
  • Π˜Π½ΠΊΠ°ΠΏΡΡƒΠ»ΡΡ†ΠΈΡ. Sa ilang mga halimbawa, ipinahayag ko ang mga intermediate function (halimbawa, despatsado, bili atbp.), kung saan itinayo ang mga kasunod na pag-andar. Pinapayagan ka nitong baguhin ang lohika ng ilang mga pag-andar, kung kinakailangan, nang hindi binabago ang lohika ng mga umaasa sa kanila. Halimbawa, maaari kang gumawa ng mga benta despatsado ay kinakalkula mula sa ganap na magkakaibang mga bagay, habang ang natitirang bahagi ng lohika ay hindi magbabago. Oo, maaari itong ipatupad sa isang RDBMS gamit ang CREATE VIEW. Ngunit kung ang lahat ng lohika ay isinulat sa ganitong paraan, ito ay hindi masyadong nababasa.
  • Walang semantic gap. Ang nasabing database ay gumagana sa mga function at klase (sa halip na mga table at field). Tulad ng sa klasikal na programming (kung ipinapalagay natin na ang isang pamamaraan ay isang function na may unang parameter sa anyo ng klase kung saan ito nabibilang). Alinsunod dito, dapat na mas madaling "makipagkaibigan" sa mga unibersal na programming language. Bilang karagdagan, ang konseptong ito ay nagbibigay-daan para sa mas kumplikadong pag-andar na maipatupad. Halimbawa, maaari kang mag-embed ng mga operator tulad ng:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'ΠŸΠ΅Ρ‚Ρ' MESSAGE  'Π§Ρ‚ΠΎ-Ρ‚ΠΎ ΠŸΠ΅Ρ‚я ΠΏΡ€ΠΎΠ΄Π°Π΅Ρ‚ ΡΠ»ΠΈΡˆΠΊΠΎΠΌ ΠΌΠ½ΠΎΠ³ΠΎ ΠΎΠ΄Π½ΠΎΠ³ΠΎ Ρ‚ΠΎΠ²Π°Ρ€Π° Π² 2019 Π³ΠΎΠ΄Ρƒ';

  • Pamana at polymorphism. Sa isang functional na database, maaari kang magpakilala ng maramihang pamana sa pamamagitan ng CLASS ClassP: Class1, Class2 constructs at ipatupad ang maramihang polymorphism. Marahil ay isusulat ko kung gaano ka eksakto sa mga susunod na artikulo.

Kahit na ito ay isang konsepto lamang, mayroon na kaming ilang pagpapatupad sa Java na nagsasalin ng lahat ng functional logic sa relational logic. Dagdag pa, ang lohika ng mga representasyon at maraming iba pang mga bagay ay napakagandang nakakabit dito, salamat sa kung saan nakuha namin ang kabuuan platform. Sa esensya, ginagamit namin ang RDBMS (PostgreSQL lang sa ngayon) bilang isang "virtual machine". May mga problema kung minsan sa pagsasaling ito dahil hindi alam ng RDBMS query optimizer ang ilang partikular na istatistika na alam ng FDBMS. Sa teorya, posible na ipatupad ang isang database management system na gagamit ng isang tiyak na istraktura bilang imbakan, partikular na inangkop para sa functional logic.

Pinagmulan: www.habr.com

Magdagdag ng komento