Funkcinė DBVS

Duomenų bazių pasaulyje jau seniai dominavo reliacinės DBVS, kurios naudoja SQL kalbą. Tiek, kad atsirandantys variantai vadinami NoSQL. Jie sugebėjo išsiskirti tam tikrą vietą šioje rinkoje, tačiau reliacinės DBVS nemirs ir toliau bus aktyviai naudojamos savo tikslams.

Šiame straipsnyje noriu apibūdinti funkcinės duomenų bazės sąvoką. Kad geriau suprasčiau, tai padarysiu palygindamas su klasikiniu reliaciniu modeliu. Kaip pavyzdžiai bus panaudotos problemos iš įvairių internete rastų SQL testų.

įvedimas

Reliacinės duomenų bazės veikia lentelėse ir laukuose. Funkcinėje duomenų bazėje bus naudojamos atitinkamai klasės ir funkcijos. Lentelės laukas su N klavišais bus pavaizduotas kaip N parametrų funkcija. Vietoj ryšių tarp lentelių bus naudojamos funkcijos, kurios grąžina klasės, su kuria užmezgamas ryšys, objektus. Vietoj JOIN bus naudojama funkcijos sudėtis.

Prieš pereinant tiesiai prie užduočių, aprašysiu domeno logikos užduotį. DDL naudosiu PostgreSQL sintaksę. Funkcionaliems jis turi savo sintaksę.

Lentelės ir laukai

Paprastas Sku objektas su pavadinimo ir kainos laukais:

Santykinis

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

Funkcionalus

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

Skelbiame du функции, kurie ima vieną parametrą Sku kaip įvestį ir grąžina primityvų tipą.

Daroma prielaida, kad funkcinėje DBVS kiekvienas objektas turės tam tikrą vidinį kodą, kuris yra automatiškai generuojamas ir gali būti pasiekiamas, jei reikia.

Nustatykime prekės/parduotuvės/tiekėjo kainą. Laikui bėgant jis gali keistis, todėl į lentelę įtraukime laiko lauką. Praleisiu katalogų lentelių deklaravimą reliacinėje duomenų bazėje, kad sutrumpinčiau kodą:

Santykinis

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

Funkcionalus

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

Indeksai

Paskutiniame pavyzdyje mes sukursime visų raktų ir datos indeksą, kad galėtume greitai rasti kainą konkrečiam laikui.

Santykinis

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

Funkcionalus

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

užduotys

Pradėkime nuo gana paprastų problemų, paimtų iš atitinkamų Straipsnis apie Habr.

Pirmiausia deklaruokime domeno logiką (reliacinės duomenų bazės atveju tai daroma tiesiogiai aukščiau esančiame straipsnyje).

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 užduotis

Parodykite sąrašą darbuotojų, kurie gauna didesnį atlyginimą nei jų tiesioginis vadovas.

Santykinis

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

Funkcionalus

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

1.2 užduotis

Išvardykite darbuotojus, kurie savo skyriuje gauna maksimalų atlyginimą

Santykinis

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

Funkcionalus

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

Abu įgyvendinimai yra lygiaverčiai. Pirmuoju atveju reliacinėje duomenų bazėje galite naudoti CREATE VIEW, kuri tuo pačiu būdu pirmiausia apskaičiuos maksimalų atlyginimą konkrečiam joje esančiam padaliniui. Toliau, siekiant aiškumo, naudosiu pirmąjį atvejį, nes jis geriau atspindi sprendimą.

1.3 užduotis

Rodyti skyrių ID sąrašą, kuriame darbuotojų skaičius neviršija 3 žmonių.

Santykinis

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

Funkcionalus

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

1.4 užduotis

Rodyti darbuotojų, kurie neturi paskirto vadovo, dirbančio tame pačiame skyriuje, sąrašą.

Santykinis

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

Funkcionalus

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

1.5 užduotis

Raskite skyrių ID sąrašą su maksimaliu bendru darbuotojo atlyginimu.

Santykinis

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 )

Funkcionalus

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

Pereikime prie sudėtingesnių užduočių nuo kitos Straipsnis. Jame pateikiama išsami analizė, kaip įgyvendinti šią užduotį MS SQL.

2.1 užduotis

Kurie pardavėjai 1997 m. pardavė daugiau nei 30 vienetų prekės Nr. 1?

Domeno logika (kaip ir anksčiau RDBMS, mes praleidžiame deklaraciją):

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

Santykinis

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

Funkcionalus

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 užduotis

Kiekvienam pirkėjui (vardas, pavardė) suraskite dvi prekes (pavadinimą), kurioms 1997 metais pirkėjas išleido daugiausia pinigų.

Išplečiame domeno logiką iš ankstesnio pavyzdžio:

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

customer = DATA Customer (Order);

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

Santykinis

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

Funkcionalus

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 operatorius veikia tokiu principu: susumuoja po SUM nurodytą išraišką (čia 1), nurodytose grupėse (čia Klientas ir Metai, bet gali būti bet kokia išraiška), rūšiuoja grupėse pagal UŽSAKYME nurodytas išraiškas ( čia pirktas, o jei lygus, tai pagal vidinį prekės kodą).

2.3 užduotis

Kiek prekių reikia užsakyti iš tiekėjų, kad būtų įvykdyti esami užsakymai.

Dar kartą išplėskime domeno logiką:

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

supplier = DATA Supplier (Product);

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

Santykinis

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

Funkcionalus

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 su žvaigždute

Ir paskutinis pavyzdys yra iš manęs asmeniškai. Yra socialinio tinklo logika. Žmonės gali draugauti vieni su kitais ir patikti vienas kitam. Iš funkcinės duomenų bazės perspektyvos tai atrodytų taip:

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

Būtina rasti galimus kandidatus į draugystę. Kalbant formaliau, reikia surasti visus žmones A, B, C taip, kad A draugautų su B, o B draugautų su C, A mėgtų C, bet A nedraugautų su C.
Iš funkcinės duomenų bazės perspektyvos užklausa atrodytų taip:

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

Skaitytojas raginamas savarankiškai išspręsti šią problemą SQL. Manoma, kad draugų yra daug mažiau nei jums patinkančių žmonių. Todėl jie yra atskirose lentelėse. Jei pavyks, taip pat yra užduotis su dviem žvaigždutėmis. Joje draugystė nėra simetriška. Funkcinėje duomenų bazėje tai atrodytų taip:

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: problemos sprendimas su pirmąja ir antra žvaigždute nuo 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 

išvada

Pažymėtina, kad pateikta kalbos sintaksė yra tik vienas iš variantų, kaip įgyvendinti pateiktą koncepciją. SQL buvo laikomas pagrindu ir buvo siekiama, kad jis būtų kuo panašesnis į jį. Žinoma, kai kam gali nepatikti raktinių žodžių pavadinimai, žodžių registrai ir pan. Svarbiausia čia yra pati koncepcija. Jei norite, galite sukurti panašią C++ ir Python sintaksę.

Mano nuomone, aprašyta duomenų bazės koncepcija turi šiuos privalumus:

  • palengvinti. Tai gana subjektyvus rodiklis, kuris paprastais atvejais nėra akivaizdus. Bet jei pažvelgsite į sudėtingesnius atvejus (pavyzdžiui, problemas su žvaigždutėmis), tada, mano nuomone, rašyti tokias užklausas yra daug lengviau.
  • Inkubacija. Kai kuriuose pavyzdžiuose deklaravau tarpines funkcijas (pvz. parduodami, nusipirkau ir kt.), iš kurių buvo kuriamos tolesnės funkcijos. Tai leidžia, jei reikia, keisti tam tikrų funkcijų logiką, nekeičiant nuo jų priklausančių funkcijų logikos. Pavyzdžiui, galite parduoti parduodami buvo skaičiuojami iš visiškai skirtingų objektų, o likusi logika nepasikeis. Taip, tai galima įdiegti RDBMS naudojant CREATE VIEW. Bet jei visa logika taip parašyta, tai atrodys nelabai įskaitoma.
  • Nėra semantinio atotrūkio. Tokia duomenų bazė veikia pagal funkcijas ir klases (vietoj lentelių ir laukų). Kaip ir klasikiniame programavime (jeigu metodas yra funkcija su pirmuoju parametru klasės, kuriai jis priklauso, forma). Atitinkamai, „susidraugauti“ su universaliomis programavimo kalbomis turėtų būti daug lengviau. Be to, ši koncepcija leidžia įgyvendinti daug sudėtingesnes funkcijas. Pavyzdžiui, galite įterpti tokius operatorius:

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

  • Paveldėjimas ir polimorfizmas. Funkcinėje duomenų bazėje galite įvesti daugialypį paveldėjimą per CLASS ClassP: Class1, Class2 konstrukcijas ir įgyvendinti daugialypį polimorfizmą. Kaip tiksliai, tikriausiai parašysiu kituose straipsniuose.

Nors tai tik koncepcija, mes jau turime tam tikrą „Java“ diegimą, kuris visą funkcinę logiką paverčia reliacine logika. Be to, prie jo gražiai pritvirtinta reprezentacijų logika ir daug kitų dalykų, kurių dėka gauname visumą platforma. Iš esmės mes naudojame RDBMS (kol kas tik PostgreSQL) kaip „virtualią mašiną“. Su šiuo vertimu kartais kyla problemų, nes RDBMS užklausų optimizavimo priemonė nežino tam tikros statistikos, kurią žino FDBVS. Teoriškai galima įdiegti duomenų bazių valdymo sistemą, kuri kaip saugyklą naudos tam tikrą struktūrą, pritaikytą būtent funkcinei logikai.

Šaltinis: www.habr.com

Добавить комментарий