Verden av databaser har lenge vært dominert av relasjonelle DBMS-er, som bruker SQL-språket. Så mye at nye varianter kalles NoSQL. De klarte å skaffe seg en viss plass i dette markedet, men relasjonelle DBMS-er kommer ikke til å dø, og fortsetter å bli aktivt brukt til deres formål.
I denne artikkelen ønsker jeg å beskrive konseptet med en funksjonell database. For en bedre forståelse vil jeg gjøre dette ved å sammenligne det med den klassiske relasjonsmodellen. Problemer fra ulike SQL-tester funnet på Internett vil bli brukt som eksempler.
Innledning
Relasjonsdatabaser opererer på tabeller og felt. I en funksjonell database vil henholdsvis klasser og funksjoner brukes i stedet. Et felt i en tabell med N nøkler vil bli representert som en funksjon av N parametere. I stedet for relasjoner mellom tabeller vil det brukes funksjoner som returnerer objekter av klassen som koblingen er gjort til. Funksjonssammensetning vil bli brukt i stedet for JOIN.
Før jeg går direkte til oppgavene, vil jeg beskrive oppgaven med domenelogikk. For DDL vil jeg bruke PostgreSQL-syntaks. For funksjonell har den sin egen syntaks.
Tabeller og felt
Et enkelt Sku-objekt med navn og prisfelt:
Relasjonell
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
Funksjonell
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
Vi annonserer to funksjoner, som tar én parameter Sku som input og returnerer en primitiv type.
Det antas at i et funksjonelt DBMS vil hvert objekt ha en eller annen intern kode som genereres automatisk og kan nås om nødvendig.
La oss sette prisen for produktet/butikken/leverandøren. Det kan endre seg over tid, så la oss legge til et tidsfelt i tabellen. Jeg vil hoppe over å deklarere tabeller for kataloger i en relasjonsdatabase for å forkorte koden:
Relasjonell
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)
)
Funksjonell
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
Indekser
For det siste eksemplet vil vi bygge en indeks på alle nøkler og datoen slik at vi raskt kan finne prisen for et bestemt tidspunkt.
Relasjonell
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Funksjonell
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
oppgaver
La oss starte med relativt enkle problemer hentet fra de tilsvarende
Først, la oss erklære domenelogikken (for relasjonsdatabasen gjøres dette direkte i artikkelen ovenfor).
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);
Oppgave 1.1
Vis en liste over ansatte som mottar høyere lønn enn den nærmeste lederen.
Relasjonell
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Funksjonell
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
Oppgave 1.2
List opp de ansatte som får maksimal lønn i sin avdeling
Relasjonell
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
Funksjonell
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 implementeringene er likeverdige. For det første tilfellet kan du i en relasjonsdatabase bruke CREATE VIEW, som på samme måte først vil beregne maksimal lønn for en bestemt avdeling i den. I det følgende, for klarhets skyld, vil jeg bruke det første tilfellet, siden det bedre gjenspeiler løsningen.
Oppgave 1.3
Vis en liste over avdelings-IDer, hvor antall ansatte ikke overstiger 3 personer.
Relasjonell
select department_id
from employee
group by department_id
having count(*) <= 3
Funksjonell
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
Oppgave 1.4
Vis en liste over ansatte som ikke har en utpekt leder som jobber i samme avdeling.
Relasjonell
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
Funksjonell
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
Oppgave 1.5
Finn en liste over avdelings-IDer med maksimal totallønn for ansatte.
Relasjonell
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 )
Funksjonell
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();
La oss gå videre til mer komplekse oppgaver fra en annen
Oppgave 2.1
Hvilke selgere solgte mer enn 1997 enheter av produkt nr. 30 i 1?
Domenelogikk (som før på RDBMS hopper vi over erklæringen):
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);
Relasjonell
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
Funksjonell
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;
Oppgave 2.2
For hver kjøper (navn, etternavn), finn de to varene (navn) som kjøperen brukte mest penger på i 1997.
Vi utvider domenelogikken fra forrige eksempel:
CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
Relasjonell
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
Funksjonell
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 etter følgende prinsipp: den summerer uttrykket spesifisert etter SUM (her 1), innenfor de spesifiserte gruppene (her Customer og Year, men kan være et hvilket som helst uttrykk), sorterer innenfor gruppene etter uttrykkene spesifisert i ORDER ( her kjøpt, og hvis lik, så i henhold til den interne produktkoden).
Oppgave 2.3
Hvor mange varer må bestilles fra leverandører for å oppfylle gjeldende bestillinger.
La oss utvide domenelogikken igjen:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
Relasjonell
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
Funksjonell
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;
Problem med en stjerne
Og det siste eksemplet er fra meg personlig. Det er logikken i et sosialt nettverk. Folk kan være venner med hverandre og like hverandre. Fra et funksjonelt databaseperspektiv vil det se slik ut:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
Det er nødvendig å finne mulige kandidater for vennskap. Mer formelt må du finne alle personer A, B, C slik at A er venn med B, og B er venn med C, A liker C, men A er ikke venn med C.
Fra et funksjonelt databaseperspektiv vil spørringen se slik ut:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
Leseren oppfordres til å løse dette problemet i SQL på egen hånd. Det antas at det er mye færre venner enn folk du liker. Derfor er de i separate tabeller. Hvis vellykket, er det også en oppgave med to stjerner. I den er vennskap ikke symmetrisk. På en funksjonell database vil det se slik ut:
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 på problemet med den første og andre stjernen 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
Konklusjon
Det skal bemerkes at den gitte språksyntaksen bare er ett av alternativene for å implementere det gitte konseptet. SQL ble lagt til grunn, og målet var at den skulle være mest mulig lik den. Selvfølgelig kan det hende at noen ikke liker navnene på nøkkelord, ordregistre osv. Hovedsaken her er selve konseptet. Hvis ønskelig, kan du lage både C++ og Python lignende syntaks.
Det beskrevne databasekonseptet har etter min mening følgende fordeler:
- lette. Dette er en relativt subjektiv indikator som ikke er åpenbar i enkle tilfeller. Men hvis du ser på mer komplekse saker (for eksempel problemer med stjerner), er det etter min mening mye enklere å skrive slike spørsmål.
- innkapsling. I noen eksempler erklærte jeg mellomfunksjoner (f.eks. solgt, kjøpt etc.), som påfølgende funksjoner ble bygget fra. Dette lar deg endre logikken til visse funksjoner, om nødvendig, uten å endre logikken til de som er avhengige av dem. Du kan for eksempel gjøre salg solgt ble beregnet fra helt andre objekter, mens resten av logikken ikke vil endre seg. Ja, dette kan implementeres i en RDBMS ved å bruke CREATE VIEW. Men hvis all logikken er skrevet på denne måten, vil den ikke se særlig lesbar ut.
- Ingen semantisk gap. En slik database opererer på funksjoner og klasser (i stedet for tabeller og felt). Akkurat som i klassisk programmering (hvis vi antar at en metode er en funksjon med den første parameteren i form av klassen den tilhører). Følgelig burde det være mye lettere å "bli venner" med universelle programmeringsspråk. I tillegg tillater dette konseptet at mye mer kompleks funksjonalitet kan implementeres. Du kan for eksempel bygge inn operatører som:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- Arv og polymorfisme. I en funksjonell database kan du introdusere multippel arv gjennom CLASS ClassP: Class1, Class2-konstruksjonene og implementere multippel polymorfisme. Jeg kommer nok til å skrive nøyaktig hvordan i fremtidige artikler.
Selv om dette bare er et konsept, har vi allerede en del implementering i Java som oversetter all funksjonell logikk til relasjonslogikk. I tillegg er logikken til representasjoner og mange andre ting vakkert knyttet til den, takket være at vi får en helhet
Kilde: www.habr.com