Funkcionāla DBVS

Datu bāzu pasaulē jau sen dominē relāciju DBVS, kas izmanto SQL valodu. Tik daudz, ka topošie varianti tiek saukti par NoSQL. Viņiem izdevās atvēlēt sev noteiktu vietu šajā tirgū, taču relāciju DBVS nemirst un turpinās aktīvi izmantot saviem mērķiem.

Šajā rakstā es vēlos aprakstīt funkcionālās datu bāzes jēdzienu. Lai labāk izprastu, es to darīšu, salīdzinot to ar klasisko relāciju modeli. Kā piemēri tiks izmantotas problēmas no dažādiem internetā atrodamiem SQL testiem.

Ievads

Relāciju datu bāzes darbojas uz tabulām un laukiem. Funkcionālā datu bāzē tā vietā tiks izmantotas attiecīgi klases un funkcijas. Lauks tabulā ar N taustiņiem tiks attēlots kā N parametru funkcija. Tabulu attiecību vietā tiks izmantotas funkcijas, kas atgriež tās klases objektus, ar kuru tiek izveidots savienojums. JOIN vietā tiks izmantots funkciju sastāvs.

Pirms pāriet tieši uz uzdevumiem, aprakstīšu domēna loģikas uzdevumu. DDL izmantošu PostgreSQL sintaksi. Funkcionālai tam ir sava sintakse.

Tabulas un lauki

Vienkāršs Sku objekts ar nosaukuma un cenas laukiem:

Relāciju

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

Funkcionāls

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

Izziņojam divus функции, kas ņem vienu parametru Sku kā ievadi un atgriež primitīvu tipu.

Tiek pieņemts, ka funkcionālā DBVS katram objektam būs iekšējais kods, kas tiek automātiski ģenerēts un kuram var piekļūt, ja nepieciešams.

Nosakīsim preces/veikala/piegādātāja cenu. Laika gaitā tas var mainīties, tāpēc pievienosim tabulai laika lauku. Es izlaidīšu tabulu deklarēšanu direktorijiem relāciju datu bāzē, lai saīsinātu kodu:

Relāciju

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

Funkcionāls

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

Indeksi

Pēdējā piemērā mēs izveidosim indeksu uz visām atslēgām un datumu, lai mēs varētu ātri atrast cenu konkrētam laikam.

Relāciju

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

Funkcionāls

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

uzdevumi

Sāksim ar salīdzinoši vienkāršām problēmām, kas ņemtas no atbilstošā Raksts uz Habr.

Vispirms deklarēsim domēna loģiku (relāciju datu bāzei tas tiek darīts tieši iepriekš minētajā rakstā).

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

Parādiet to darbinieku sarakstu, kuri saņem lielāku algu nekā viņu tiešais vadītājs.

Relāciju

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

Funkcionāls

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

1.2. uzdevums

Uzskaitiet darbiniekus, kuri savā nodaļā saņem maksimālo algu

Relāciju

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

Funkcionāls

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

Abas ieviešanas ir līdzvērtīgas. Pirmajā gadījumā relāciju datu bāzē var izmantot CREATE VIEW, kas tādā pašā veidā vispirms aprēķinās maksimālo algu konkrētai nodaļai tajā. Turpmāk skaidrības labad es izmantošu pirmo gadījumu, jo tas labāk atspoguļo risinājumu.

1.3. uzdevums

Parādīt nodaļu ID sarakstu, kurā darbinieku skaits nepārsniedz 3 cilvēkus.

Relāciju

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

Funkcionāls

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

1.4. uzdevums

Parādiet to darbinieku sarakstu, kuriem nav iecelta vadītāja, kas strādā tajā pašā nodaļā.

Relāciju

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

Funkcionāls

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

1.5. uzdevums

Atrodiet nodaļu ID sarakstu ar maksimālo darbinieku kopējo algu.

Relāciju

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 )

Funkcionāls

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

Pāriesim pie sarežģītākiem uzdevumiem no cita Raksts. Tajā ir detalizēta analīze par to, kā šo uzdevumu īstenot MS SQL.

2.1. uzdevums

Kuri pārdevēji 1997. gadā pārdeva vairāk nekā 30 vienības preces Nr.1?

Domēna loģika (tāpat kā iepriekš RDBMS mēs izlaižam deklarāciju):

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

Relāciju

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

Funkcionāls

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

Katram pircējam (vārds, uzvārds) atrodiet divas preces (nosaukums), kurām pircējs 1997. gadā iztērēja visvairāk naudas.

Mēs paplašinām domēna loģiku no iepriekšējā piemēra:

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

customer = DATA Customer (Order);

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

Relāciju

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

Funkcionāls

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;

Operators PARTITION darbojas pēc šāda principa: summē aiz SUM norādīto izteiksmi (šeit 1), norādīto grupu ietvaros (šeit Klients un Gads, bet var būt jebkura izteiksme), kārtojot grupu ietvaros pēc PASŪTĪJUMĀ norādītajām izteiksmēm ( šeit nopirkts, un, ja vienāds, tad saskaņā ar iekšējo produkta kodu).

2.3. uzdevums

Cik preces ir jāpasūta no piegādātājiem, lai izpildītu pašreizējos pasūtījumus.

Atkal paplašināsim domēna loģiku:

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

supplier = DATA Supplier (Product);

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

Relāciju

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

Funkcionāls

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;

Problēma ar zvaigznīti

Un pēdējais piemērs ir no manis personīgi. Ir sociālā tīkla loģika. Cilvēki var draudzēties viens ar otru un patikt viens otram. No funkcionālās datu bāzes viedokļa tas izskatītos šādi:

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

Ir jāatrod iespējamie kandidāti draudzībai. Formālāk sakot, jums ir jāatrod visi cilvēki A, B, C tā, lai A būtu draugs ar B un B būtu draugs ar C, A patīk C, bet A nedraudzētos ar C.
No funkcionālās datu bāzes viedokļa vaicājums izskatītos šādi:

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

Lasītājs tiek mudināts patstāvīgi atrisināt šo problēmu SQL. Tiek pieņemts, ka draugu ir daudz mazāk nekā cilvēku, kas jums patīk. Tāpēc tie ir atsevišķās tabulās. Ja izdodas, ir arī uzdevums ar divām zvaigznēm. Tajā draudzība nav simetriska. Funkcionālā datu bāzē tas izskatītos šādi:

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: problēmas risinājums ar pirmo un otro zvaigznīti no 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 

Secinājums

Jāpiebilst, ka dotā valodas sintakse ir tikai viena no dotās koncepcijas realizācijas iespējām. SQL tika ņemts par pamatu, un mērķis bija, lai tas būtu pēc iespējas līdzīgāks tam. Protams, dažiem var nepatikt atslēgvārdu nosaukumi, vārdu reģistri utt. Šeit galvenais ir pati koncepcija. Ja vēlaties, varat izveidot līdzīgu sintaksi gan C++, gan Python.

Aprakstītajai datu bāzes koncepcijai, manuprāt, ir šādas priekšrocības:

  • Vienkāršība. Tas ir salīdzinoši subjektīvs rādītājs, kas vienkāršos gadījumos nav acīmredzams. Bet, ja paskatās uz sarežģītākiem gadījumiem (piemēram, problēmas ar zvaigznītēm), tad, manuprāt, šādu vaicājumu rakstīšana ir daudz vienkāršāka.
  • Iekapsulēšana. Dažos piemēros es deklarēju starpfunkcijas (piemēram, pārdots, nopirka utt.), no kā tika uzbūvētas turpmākās funkcijas. Tas ļauj vajadzības gadījumā mainīt noteiktu funkciju loģiku, nemainot no tām atkarīgo funkciju loģiku. Piemēram, jūs varat veikt pārdošanu pārdots tika aprēķināti no pilnīgi citiem objektiem, savukārt pārējā loģika nemainīsies. Jā, to var ieviest RDBMS, izmantojot CREATE VIEW. Bet, ja visa loģika ir uzrakstīta šādi, tas neizskatīsies īpaši lasāms.
  • Nav semantiskas plaisas. Šāda datu bāze darbojas ar funkcijām un klasēm (nevis tabulām un laukiem). Tāpat kā klasiskajā programmēšanā (ja pieņemam, ka metode ir funkcija ar pirmo parametru tās klases formā, kurai tā pieder). Attiecīgi ar universālajām programmēšanas valodām vajadzētu būt daudz vieglāk “sadraudzēties”. Turklāt šī koncepcija ļauj īstenot daudz sarežģītāku funkcionalitāti. Piemēram, varat iegult operatorus, piemēram:

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

  • Mantojums un polimorfisms. Funkcionālā datu bāzē varat ieviest vairāku mantojumu, izmantojot CLASS ClassP: Class1, Class2 konstrukcijas un ieviest vairākus polimorfismus. Droši vien turpmākajos rakstos rakstīšu, kā tieši.

Lai gan tas ir tikai jēdziens, mums jau ir kāda Java ieviešana, kas visu funkcionālo loģiku pārvērš relāciju loģikā. Turklāt tai ir skaisti pievienota attēlojumu loģika un daudzas citas lietas, pateicoties kurām mēs iegūstam veselumu platforma. Būtībā mēs izmantojam RDBMS (pagaidām tikai PostgreSQL) kā “virtuālu mašīnu”. Dažreiz ar šo tulkojumu rodas problēmas, jo RDBMS vaicājumu optimizētājs nezina noteiktu statistiku, ko zina FDBMS. Teorētiski ir iespējams ieviest datu bāzes pārvaldības sistēmu, kas kā krātuvi izmantos noteiktu struktūru, kas pielāgota tieši funkcionālajai loģikai.

Avots: www.habr.com

Pievieno komentāru