De wereld van databases wordt lange tijd gedomineerd door relationele DBMS'en, die de SQL-taal gebruiken. Zozeer zelfs dat opkomende varianten NoSQL worden genoemd. Ze zijn erin geslaagd een bepaalde plaats voor zichzelf te veroveren op deze markt, maar relationele DBMS'en zullen niet uitsterven en actief voor hun doeleinden worden gebruikt.
In dit artikel wil ik het concept van een functionele database beschrijven. Voor een beter begrip zal ik dit doen door het te vergelijken met het klassieke relationele model. Problemen uit verschillende SQL-tests die op internet te vinden zijn, zullen als voorbeeld worden gebruikt.
Introductie
Relationele databases werken met tabellen en velden. In een functionele database zullen in plaats daarvan respectievelijk klassen en functies worden gebruikt. Een veld in een tabel met N sleutels zal worden weergegeven als een functie van N parameters. In plaats van relaties tussen tabellen zullen functies worden gebruikt die objecten retourneren van de klasse waarmee de verbinding is gemaakt. Er zal gebruik worden gemaakt van functiesamenstelling in plaats van JOIN.
Voordat ik direct naar de taken ga, zal ik de taak van domeinlogica beschrijven. Voor DDL gebruik ik de PostgreSQL-syntaxis. Voor functioneel heeft het zijn eigen syntaxis.
Tabellen en velden
Een eenvoudig Sku-object met naam- en prijsvelden:
relationeel
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
functioneel
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
Wij kondigen er twee aan functies, die één parameter Sku als invoer gebruiken en een primitief type retourneren.
Er wordt aangenomen dat in een functioneel DBMS elk object een interne code zal hebben die automatisch wordt gegenereerd en waartoe indien nodig toegang kan worden verkregen.
Laten we de prijs voor het product/winkel/leverancier instellen. Dit kan in de loop van de tijd veranderen, dus laten we een tijdveld aan de tabel toevoegen. Ik sla het declareren van tabellen voor mappen in een relationele database over om de code in te korten:
relationeel
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)
)
functioneel
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
Index
Voor het laatste voorbeeld bouwen we een index op alle sleutels en de datum zodat we snel de prijs voor een bepaalde tijd kunnen vinden.
relationeel
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
functioneel
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
taken
Laten we beginnen met relatief eenvoudige problemen uit de overeenkomstige
Laten we eerst de domeinlogica declareren (voor de relationele database gebeurt dit rechtstreeks in het bovenstaande artikel).
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);
Taak 1.1
Geef een lijst weer van werknemers die een salaris ontvangen dat hoger is dan dat van hun directe leidinggevende.
relationeel
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
functioneel
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
Taak 1.2
Maak een lijst van de werknemers die op hun afdeling het maximale salaris ontvangen
relationeel
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
functioneel
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));
Beide implementaties zijn gelijkwaardig. Voor het eerste geval kunt u in een relationele database CREATE VIEW gebruiken, die op dezelfde manier eerst het maximale salaris voor een specifieke afdeling daarin berekent. In wat volgt, zal ik voor de duidelijkheid het eerste geval gebruiken, omdat dit de oplossing beter weergeeft.
Taak 1.3
Geef een lijst met afdelings-ID's weer, waarbij het aantal werknemers niet groter is dan 3 personen.
relationeel
select department_id
from employee
group by department_id
having count(*) <= 3
functioneel
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
Taak 1.4
Geef een lijst weer van medewerkers die geen toegewezen manager hebben die op dezelfde afdeling werkt.
relationeel
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
functioneel
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
Taak 1.5
Zoek een lijst met afdelings-ID's met het maximale totale werknemerssalaris.
relationeel
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 )
functioneel
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();
Laten we verder gaan met complexere taken van een andere
Taak 2.1
Welke verkopers verkochten in 1997 meer dan 30 eenheden van product nr. 1?
Domeinlogica (zoals eerder op RDBMS slaan we de declaratie over):
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);
relationeel
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
functioneel
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;
Taak 2.2
Zoek voor elke koper (naam, achternaam) de twee goederen (naam) waaraan de koper in 1997 het meeste geld heeft uitgegeven.
We breiden de domeinlogica uit het vorige voorbeeld uit:
CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
relationeel
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
functioneel
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;
De operator PARTITION werkt volgens het volgende principe: hij somt de expressie op die gespecificeerd is na SUM (hier 1), binnen de gespecificeerde groepen (hier Klant en Jaar, maar kan elke expressie zijn), en sorteert binnen de groepen op de expressies gespecificeerd in de ORDER ( hier gekocht, en indien gelijk, dan volgens de interne productcode).
Taak 2.3
Hoeveel goederen moeten er bij leveranciers worden besteld om lopende bestellingen uit te voeren.
Laten we de domeinlogica opnieuw uitbreiden:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
relationeel
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
functioneel
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;
Probleem met een sterretje
En het laatste voorbeeld is van mij persoonlijk. Er is de logica van een sociaal netwerk. Mensen kunnen vrienden met elkaar zijn en elkaar aardig vinden. Vanuit een functioneel databaseperspectief zou het er als volgt uitzien:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
Het is noodzakelijk om mogelijke kandidaten voor vriendschap te vinden. Formeeler moet je alle mensen A, B, C vinden, zodat A bevriend is met B, en B bevriend is met C, A houdt van C, maar A is geen vrienden met C.
Vanuit een functioneel databaseperspectief zou de query er als volgt uitzien:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
De lezer wordt aangemoedigd dit probleem zelf in SQL op te lossen. Er wordt aangenomen dat er veel minder vrienden zijn dan mensen die je leuk vindt. Daarom staan ze in aparte tabellen. Bij succes is er ook een taak met twee sterren. Daarin is vriendschap niet symmetrisch. Op een functionele database zou het er als volgt uitzien:
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: oplossing voor het probleem met het eerste en tweede sterretje uit
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
Conclusie
Opgemerkt moet worden dat de gegeven taalsyntaxis slechts een van de opties is om het gegeven concept te implementeren. SQL werd als basis genomen en het doel was dat het er zo veel mogelijk op zou lijken. Natuurlijk zullen sommigen de namen van trefwoorden, woordregisters, enz. misschien niet leuk vinden. Het belangrijkste hier is het concept zelf. Indien gewenst kunt u zowel C++ als Python een vergelijkbare syntaxis maken.
Het beschreven databaseconcept heeft naar mijn mening de volgende voordelen:
- Gemak. Dit is een relatief subjectieve indicator die in eenvoudige gevallen niet duidelijk is. Maar als je naar complexere gevallen kijkt (bijvoorbeeld problemen met sterretjes), dan is het schrijven van dergelijke vragen naar mijn mening veel eenvoudiger.
- Инкапсуляция. In sommige voorbeelden heb ik tussenfuncties gedeclareerd (bijvoorbeeld uitverkocht, gekocht enz.), waaruit daaropvolgende functies werden opgebouwd. Hierdoor kunt u indien nodig de logica van bepaalde functies wijzigen, zonder de logica te veranderen van de functies die ervan afhankelijk zijn. U kunt bijvoorbeeld verkopen doen uitverkocht werden berekend op basis van totaal verschillende objecten, terwijl de rest van de logica niet zal veranderen. Ja, dit kan worden geïmplementeerd in een RDBMS met behulp van CREATE VIEW. Maar als alle logica op deze manier is geschreven, zal het er niet erg leesbaar uitzien.
- Geen semantische kloof. Zo'n database werkt op functies en klassen (in plaats van op tabellen en velden). Net als bij klassiek programmeren (als we aannemen dat een methode een functie is met als eerste parameter de vorm van de klasse waartoe deze behoort). Dienovereenkomstig zou het veel gemakkelijker moeten zijn om “vrienden te maken” met universele programmeertalen. Bovendien maakt dit concept het mogelijk om veel complexere functionaliteit te implementeren. U kunt bijvoorbeeld operators insluiten zoals:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Overerving en polymorfisme. In een functionele database kunt u meervoudige overerving introduceren via de CLASS ClassP: Class1, Class2-constructies en meervoudig polymorfisme implementeren. Ik zal waarschijnlijk in toekomstige artikelen schrijven hoe precies.
Hoewel dit slechts een concept is, hebben we al een implementatie in Java die alle functionele logica vertaalt in relationele logica. Bovendien is de logica van representaties en een heleboel andere dingen er prachtig aan verbonden, waardoor we een geheel krijgen
Bron: www.habr.com