Funktsionaalne DBMS

Andmebaasimaailma on pikka aega üle võtnud relatsioonilised DBMS-id, mis kasutavad SQL-keelt. Nii palju, et esilekerkivaid sorte nimetatakse NoSQL-iks. Neil õnnestus sellel turul endale kindel koht võita, kuid relatsiooniline DBMS ei sure ja seda kasutatakse jätkuvalt aktiivselt oma eesmärkidel.

Selles artiklis tahan kirjeldada funktsionaalse andmebaasi kontseptsiooni. Parema mõistmise huvides teen seda klassikalise suhtemudeliga võrreldes. Näidetena kasutame ülesandeid erinevatest Internetist leitud SQL-testidest.

Sissejuhatus

Relatsiooniandmebaasid töötavad tabelitel ja väljadel. Funktsionaalses andmebaasis kasutatakse selle asemel klasse ja funktsioone. N võtmega tabeli väli esitatakse N parameetri funktsioonina. Tabelitevaheliste linkide asemel kasutatakse funktsioone, mis tagastavad selle klassi objekte, kuhu link läheb. Funktsioonide koostist kasutatakse asemel JOIN.

Enne otse ülesannete juurde asumist kirjeldan domeeniloogika ülesannet. DDL-i jaoks kasutan PostgreSQL-i süntaksit. Funktsionaalsuse jaoks oma süntaks.

Lauad ja väljad

Lihtne Sku objekt nime- ja hinnaväljadega:

suhteline

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

Funktsionaalne

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

Anname teada kaks funktsioonid, mis võtavad sisendiks ühe Sku parameetri ja tagastavad primitiivse tüübi.

Eeldatakse, et funktsionaalses DBMS-is on igal objektil mingi sisemine kood, mis genereeritakse automaatselt ja millele saab vajadusel juurde pääseda.

Määrame tootele/poele/tarnijale hinna. See võib aja jooksul muutuda, seega lisame tabelisse ajavälja. Koodi lühendamiseks jätan vahele relatsiooniandmebaasi kataloogide tabelite deklareerimise:

suhteline

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

Funktsionaalne

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

Indeksid

Viimase näite puhul koostame kõikidele võtmetele ja kuupäevale indeksi, et saaksime kiiresti leida teatud aja hinna.

suhteline

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

Funktsionaalne

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

ülesanded

Alustame suhteliselt lihtsatest ülesannetest, mis on võetud vastavast artiklid kohta Habr.

Esmalt deklareerime domeeniloogika (relatsiooniandmebaasi puhul tehakse seda otse ülaltoodud artiklis).

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. ülesanne

Kuvage nimekiri töötajatest, kes saavad suuremat palka kui vahetu juhi oma.

suhteline

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

Funktsionaalne

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

1.2. ülesanne

Kuvage oma osakonna kõrgeimat palka teenivate töötajate loend

suhteline

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

Funktsionaalne

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

Mõlemad teostused on samaväärsed. Esimesel juhul saab relatsiooniandmebaasis kasutada CREATE VIEW-i, mis samamoodi arvutab esmalt konkreetse osakonna maksimumpalga. Edaspidi kasutan selguse huvides esimest juhtumit, kuna see kajastab lahendust paremini.

1.3. ülesanne

Kuvage osakonna ID-de loend, mille töötajate arv ei ületa 3 inimest.

suhteline

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

Funktsionaalne

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

1.4. ülesanne

Kuvage nimekiri töötajatest, kellel ei ole samas osakonnas töötavat juhti.

suhteline

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

Funktsionaalne

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

1.5. ülesanne

Leidke osakonna ID-de loend, millel on maksimaalne töötaja kogupalk.

suhteline

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 )

Funktsionaalne

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

Liigume teisest keerulisemate ülesannete juurde artiklid. See sisaldab üksikasjalikku analüüsi selle ülesande rakendamiseks MS SQL-is.

2.1. ülesanne

Millised müüjad müüsid 1997. aastal rohkem kui 30 tükki kaupa nr 1?

Domeeniloogika (nagu varem, jätame RDBMS-i deklaratsiooni vahele):

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

suhteline

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

Funktsionaalne

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. ülesanne

Leidke iga kliendi (eesnimi, perekonnanimi) kohta kaks eset (nimi), millele klient 1997. aastal kõige rohkem raha kulutas.

Domeeniloogika laiendamine eelmisest näitest:

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

customer = DATA Customer (Order);

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

suhteline

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

Funktsionaalne

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;

Operaator PARTITSIOON töötab järgmisel põhimõttel: summeerib SUM (siin 1) järel määratud avaldise määratud rühmade piires (siin Klient ja Aasta, kuid võib olla mis tahes avaldis), sorteerides rühmade sees vastavalt ORDER määratud avaldistele ( siit ostetud ja kui need on võrdsed, siis sisemise tootekoodi järgi).

2.3. ülesanne

Kui palju kaupu tuleb tarnijatelt tellida jooksvate tellimuste täitmiseks.

Laiendame uuesti domeeniloogikat:

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

supplier = DATA Supplier (Product);

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

suhteline

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

Funktsionaalne

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;

Ülesanne tärniga

Ja viimane näide on minult isiklikult. Seal on sotsiaalvõrgustiku loogika. Inimesed võivad olla üksteisega sõbrad ja üksteisele meeldida. Funktsionaalse andmebaasi vaatenurgast näeks see välja järgmine:

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

Sõpruse jaoks on vaja leida võimalikud kandidaadid. Ametlikumalt peate leidma kõik inimesed A, B ja C nii, et A oleks B-ga sõber ja B oleks C-ga sõber, A-le meeldib C, kuid A-le ei meeldi C.
Funktsionaalse andmebaasi seisukohast näeks päring välja järgmine:

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

Lugejal palutakse seda ülesannet SQL-is iseseisvalt lahendada. Eeldatakse, et sõpru on palju vähem kui neid, kellele meeldib. Seetõttu on need eraldi tabelites. Eduka lahenduse korral on probleem ka kahe tärniga. Tema sõprus ei ole sümmeetriline. Funktsionaalses andmebaasis näeks see välja järgmine:

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: ülesande lahendus esimese ja teise tärniga alates 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 

Järeldus

Tuleb märkida, et ülaltoodud keele süntaks on vaid üks ülaltoodud kontseptsiooni rakendamise võimalustest. Aluseks võeti just SQL ja eesmärk oli muuta see sellega võimalikult sarnaseks. Muidugi ei pruugi kellelegi meeldida märksõnade nimed, sõnade suurtähted ja nii edasi. Peamine on siin kontseptsioon ise. Soovi korral saab nii C ++ kui ka Pythoni süntaksi teha sarnaseks.

Kirjeldatud andmebaasi kontseptsioonil on minu arvates järgmised eelised:

  • Lihtsus. See on suhteliselt subjektiivne näitaja, mis pole lihtsatel juhtudel ilmne. Aga kui vaadata keerulisemaid juhtumeid (näiteks tärnidega ülesandeid), siis minu arvates on selliste päringute kirjutamine palju lihtsam.
  • Инкапсуляция. Mõnes näites deklareerisin vahefunktsioone (näiteks müüdud, ostnud jne), millest ehitati üles järgnevad funktsioonid. See võimaldab vajadusel muuta teatud funktsioonide loogikat, muutmata nendest sõltuvate funktsioonide loogikat. Näiteks saate teha müüki müüdud arvutati täiesti erinevate objektide põhjal, samas kui ülejäänud loogika ei muutu. Jah, RDBMS-is saab seda teha kasutades CREATE VIEW. Aga kui kogu loogika niimoodi kirja panna, siis ei näe see eriti loetav välja.
  • Semantilist lõhet pole. Selline andmebaas töötab funktsioonide ja klassidega (tabelite ja väljade asemel). Samamoodi nagu klassikalises programmeerimises (eeldusel, et meetod on funktsioon, mille esimene parameeter on selle klassi kujul, kuhu ta kuulub). Sellest lähtuvalt peaks universaalsete programmeerimiskeeltega “sõpru looma” olema palju lihtsam. Lisaks võimaldab see kontseptsioon rakendada palju keerukamaid funktsioone. Näiteks saate andmebaasi manustada selliseid avaldusi:

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

  • Pärand ja polümorfism. Funktsionaalses andmebaasis saate CLASS ClassP: Class1, Class2 konstruktsioonide kaudu juurutada mitut pärandit ja rakendada mitut polümorfismi. Kuidas täpselt, kirjutan võib-olla järgmistes artiklites.

Kuigi see on vaid kontseptsioon, on meil Java-s juba mõni rakendus, mis muudab kogu funktsionaalse loogika relatsiooniloogikaks. Lisaks on esinduste loogika ja palju muud ilusasti külge keeratud, tänu millele saame terviku platvorm. Põhimõtteliselt kasutame RDBMS-i (seni ainult PostgreSQL) "virtuaalse masinana". See tõlge põhjustab mõnikord probleeme, kuna RDBMS-i päringu optimeerija ei tea teatud statistikat, mida FDBMS teab. Teoreetiliselt on võimalik rakendada andmebaasihaldussüsteemi, mis kasutab mäluna teatud struktuuri, mis on kohandatud spetsiaalselt funktsionaalse loogika jaoks.

Allikas: www.habr.com

Lisa kommentaar