DBMS funksionale

Bota e bazave të të dhënave ka qenë prej kohësh e dominuar nga DBMS-të relacionale, të cilat përdorin gjuhën SQL. Aq shumë sa që variantet në zhvillim quhen NoSQL. Ata arritën të krijojnë një vend të caktuar për veten e tyre në këtë treg, por DBMS-të relacionale nuk do të vdesin dhe do të vazhdojnë të përdoren në mënyrë aktive për qëllimet e tyre.

Në këtë artikull dua të përshkruaj konceptin e një baze të dhënash funksionale. Për një kuptim më të mirë, këtë do ta bëj duke e krahasuar me modelin klasik relacional. Problemet nga teste të ndryshme SQL të gjetura në internet do të përdoren si shembuj.

Paraqitje

Bazat e të dhënave relacionale funksionojnë në tabela dhe fusha. Në një bazë të dhënash funksionale, klasat dhe funksionet do të përdoren në vend të tyre, përkatësisht. Një fushë në një tabelë me N çelësa do të përfaqësohet si një funksion i parametrave N. Në vend të marrëdhënieve ndërmjet tabelave, do të përdoren funksione që kthejnë objektet e klasës me të cilën është bërë lidhja. Përbërja e funksionit do të përdoret në vend të JOIN.

Para se të kaloj drejtpërdrejt në detyrat, unë do të përshkruaj detyrën e logjikës së domenit. Për DDL do të përdor sintaksën PostgreSQL. Për funksionalitet ka sintaksën e vet.

Tabelat dhe fushat

Një objekt i thjeshtë Sku me fushat e emrit dhe çmimit:

Relacionale

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

funksionale

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

Ne shpallim dy funksionet, të cilat marrin një parametër Sku si hyrje dhe kthejnë një tip primitiv.

Supozohet se në një DBMS funksionale çdo objekt do të ketë një kod të brendshëm që gjenerohet automatikisht dhe mund të aksesohet nëse është e nevojshme.

Le të vendosim çmimin për produktin/dyqanin/furnizuesin. Mund të ndryshojë me kalimin e kohës, kështu që le të shtojmë një fushë kohore në tabelë. Do të kapërcej deklarimin e tabelave për drejtoritë në një bazë të dhënash relacionale për të shkurtuar kodin:

Relacionale

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

funksionale

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

Tregues

Për shembullin e fundit, ne do të ndërtojmë një indeks për të gjithë çelësat dhe datën në mënyrë që të mund të gjejmë shpejt çmimin për një kohë të caktuar.

Relacionale

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

funksionale

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

detyrat

Le të fillojmë me probleme relativisht të thjeshta të marra nga ato përkatëse Artikull në Habr.

Së pari, le të deklarojmë logjikën e domenit (për bazën e të dhënave relacionale kjo bëhet drejtpërdrejt në artikullin e mësipërm).

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

Problemi 1.1

Shfaq një listë të punonjësve që marrin një pagë më të madhe se ajo e mbikëqyrësit të tyre të menjëhershëm.

Relacionale

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

funksionale

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

Problemi 1.2

Listoni punonjësit që marrin pagën maksimale në departamentin e tyre

Relacionale

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

funksionale

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

Të dy implementimet janë ekuivalente. Për rastin e parë, në një bazë të dhënash relacionale mund të përdorni CREATE VIEW, i cili në të njëjtën mënyrë fillimisht do të llogarisë pagën maksimale për një departament të caktuar në të. Në vijim, për qartësi, do të përdor rastin e parë, pasi pasqyron më mirë zgjidhjen.

Problemi 1.3

Shfaq një listë të ID-ve të departamentit, numri i punonjësve në të cilin nuk i kalon 3 persona.

Relacionale

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

funksionale

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

Problemi 1.4

Shfaq një listë të punonjësve që nuk kanë një menaxher të caktuar që punon në të njëjtin departament.

Relacionale

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

funksionale

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

Problemi 1.5

Gjeni një listë të ID-ve të departamentit me pagën maksimale totale të punonjësve.

Relacionale

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 )

funksionale

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

Le të kalojmë në detyra më komplekse nga një tjetër Artikull. Ai përmban një analizë të detajuar se si të zbatohet kjo detyrë në MS SQL.

Problemi 2.1

Cilët shitës shitën më shumë se 1997 njësi të produktit nr. 30 në 1?

Logjika e domenit (si më parë në RDBMS ne e kapërcejmë deklaratën):

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

Relacionale

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

funksionale

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;

Problemi 2.2

Për çdo blerës (emër, mbiemër), gjeni dy mallrat (emri) për të cilat blerësi shpenzoi më shumë para në vitin 1997.

Ne zgjerojmë logjikën e domenit nga shembulli i mëparshëm:

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

customer = DATA Customer (Order);

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

Relacionale

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

funksionale

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;

Operatori PARTITION punon në parimin e mëposhtëm: ai përmbledh shprehjen e specifikuar pas SUM (këtu 1), brenda grupeve të specifikuara (këtu Klienti dhe Viti, por mund të jetë çdo shprehje), duke renditur brenda grupeve sipas shprehjeve të specifikuara në ORDER ( blerë këtu, dhe nëse është e barabartë, atëherë sipas kodit të brendshëm të produktit).

Problemi 2.3

Sa mallra duhet të porositen nga furnitorët për të përmbushur porositë aktuale.

Le të zgjerojmë përsëri logjikën e domenit:

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

supplier = DATA Supplier (Product);

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

Relacionale

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

funksionale

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;

Problem me një yll

Dhe shembulli i fundit është nga unë personalisht. Ekziston logjika e një rrjeti social. Njerëzit mund të jenë miq me njëri-tjetrin dhe të pëlqejnë njëri-tjetrin. Nga një këndvështrim funksional i bazës së të dhënave do të duket kështu:

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

Është e nevojshme të gjenden kandidatë të mundshëm për miqësi. Më formalisht, ju duhet t'i gjeni të gjithë njerëzit A, B, C në mënyrë që A të jetë shok me B, dhe B të jetë shok me C, A të pëlqen C, por A nuk është mik me C.
Nga një këndvështrim funksional i bazës së të dhënave, pyetja do të dukej kështu:

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

Lexuesi inkurajohet ta zgjidhë vetë këtë problem në SQL. Supozohet se ka shumë më pak miq se njerëzit që ju pëlqen. Prandaj ato janë në tabela të veçanta. Nëse është e suksesshme, ekziston edhe një detyrë me dy yje. Në të, miqësia nuk është simetrike. Në një bazë të dhënash funksionale do të duket kështu:

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: zgjidhja e problemit me yllin e parë dhe të dytë nga 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 

Përfundim

Duhet të theksohet se sintaksa e gjuhës së dhënë është vetëm një nga opsionet për zbatimin e konceptit të dhënë. SQL u mor si bazë dhe qëllimi ishte që ajo të ishte sa më e ngjashme me të. Sigurisht, disave mund të mos ju pëlqejnë emrat e fjalëve kyçe, regjistrat e fjalëve, etj. Gjëja kryesore këtu është vetë koncepti. Nëse dëshironi, mund të bëni sintaksë të ngjashme si C++ ashtu edhe Python.

Koncepti i përshkruar i bazës së të dhënave, për mendimin tim, ka përparësitë e mëposhtme:

  • Lehtësi. Ky është një tregues relativisht subjektiv që nuk është i dukshëm në raste të thjeshta. Por nëse shikoni raste më komplekse (për shembull, problemet me yjet), atëherë, për mendimin tim, shkrimi i pyetjeve të tilla është shumë më i lehtë.
  • Инкапсуляция. Në disa shembuj kam deklaruar funksione të ndërmjetme (për shembull, shitur, blerë etj.), nga i cili u ndërtuan funksionet e mëvonshme. Kjo ju lejon të ndryshoni logjikën e funksioneve të caktuara, nëse është e nevojshme, pa ndryshuar logjikën e atyre që varen prej tyre. Për shembull, ju mund të bëni shitje shitur janë llogaritur nga objekte krejtësisht të ndryshme, ndërsa pjesa tjetër e logjikës nuk do të ndryshojë. Po, kjo mund të zbatohet në një RDBMS duke përdorur CREATE VIEW. Por nëse e gjithë logjika shkruhet në këtë mënyrë, nuk do të duket shumë e lexueshme.
  • Nuk ka boshllëk semantik. Një bazë e tillë e të dhënave funksionon në funksione dhe klasa (në vend të tabelave dhe fushave). Ashtu si në programimin klasik (nëse supozojmë se një metodë është një funksion me parametrin e parë në formën e klasës së cilës i përket). Prandaj, duhet të jetë shumë më e lehtë të "krijosh miq" me gjuhët universale të programimit. Për më tepër, ky koncept lejon zbatimin e funksionalitetit shumë më kompleks. Për shembull, mund të futni operatorë si:

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

  • Trashëgimia dhe polimorfizmi. Në një bazë të dhënash funksionale, ju mund të prezantoni trashëgimi të shumëfishtë përmes KLASËS ClassP: Class1, Class2 ndërton dhe implementon polimorfizëm të shumëfishtë. Ndoshta do të shkruaj saktësisht se si në artikujt e ardhshëm.

Edhe pse ky është vetëm një koncept, ne tashmë kemi një zbatim në Java që përkthen të gjithë logjikën funksionale në logjikë relacionale. Plus, logjika e përfaqësimeve dhe shumë gjëra të tjera janë ngjitur bukur me të, falë së cilës ne marrim një të tërë. platformë. Në thelb, ne përdorim RDBMS (vetëm PostgreSQL tani për tani) si një "makinë virtuale". Nganjëherë lindin probleme me këtë përkthim sepse optimizuesi i pyetjeve RDBMS nuk njeh statistika të caktuara që i di FDBMS. Në teori, është e mundur të zbatohet një sistem i menaxhimit të bazës së të dhënave që do të përdorë një strukturë të caktuar si ruajtje, të përshtatur posaçërisht për logjikën funksionale.

Burimi: www.habr.com

Shto një koment