Databaseverdenen er længe blevet overtaget af relationelle DBMS'er, der bruger SQL-sproget. Så meget, at nye varianter kaldes NoSQL. De formåede at vinde en bestemt plads til sig selv på dette marked, men relationelle DBMS vil ikke dø, og fortsætter med at blive brugt aktivt til deres egne formål.
I denne artikel vil jeg beskrive begrebet en funktionel database. For en bedre forståelse vil jeg gøre dette ved at sammenligne med den klassiske relationsmodel. Som eksempler vil vi bruge opgaver fra forskellige SQL-tests fundet på internettet.
Indledning
Relationelle databaser opererer på tabeller og felter. I en funktionel database vil der i stedet blive brugt henholdsvis klasser og funktioner. Et felt i en tabel med N nøgler vil blive repræsenteret som en funktion af N parametre. I stedet for links mellem tabeller vil der blive brugt funktioner, der returnerer objekter fra den klasse, som linket går til. Funktionssammensætning vil blive brugt i stedet for JOIN.
Inden jeg går direkte videre til opgaverne, vil jeg beskrive opgaven med domænelogik. Til DDL vil jeg bruge PostgreSQL-syntaks. For funktionel sin egen syntaks.
Tabeller og felter
Et simpelt Sku-objekt med navn og prisfelter:
relationelle
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
funktionelle
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
Vi annoncerer to funktioner, som tager én Sku-parameter som input og returnerer en primitiv type.
Det antages, at i et funktionelt DBMS vil hvert objekt have en eller anden intern kode, der automatisk genereres og kan tilgås, hvis det er nødvendigt.
Lad os fastsætte prisen for produktet / butikken / leverandøren. Det kan ændre sig over tid, så lad os tilføje et tidsfelt til tabellen. Jeg vil springe erklæringen af tabeller for mapper over i en relationsdatabase for at forkorte koden:
relationelle
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)
)
funktionelle
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
Indekser
For det sidste eksempel, lad os bygge et indeks på alle nøgler og dato, så vi hurtigt kan finde prisen for et bestemt tidspunkt.
relationelle
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
funktionelle
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
opgaver
Lad os starte med relativt simple problemer taget fra de tilsvarende
Lad os først erklære domænelogikken (for en relationel database gøres dette direkte i ovenstående 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);
1.1 udfordring
Vis en liste over medarbejdere, der modtager højere løn end den nærmeste leders.
relationelle
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
funktionelle
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
1.2 udfordring
Vis en liste over medarbejdere, der tjener den højeste løn i deres afdeling
relationelle
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
funktionelle
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));
Begge implementeringer er ækvivalente. Til det første tilfælde i relationsdatabasen kan du bruge CREATE VIEW, som på samme måde først vil beregne den maksimale løn for en bestemt afdeling i den. I fremtiden vil jeg for klarhedens skyld bruge det første tilfælde, da det bedre afspejler løsningen.
1.3 udfordring
Vis en liste over afdelings-id'er, hvor antallet af medarbejdere ikke overstiger 3 personer.
relationelle
select department_id
from employee
group by department_id
having count(*) <= 3
funktionelle
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
1.4 udfordring
Vis en liste over medarbejdere, der ikke har en tilknyttet leder, der arbejder i samme afdeling.
relationelle
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
funktionelle
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
1.5 udfordring
Find listen over afdelings-id'er med den maksimale samlede medarbejderløn.
relationelle
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 )
funktionelle
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();
Lad os gå videre til mere komplekse opgaver fra en anden
2.1 udfordring
Hvilke sælgere solgte mere end 1997 styk af vare #30 i 1?
Domænelogik (som før springer vi erklæringen på RDBMS 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);
relationelle
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
funktionelle
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 udfordring
For hver kunde (fornavn, efternavn) skal du finde de to varer (navn), som kunden brugte flest penge på i 1997.
Udvidelse af domænelogikken fra det foregående eksempel:
CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
relationelle
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
funktionelle
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;
PARTITION-operatoren fungerer efter følgende princip: den summerer udtrykket angivet efter SUM (her 1) inden for de angivne grupper (her Kunde og År, men kan være et hvilket som helst udtryk), sorterer inden for grupperne i henhold til de udtryk, der er angivet i ORDRE (her købt, og hvis lig, så efter den interne produktkode).
2.3 udfordring
Hvor mange varer skal bestilles fra leverandører for at opfylde aktuelle ordrer.
Lad os udvide domænelogikken igen:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
relationelle
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
funktionelle
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;
Opgave med en stjerne
Og det sidste eksempel er fra mig personligt. Der er logikken i et socialt netværk. Folk kan være venner med hinanden og holde af hinanden. Fra et funktionelt databaseperspektiv vil dette se sådan ud:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
Det er nødvendigt at finde mulige kandidater til venskab. Mere formelt skal du finde alle personer A, B, C, sådan at A er venner med B, og B er venner med C, A kan lide C, men A er ikke venner med C.
Fra et funktionelt databasesynspunkt vil forespørgslen se sådan ud:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
Læseren opfordres til selvstændigt at løse dette problem i SQL. Det antages, at der er langt færre venner end dem, der kan lide. Derfor er de i separate tabeller. I tilfælde af en vellykket løsning er der også et problem med to stjerner. Hendes venskab er ikke symmetrisk. På en funktionel database vil det se sådan ud:
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: løsning af problemet med den første og anden stjerne fra
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
Konklusion
Det skal bemærkes, at ovenstående syntaks for sproget kun er en af mulighederne for at implementere ovenstående koncept. Det var SQL, der blev lagt til grund, og målet var at gøre det så lig som muligt. Selvfølgelig kan nogen ikke lide navnene på søgeord, store bogstaver i ord og så videre. Det vigtigste her er selve konceptet. Hvis det ønskes, kan du lave både C ++ og Python lignende syntaks.
Det beskrevne databasekoncept har efter min mening følgende fordele:
- Ease. Dette er en relativt subjektiv indikator, der ikke er indlysende i simple tilfælde. Men hvis du ser på mere komplekse sager (for eksempel opgaver med stjerner), så er det efter min mening meget lettere at skrive sådanne forespørgsler.
- Инкапсуляция. I nogle eksempler erklærede jeg mellemliggende funktioner (f.eks. solgt, købte osv.), hvorfra efterfølgende funktioner blev bygget. Dette giver dig mulighed for at ændre logikken for visse funktioner, hvis det er nødvendigt, uden at ændre logikken for dem, der afhænger af dem. For eksempel kan du lave salg solgt blev beregnet ud fra helt andre objekter, mens resten af logikken ikke ændres. Ja, i RDBMS kan dette gøres ved hjælp af CREATE VIEW. Men hvis du skriver al logikken på denne måde, så vil den ikke se særlig læsbar ud.
- Ingen semantisk kløft. En sådan database opererer med funktioner og klasser (i stedet for tabeller og felter). På samme måde som i klassisk programmering (forudsat at en metode er en funktion med den første parameter i form af en klasse, den tilhører). Derfor burde det være meget nemmere at "blive venner" med universelle programmeringssprog. Derudover giver dette koncept dig mulighed for at implementere meget mere komplekse funktioner. For eksempel kan du indlejre udsagn som dette i databasen:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Arv og polymorfi. I en funktionel database kan du introducere multipel nedarvning gennem CLASS ClassP: Class1, Class2-konstruktionerne og implementere multipel polymorfi. Hvordan præcist vil jeg måske skrive i de følgende artikler.
Selvom dette kun er et koncept, har vi allerede en vis implementering i Java, der oversætter al funktionel logik til relationel logik. Plus, logikken i repræsentationer og en masse andre ting er smukt skruet sammen, takket være hvilket vi får en helhed
Kilde: www.habr.com