Funktionel DBMS

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 artikler 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. artikler. 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 dss_kalika:

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 platform. 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

Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere 🔥 Køb pålidelig webhosting med DDoS-beskyttelse, VPS VDS-servere | ProHoster