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
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
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
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
Forrás: will.com