Funkcionális DBMS

Az adatbázisok világát régóta a relációs DBMS-ek uralják, amelyek az SQL nyelvet használják. Olyannyira, hogy a megjelenő változatokat NoSQL-nek hívják. Sikerült kivívniuk maguknak egy bizonyos helyet ezen a piacon, de a relációs DBMS-ek nem fognak meghalni, és továbbra is aktívan használják őket céljaikra.

Ebben a cikkben a funkcionális adatbázis fogalmát szeretném leírni. A jobb megértés érdekében ezt úgy teszem, hogy összehasonlítom a klasszikus relációs modellel. Példaként az interneten található különféle SQL-tesztekből származó problémákat használjuk fel.

Bevezetés

A relációs adatbázisok táblákon és mezőkön működnek. Egy funkcionális adatbázisban helyette osztályok és függvények kerülnek felhasználásra. Egy N kulcsot tartalmazó táblázat mezője N paraméter függvényeként jelenik meg. A táblák közötti kapcsolatok helyett olyan függvények kerülnek felhasználásra, amelyek annak az osztálynak az objektumait adják vissza, amelyhez a kapcsolat létrejön. A JOIN helyett a függvényösszetétel lesz használatos.

Mielőtt közvetlenül a feladatokra térnék, leírom a tartománylogika feladatát. A DDL-hez PostgreSQL szintaxist fogok használni. A funkcionálishoz saját szintaxisa van.

Táblázatok és mezők

Egy egyszerű Sku-objektum név- és ármezőkkel:

Relációs

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

funkcionális

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

Kettőt hirdetünk funkciók, amelyek egy Sku paramétert vesznek be bemenetként, és egy primitív típust adnak vissza.

Feltételezzük, hogy egy működő DBMS-ben minden objektum rendelkezik valamilyen belső kóddal, amely automatikusan generálódik, és szükség esetén elérhető.

Állítsuk be a termék/üzlet/beszállító árat. Idővel változhat, ezért adjunk hozzá egy időmezőt a táblázathoz. A kód lerövidítése érdekében kihagyom a táblák deklarálását a relációs adatbázisban lévő könyvtárakhoz:

Relációs

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ális

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

Indexek

Az utolsó példában az összes kulcsra és a dátumra indexet építünk, hogy gyorsan megtaláljuk az árat egy adott időpontra.

Relációs

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

funkcionális

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

feladatok

Kezdjük a megfelelőből vett viszonylag egyszerű problémákkal Cikk a Habr.

Először deklaráljuk a tartomány logikáját (a relációs adatbázis esetében ez közvetlenül a fenti cikkben történik).

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

Jelenítse meg azon alkalmazottak listáját, akik magasabb fizetést kapnak, mint a közvetlen felettesük.

Relációs

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

funkcionális

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

1.2. feladat

Sorolja fel azokat az alkalmazottakat, akik az osztályukon a maximális fizetést kapják

Relációs

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

funkcionális

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

Mindkét megvalósítás egyenértékű. Az első esetben egy relációs adatbázisban használhatjuk a CREATE VIEW-t, amely ugyanígy először kiszámolja egy adott részleg maximális fizetését. A következőkben az egyértelműség kedvéért az első esetet használom, mivel az jobban tükrözi a megoldást.

1.3. feladat

Jelenítse meg a részlegazonosítók listáját, ahol az alkalmazottak száma nem haladja meg a 3 főt.

Relációs

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

funkcionális

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

1.4. feladat

Jelenítse meg azon alkalmazottak listáját, akiknek nincs kijelölt vezetője ugyanazon az osztályon.

Relációs

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ális

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

1.5. feladat

Keresse meg a részlegazonosítók listáját a maximális teljes alkalmazotti fizetéssel.

Relációs

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ális

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

Térjünk át a bonyolultabb feladatokra egy másiktól Cikk. Részletes elemzést tartalmaz a feladat MS SQL-ben való megvalósításáról.

2.1. feladat

Mely eladók adtak el 1997 egységnél többet az 30. számú termékből 1-ben?

Domain logika (ahogy az RDBMS-nél korábban, kihagyjuk a deklarációt):

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ációs

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ális

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

Minden vásárlóhoz (név, vezetéknév) keresse meg azt a két árut (név), amelyre a vevő a legtöbb pénzt költötte 1997-ben.

Bővítjük a tartomány logikáját az előző példából:

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

customer = DATA Customer (Order);

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

Relációs

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ális

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;

A PARTITION operátor a következő elven működik: összegzi a SZUM (itt 1) ​​után megadott kifejezést, a megadott csoportokon belül (itt Ügyfél és Év, de lehet bármilyen kifejezés), a csoportokon belül az ORDER-ben megadott kifejezések szerint rendezi ( itt vásárolt, és ha egyenlő, akkor a belső termékkód szerint).

2.3. feladat

Hány árut kell megrendelni a szállítóktól az aktuális rendelések teljesítéséhez.

Bővítsük újra a tartomány logikáját:

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

supplier = DATA Supplier (Product);

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

Relációs

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ális

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 a csillaggal

És az utolsó példa személyesen tőlem származik. Van egy közösségi hálózat logikája. Az emberek barátok lehetnek egymással és kedvelhetik egymást. Funkcionális adatbázis szempontjából ez így nézne ki:

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

Meg kell találni a lehetséges jelölteket a barátságra. Formálisabban meg kell találnod az összes A, B és C embert úgy, hogy A barát B-vel, B pedig C barátja, A kedveli C-t, de A nem C-vel.
Funkcionális adatbázis szempontjából a lekérdezés a következőképpen nézne ki:

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

Az olvasót arra ösztönzik, hogy ezt a problémát önállóan oldja meg SQL-ben. Feltételezhető, hogy sokkal kevesebb a barátod, mint az, akit kedvelsz. Ezért külön táblázatban vannak. Siker esetén két csillagos feladat is van. Ebben a barátság nem szimmetrikus. Egy funkcionális adatbázison ez így nézne ki:

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: a probléma megoldása az első és a második csillaggal innen 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 

Következtetés

Megjegyzendő, hogy az adott nyelvi szintaxis csak az egyik lehetőség az adott koncepció megvalósítására. Az SQL-t vették alapul, és az volt a cél, hogy minél jobban hasonlítson hozzá. Persze lehet, hogy egyeseknek nem tetszenek a kulcsszavak, szóregiszterek stb. A lényeg itt maga a koncepció. Ha szükséges, a C++ és a Python szintaxisát is hasonlóra állíthatja.

A leírt adatbázis koncepció véleményem szerint a következő előnyökkel rendelkezik:

  • Nyugalom. Ez egy viszonylag szubjektív mutató, amely egyszerű esetekben nem nyilvánvaló. De ha bonyolultabb eseteket nézünk (például a csillagokkal kapcsolatos problémákat), akkor véleményem szerint az ilyen lekérdezések írása sokkal könnyebb.
  • Инкапсуляция. Néhány példában köztes függvényeket deklaráltam (pl. eladott, megvett stb.), amelyekből a későbbi funkciók épültek. Ez lehetővé teszi bizonyos funkciók logikájának megváltoztatását, ha szükséges, anélkül, hogy megváltoztatná a tőlük függő funkciók logikáját. Például értékesíthet eladott teljesen más objektumokból számították ki, míg a logika többi része nem változik. Igen, ez megvalósítható RDBMS-ben a CREATE VIEW segítségével. De ha minden logika így van megírva, akkor nem fog kinézni túl olvashatóan.
  • Nincs szemantikai szakadék. Egy ilyen adatbázis függvényekkel és osztályokkal működik (táblák és mezők helyett). Csakúgy, mint a klasszikus programozásban (ha feltételezzük, hogy egy metódus egy függvény, amelynek az első paramétere annak az osztálynak a formájában, amelyhez tartozik). Ennek megfelelően sokkal könnyebbnek kell lennie az univerzális programozási nyelvekkel „barátkozni”. Ezenkívül ez a koncepció sokkal összetettebb funkciók megvalósítását teszi lehetővé. Például beágyazhat olyan operátorokat, mint:

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

  • Öröklődés és polimorfizmus. Egy funkcionális adatbázisban többszörös öröklődést is bevezethet a CLASS ClassP: Class1, Class2 konstrukciókon keresztül, és többszörös polimorfizmust valósíthat meg. Valószínűleg a következő cikkekben megírom, hogyan.

Annak ellenére, hogy ez csak egy fogalom, már van néhány olyan Java implementációnk, amely az összes funkcionális logikát relációs logikává fordítja. Ráadásul a reprezentációk logikája és sok minden más is szépen hozzá van kötve, aminek köszönhetően egy egészet kapunk emelvény. Lényegében az RDBMS-t (egyelőre csak PostgreSQL-t) használjuk „virtuális gépként”. Néha problémák merülnek fel ezzel a fordítással, mert az RDBMS lekérdezésoptimalizáló nem ismer bizonyos statisztikákat, amelyeket az FDBMS ismer. Elméletileg lehetséges olyan adatbázis-kezelő rendszert megvalósítani, amely egy bizonyos struktúrát használ tárolóként, kifejezetten a funkcionális logikára adaptálva.

Forrás: will.com

Hozzászólás