DBMS funtzionala

Datu baseen mundua SQL lengoaia erabiltzen duten DBMS erlazionalek hartu dute aspalditik. Hainbeste non sortzen ari diren barietateei NoSQL deitzen zaie. Merkatu honetan toki jakin bat irabaztea lortu zuten, baina DBMS erlazionalak ez dira hilko, eta aktiboki erabiltzen jarraitzen dute beren helburuetarako.

Artikulu honetan, datu-base funtzionalaren kontzeptua deskribatu nahi dut. Hobeto ulertzeko, eredu erlazional klasikoarekin alderatuz egingo dut. Adibide gisa, Interneten aurkitutako SQL proba ezberdinetako zereginak erabiliko dira.

Sarrera

Erlazio datu-baseek tauletan eta eremuetan funtzionatzen dute. Datu-base funtzional batean, klaseak eta funtzioak erabiliko dira horren ordez, hurrenez hurren. N gakoak dituen taula bateko eremu bat N parametroen arabera irudikatuko da. Taulen arteko esteken ordez, esteka doan klaseko objektuak itzultzen dituzten funtzioak erabiliko dira. Funtzio-konposizioa JOIN-en ordez erabiliko da.

Zereginetara zuzenean jarraitu aurretik, domeinu-logikaren zeregina deskribatuko dut. DDLrako, PostgreSQL sintaxia erabiliko dut. Funtzionalerako bere sintaxia.

Taulak eta eremuak

Sku objektu sinple bat izen eta prezio eremuekin:

erlazionala

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

funtzionala

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

Bi iragartzen ditugu funtzio, Sku parametro bat sarrera gisa hartzen dutenak eta mota primitibo bat itzultzen dutenak.

Suposatzen da DBMS funtzional batean, objektu bakoitzak automatikoki sortzen den barne-koderen bat izango duela eta behar izanez gero atzitu daitekeela.

Ezar dezagun produktuaren / dendaren / hornitzailearen prezioa. Denborarekin alda daiteke, beraz, gehi diezaiogun denbora-eremu bat taulari. Datu-base erlazional bateko taulen deklarazioa saltatuko dut kodea laburtzeko:

erlazionala

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

funtzionala

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

Indizeak

Azken adibiderako, eraiki dezagun indize bat gako eta data guztietan, denbora jakin baterako prezioa azkar aurkitu ahal izateko.

erlazionala

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

funtzionala

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

zereginak

Has gaitezen dagokionetik hartutako problema erraz samarrak Artikulua on Habr.

Lehenik eta behin, deklaratu dezagun domeinuaren logika (erlaziozko datu-base baterako, hau zuzenean goiko artikuluan egiten da).

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. ataza

Erakutsi berehalako arduradunarenak baino soldata handiagoak jasotzen dituzten langileen zerrenda.

erlazionala

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

funtzionala

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

1.2. ataza

Erakutsi beren sailean soldatarik handiena irabazten duten langileen zerrenda

erlazionala

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

funtzionala

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

Bi inplementazio baliokideak dira. Datu-base erlazionaleko lehen kasurako, SORTU IKUSPEGIA erabil dezakezu, era berean, lehenik eta behin, bertan dagoen sail jakin baterako gehienezko soldata kalkulatuko duena. Etorkizunean, argitzeko, lehen kasua erabiliko dut, irtenbidea hobeto islatzen baitu.

1.3. ataza

Bistaratu saileko IDen zerrenda, zeinetan langile kopurua 3 pertsona baino gehiagokoa ez den.

erlazionala

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

funtzionala

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

1.4. ataza

Erakutsi sail berean lanean esleitutako zuzendaririk ez duten langileen zerrenda.

erlazionala

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

funtzionala

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

1.5. ataza

Bilatu langileen gehienezko soldata osoa duten saileko IDen zerrenda.

erlazionala

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 )

funtzionala

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

Joan gaitezen zeregin konplexuagoetara beste batetik Artikulua. Zeregin hori MS SQL-n nola inplementatu jakiteko azterketa zehatza dauka.

2.1. ataza

Zein saltzaileek 1997. elementuaren 30 pieza baino gehiago saldu zituzten 1an?

Domeinu-logika (lehen bezala, RDBMS-en deklarazioa saltatzen dugu):

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

erlazionala

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

funtzionala

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. ataza

Bezero bakoitzeko (izen-abizenak), bilatu bezeroak diru gehien gastatu zuen bi elementu (izena) 1997an.

Aurreko adibideko domeinu-logika hedatuz:

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

customer = DATA Customer (Order);

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

erlazionala

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

funtzionala

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 operadoreak printzipio honen arabera funtzionatzen du: SUM (hemen 1) ondoren zehaztutako adierazpena batutzen du zehaztutako taldeen barruan (hemen Bezeroa eta Urtea, baina edozein adierazpen izan daiteke), ORDENA atalean zehaztutako esamoldeen arabera ordenatuz taldeetan ( hemen erosi, eta berdinak badira, barruko produktuaren kodearen arabera).

2.3. ataza

Hornitzaileei zenbat salgai eskatu behar zaizkien unean uneko eskaerak betetzeko.

Zabal dezagun berriro domeinuaren logika:

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

supplier = DATA Supplier (Product);

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

erlazionala

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

funtzionala

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;

Zeregin izartxo batekin

Eta azken adibidea pertsonalki nirea da. Sare sozial baten logika dago. Jendea elkarren lagun izan daiteke eta elkar gustatu. Datu-base funtzionalaren ikuspegitik, hau itxura hau izango litzateke:

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

Beharrezkoa da adiskidetasunerako hautagai posibleak aurkitzea. Formalkiago, A, B, C pertsona guztiak aurkitu behar dituzu, A B-ren laguna dela eta B C-ren laguna dela, A-k C gustatzen zaiola, baina A ez da C-ren laguna.
Datu-base funtzionalaren ikuspuntutik, kontsultak honela izango luke:

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

Irakurleari arazo hau modu independentean konpontzera gonbidatzen da SQLn. Suposatzen da gustuko dutenak baino askoz lagun gutxiago daudela. Hori dela eta, taula bereizietan daude. Konponbide arrakastatsua izanez gero, bi izarrekin arazo bat ere badago. Bere adiskidetasuna ez da simetrikoa. Datu-base funtzional batean honela izango litzateke:

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: lehenengo eta bigarren izartxoarekin arazoaren konponbidea 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 

Ondorioa

Kontuan izan behar da hizkuntzaren goiko sintaxia aurreko kontzeptua ezartzeko aukeretako bat besterik ez dela. SQL izan zen oinarritzat hartu zena, eta helburua ahalik eta antzekoena egitea zen. Jakina, norbaitek hitz gakoen izenak, hitzen kasuak, etab. Hemen gauza nagusia kontzeptua bera da. Nahi izanez gero, C ++ eta Python antzeko sintaxia egin ditzakezu.

Deskribatutako datu-basearen kontzeptuak, nire ustez, abantaila hauek ditu:

  • arintzeko. Adierazle nahiko subjektiboa da, kasu errazetan agerikoa ez dena. Baina kasu konplexuagoak aztertzen badituzu (adibidez, izartxoak dituzten atazak), orduan, nire ustez, horrelako kontsultak idaztea askoz errazagoa da.
  • Π˜Π½ΠΊΠ°ΠΏΡΡƒΠ»ΡΡ†ΠΈΡ. Adibide batzuetan, bitarteko funtzioak deklaratu ditut (adibidez, saldu, erosi etab.), zeinetatik ondorengo funtzioak eraiki ziren. Horrek funtzio batzuen logika aldatzeko aukera ematen du, behar izanez gero, horien menpe daudenen logika aldatu gabe. Adibidez, salmentak egin ditzakezu saldu objektu guztiz desberdinetatik kalkulatu ziren, gainerako logika ez da aldatuko. Bai, RDBMSn hau CREATE VIEW-rekin egin daiteke. Baina logika guztia horrela idazten baduzu, ez da oso irakurgarria izango.
  • Hutsune Semantikorik ez. Datu-base horrek funtzio eta klaseekin funtzionatzen du (taulen eta eremuen ordez). Programazio klasikoan gertatzen den moduan (metodo bat dagokion klase moduan lehen parametroa duen funtzioa dela suposatuz). Horren arabera, askoz errazagoa izan beharko litzateke programazio lengoaia unibertsalekin "lagunak egitea". Gainera, kontzeptu honek askoz funtzio konplexuagoak ezartzeko aukera ematen du. Adibidez, honelako adierazpenak txerta ditzakezu datu-basean:

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

  • Herentzia eta polimorfismoa. Datu-base funtzional batean, herentzia anitza sar dezakezu CLASS ClassP-ren bidez: Class1, Class2 eraikitzen eta polimorfismo anitz inplementatu. Nola zehazki, beharbada hurrengo artikuluetan idatziko dut.

Nahiz eta kontzeptu bat besterik ez den, dagoeneko badugu Java-n logika funtzional guztia erlazio-logika bihurtzen duen inplementazioren bat. Gainera, irudikapenen logika eta beste gauza asko ederki izorratuta daude, horri esker osotasun bat lortzen dugu plataforma. Funtsean, RDBMS bat erabiltzen ari gara (orain arte PostgreSQL bakarrik) "makina birtual" gisa. Itzulpen honek batzuetan arazoak sortzen ditu RDBMS kontsulta-optimizatzaileak ez dituelako ezagutzen FDBMSak egiten dituen estatistikak. Teorian, biltegiratze gisa egitura jakin bat erabiliko duen datu-baseak kudeatzeko sistema bat ezartzea posible da, logika funtzionalerako bereziki egokitua.

Iturria: www.habr.com

Gehitu iruzkin berria