Verdenen af databaser er længe blevet domineret af relationelle DBMS, hvor SQL-sproget anvendes. Så meget, at de nye varianter kaldes NoSQL. De har formået at finde deres plads på dette marked, men relationelle DBMS er ikke ved at forsvinde og anvendes fortsat aktivt til deres formål.
I denne artikel vil jeg beskrive konceptet med en funktionel database. For bedre forståelse vil jeg gøre det ved at sammenligne med den klassiske relationelle model. Som eksempler vil der blive brugt opgaver fra forskellige SQL-tests, som findes på internettet.
Introduktion
Relationelle databaser opererer med tabeller og felter. I en funktionel database vil der i stedet blive brugt klasser og funktioner. Et felt i en tabel med N nøgler vil blive præsenteret som en funktion af N parametre. I stedet for forbindelser mellem tabellerne vil der blive brugt funktioner, der returnerer objekter af den klasse, der er i forbindelse. I stedet for JOIN vil der blive anvendt funktionskomposition.
Før vi går videre til opgaverne, vil jeg beskrive opgaven med domænelogik. Jeg vil bruge PostgreSQL-syntaks for DDL. Til det funktionelle vil jeg bruge min egen syntaks.
Tabeller og felter
Et simpelt objekt Sku med felterne navn og pris:
Relationel
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
Funktionel
KLASSE Sku;
navn = DATA STRENG[100] (Sku);
pris = DATA NUMERISK[10,5] (Sku);
Vi erklærer to funktioner, som tager et enkelt parameter Sku som input og returnerer en primitiv datatype.
Det antages, at hver objekt i en funktionel DBMS vil have en intern kode, som genereres automatisk, og som kan tilgås efter behov.
Vi sætter prisen for produktet / butikken / leverandøren. Denne kan ændre sig over tid, så vi tilføjer et felt til tid i tabellen. Jeg vil undlade at angive tabellerne for ordbøger i den relationelle database for at forkorte koden:
Relationel
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)
)
Funktionel
KLASSE Sku;
KLASSE Butik;
KLASSE Leverandør;
dateTime = DATA DATETIME (Sku, Butik, Leverandør);
pris = DATA NUMERISK[10,5] (Sku, Butik, Leverandør);
Indekser
For det sidste eksempel vil vi opbygge et indeks baseret på alle nøgler og datoen, så det hurtigt kan finde prisen på et bestemt tidspunkt.
Relationel
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Funktionel
INDEX Sku sk, Butik st, Leverandør sp, dateTime(sk, st, sp);
Opgaver
Lad os begynde med relativt simple opgaver, taget fra den relevante på Habr.
Lad os først definere domænelogikken (for relationsdatabaser er dette gjort direkte i den nævnte artikel).
KLASS Department;
navn = DATA STRINGS[100] (Department);
CLASS Employee;
department = DATA Department (Employee);
chief = DATA Employee (Employee);
name = DATA STRING[100] (Employee);
salary = DATA NUMERIC[14,2] (Employee);
Opgave 1.1
Vis en liste over medarbejdere, der har en højere løn end deres umiddelbare leder.
Relationel
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Funktionel
VÆLG navn(Ansat a) HVOR løn(a) > løn(chef(a));
Opgave 1.2
Vis en liste over medarbejdere, der har den højeste løn i deres afdeling
Relationel
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
Funktionel
maxSalary 'Maksimal løn' (Afdeling s) =
GRUPPER MAX løn(Ansat e) HVIS afdeling(e) = s;
VÆLG navn(Ansat a) HVOR løn(a) = maxSalary(afdeling(a));
// или если "заинлайнить"
SELECT name(Employee a) WHERE
salary(a) = maxSalary(GROUP MAX salary(Employee e) IF department(e) = department(a));
Begge implementeringer er ækvivalente. I det første tilfælde kan man i relationsdatabasen bruge CREATE VIEW, som på samme måde først vil beregne den maksimale løn for en konkret afdeling. Fremadrettet vil jeg for klarhedens skyld bruge det første tilfælde, da det bedre afspejler løsningen.
Opgave 1.3
Vis en liste over ID'er på afdelinger, hvor antallet af medarbejdere ikke overstiger 3.
Relationel
select department_id
from employee
group by department_id
having count(*) <= 3
Funktionel
countEmployees 'Antal medarbejdere' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
Opgave 1.4
Vis en liste over medarbejdere, der ikke har en tildelt leder, som arbejder i samme afdeling.
Relationel
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
Funktionel
VÆLG navn(Fuldtid a) HVOR IKKE (afdeling(chief(a)) = afdeling(a));
Opgave 1.5
Find en liste over ID'er på afdelinger med den maksimale samlede løn for medarbejdere.
Relationel
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 )
Funktionel
salarySum 'Maksimal løn' (Department d) =
GROUP SUM salary(Employee e) IF department(e) = d;
maxSalarySum 'Maksimal løn for afdelinger' () =
GROUP MAX salarySum(Department d);
SELECT Department d WHERE salarySum(d) = maxSalarySum();
Lad os gå videre til mere komplekse opgaver fra en anden. . Her er der en detaljeret gennemgang af, hvordan man implementerer denne opgave i MS SQL.
Opgave 2.1
Hvilke sælgere solgte mere end 30 stykker af produkt nr. 1 i 1997?
Domænelogik (som før i RDBMS springer vi deklarationen over):
CLASS Employee 'Sælger';
lastName 'Efternavn' = DATA STRING[100] (Employee);
CLASS Product 'Produkt';
id = DATA INTEGER (Product);
name = DATA STRING[100] (Product);
CLASS Order 'Bestilling';
date = DATA DATE (Order);
employee = DATA Employee (Order);
CLASS Detail 'Ordrelinje';
order = DATA Order (Detail);
product = DATA Product (Detail);
quantity = DATA NUMERIC[10,5] (Detail);
Relationel
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
Funktionel
solgt (Medarbejder e, INTEGER produktId, INTEGER år) =
GRUPPER SUM mængde(OrderDetail d) HVIS
medarbejder(order(d)) = e OG
id(produkt(d)) = produktId OG
udtrækÅr(dato(order(d))) = år;
VÆLG efternavn(Medarbejder e) HVOR solgt(e, 1, 1997) > 30;
Opgave 2.2
For hver kunde (fornavn, efternavn) find to produkter (navn), som kunden har brugt flest penge på i 1997.
Vi udvider domænelogikken fra det foregående eksempel:
CLASS Customer 'Kunde';
contactName 'Navn' = DATA STRING[100] (Customer);
kunde = DATA Customer (Ordre);
enhedspris = DATA NUMERIC[14,2] (Detalje);
rabat = DATA NUMERIC[6,2] (Detalje);
Relationel
VÆLG KontaktNavn, ProduktNavn FRA (
VÆLG c.KontaktNavn, p.ProduktNavn,
ROW_NUMBER() OVER (
PARTITION BY c.KontaktNavn
BESTILLED EFTER SUM(od.Mængde * od.Enhedspris * (1 - od.Rabat)) DESC
) AS RatingByAmt
FRA Kunder c
JOIN Ordrer o ON o.KundeID = c.KundeID
JOIN [Ordre Detaljer] od ON od.OrdreID = o.OrdreID
JOIN Produkter p ON p.ProduktID = od.ProduktID
HVOR ÅR(o.OrdreDato) = 1997
GRUPPER EFTER c.KontaktNavn, p.ProduktNavn
) t
HVOR RatingByAmt < 3
Funktionel
sum (Detail d) = antal(d) * enhedspris(d) * (1 - rabat(d));
købt 'Købt' (Kunde c, Produkt p, INTEGER y) =
GRUPPER SUM sum(Detalje d) HVIS
kunde(ordrer(d)) = c OG
produkt(d) = p OG
udtrækÅr(dato(order(d))) = y;
bedømmelse 'Bedømmelse' (Kunde c, Produkt p, INTEGER y) =
PARTITION SUM 1 ORDER DESC købt(c, p, y), p BY c, y;
VÆLG kontaktNavn(Kunde c), navn(Produkt p) HVOR bedømmelse(c, p, 1997) < 3;
Partition-operatoren fungerer på følgende måde: den summerer det udtryk, der er angivet efter SUM (her 1), inden for de angivne grupper (her Kunde og År, men det kan være et hvilket som helst udtryk), sorterer inden for grupperne efter de udtryk, der er angivet i ORDER (her købte, og hvis de er lige, så efter produktets interne kode).
Opgave 2.3
Hvor mange varer skal bestilles fra leverandørerne for at opfylde de nuværende ordrer.
Vi udvider igen domænelogikken:
CLASS Leverandør 'Leverandør';
companyName = DATA STRING[100] (Leverandør);
leverandør = DATA Leverandør (Produkt);
enhederPåLager 'Lagerstatus' = DATA NUMERIC[10,3] (Produkt);
genbestillingsniveau 'Salgstandard' = DATA NUMERIC[10,3] (Produkt);
Relationel
VÆLG s.FirmaNavn, p.ProduktNavn, sum(od.Mængde) + p.Genbestillingsniveau - p.EnhederPåLager AS ToOrder
FRA Ordrer o
JOIN [Ordre Detaljer] od ON o.OrdreID = od.OrdreID
JOIN Produkter p ON od.ProduktID = p.ProduktID
JOIN Leverandører s ON p.LeverandørID = s.LeverandørID
HVOR o.AfsendtDato ER NULL
GRUPPER EFTER s.FirmaNavn, p.ProduktNavn, p.EnhederPåLager, p.Genbestillingsniveau
HAVING p.EnhederPåLager < sum(od.Mængde) + p.Genbestillingsniveau
Funktionel
bestiltIkkeAfgivet 'Bestilt, men ikke afsendt' (Produkt p) =
GRUPPE SUM mængde(OrderDetail d) HVIS produkt(d) = p;
tilBestilling 'Til bestilling' (Produkt p) = bestiltIkkeAfgivet(p) + genbestillingsniveau(p) - enhederPåLager(p);
VÆLG firmanavn(læge(Produkt p)), navn(p), tilBestilling(p) HVOR tilBestilling(p) > 0;
Opgave med stjerne
Og her et sidste eksempel fra mig. Der er logik til sociale netværk. Folk kan være venner med hinanden og kan lide hinanden. Set fra et funktionelt databasesynspunkt vil det se således ud:
KLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
Det er nødvendigt at finde mulige kandidater til venskab. Mere formelt skal vi 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.
Set fra et funktionelt databasesynspunkt vil forespørgslen se således ud:
VÆLG Person a, Person b, Person c HVOR
kan lide(a, c) OG IKKE venner(a, c) OG
venner(a, b) OG venner(b, c);
Læseren opfordres til selv at løse denne opgave med SQL. Det antages, at der er langt færre venner end dem, der er populære. Derfor ligger de i separate tabeller. Ved en vellykket løsning er der også en opgave med to stjerner. Her er venskab ikke symmetrisk. Set fra et funktionelt databasesynspunkt vil det se sådan ud:
VÆLG Person a, Person b, Person c HVOR
kan lide(a, c) OG IKKE venner(a, c) OG
(venner(a, b) ELLER venner(b, a)) OG
(venner(b, c) ELLER venner(c, b));
UPD: løsning af opgaven med første og anden stjerne fra :
VÆLG
pl.PersonAID
,pf.PersonAID
,pff.PersonAID
FRA Persons AS p
--Likes
JOIN PersonRelationShip AS pl ON pl.PersonAID = p.PersonID
AND pl.Relation = 'Like'
--Venner
JOIN PersonRelationShip AS pf ON pf.PersonAID = p.PersonID
AND pf.Relation = 'Friend'
--Venner af Venner
JOIN PersonRelationShip AS pff ON pff.PersonAID = pf.PersonBID
AND pff.PersonBID = pl.PersonBID
AND pff.Relation = 'Friend'
--Ikke venner endnu
LEFT JOIN PersonRelationShip AS pnf ON pnf.PersonAID = p.PersonID
AND pnf.PersonBID = pff.PersonBID
AND pnf.Relation = 'Friend'
HVOR pnf.PersonAID ER NULL
;MED PersonRelationShipCollapsed SOM (
VÆLG pl.PersonAID
,pl.PersonBID
,pl.Relation
FRA #PersonRelationShip AS pl
UNION
VÆLG pl.PersonBID SOM PersonAID
,pl.PersonAID SOM PersonBID
,pl.Relation
FRA #PersonRelationShip AS pl
)
VÆLG
pl.PersonAID
,pf.PersonBID
,pff.PersonBID
FRA #Persons AS p
--Likes
JOIN PersonRelationShipCollapsed AS pl ON pl.PersonAID = p.PersonID
AND pl.Relation = 'Like'
--Venner
JOIN PersonRelationShipCollapsed AS pf ON pf.PersonAID = p.PersonID
AND pf.Relation = 'Friend'
--Venner af Venner
JOIN PersonRelationShipCollapsed AS pff ON pff.PersonAID = pf.PersonBID
AND pff.PersonBID = pl.PersonBID
AND pff.Relation = 'Friend'
--Ikke venner endnu
LEFT JOIN PersonRelationShipCollapsed AS pnf ON pnf.PersonAID = p.PersonID
AND pnf.PersonBID = pff.PersonBID
AND pnf.Relation = 'Friend'
HVOR pnf.[PersonAID] ER NULL
Konklusion
Det skal bemærkes, at den nævnte syntaks er kun én af mulighederne for at implementere det beskrevne koncept. SQL blev taget som basis, og målet var, at det skulle ligne det så meget som muligt. Selvfølgelig kan nogle personer have indvendinger mod navne på nøgleord, ords store og små bogstaver og lignende. Det vigtigste her er netop konceptet. Hvis man ønsker det, kan man også lave C++ og Python-lignende syntaks.
Den beskrevne databasekoncept har ifølge min mening følgende fordele:
- Simplicity. Det er et relativt subjektivt mål, som ikke er indlysende i enkle tilfælde. Men hvis man ser på mere komplekse tilfælde (for eksempel opgaver med stjerner), mener jeg, at det er betydeligt lettere at skrive sådanne forespørgsler.
- Indkapsling. I nogle eksempler har jeg erklæret mellemfunktioner (for eksempel, solgt, købt osv.), som efterfølgende funktioner byggede på. Dette gør det muligt at ændre logikken i bestemte funktioner uden at ændre logikken i de funktioner, der er afhængige af dem. For eksempel kan man gøre det sådan, at salget solgt man betragtes som helt andre objekter, mens resten af logikken forbliver uændret. Ja, i RDBMS kan dette realiseres ved hjælp af CREATE VIEW. Men hvis al logik skrives på denne måde, vil den ikke være meget læsbar.
- Manglende semantisk brud. En sådan database håndterer funktioner og klasser (i stedet for tabeller og felter). Ligesom i klassisk programmering (hvis man betragter en metode som en funktion med den første parameter som klassen, den tilhører). Derfor bør det være væsentligt lettere at "forene" det med universelle programmeringssprog. Derudover muliggør denne koncept at implementere langt mere komplekse funktioner. For eksempel kan man indføre sådanne operatorer i databasen:
KONSTRIKTION solgt(Ansat e, 1, 2019) > 100 HVIS navn(e) = 'Petya' BESKED 'Noget Petya sælger for meget af én vare i 2019';
- Arv og polymorfi. I en funktionel database kan man indføre multiarv gennem konstruktionen CLASS ClassP: Class1, Class2 og implementere multiform polymorfi. Hvordan dette gøres, vil jeg muligvis skrive om i kommende artikler.
På trods af at dette kun er et koncept, har vi allerede en vis implementering i Java, som oversætter al funktionel logik til relationel logik. Desuden er logikken for visninger smukt tilknyttet, og meget andet, hvilket giver en hel . I bund og grund bruger vi RDBMS (lige nu kun PostgreSQL) som en "virtuelt maskine". Ved denne oversættelse opstår der nogle gange problemer, da RDBMS's forespørgselsoptimering ikke kender bestemte statistikker, som FDBMS er bekendt med. I teorien kan man implementere et database management system, der vil bruge en struktur, tilpasset specifikt til den funktionelle logik, som opbevaring.
Kilde: habr.com
