Funksjonell DBMS

Database systems have long been dominated by relational DBMS that use SQL. So much so that the emerging varieties are now termed NoSQL. They have managed to carve out a niche in this market, but relational DBMS are not going away and continue to be actively used for their purposes.

In this article, I want to describe the concept of a functional database. For better understanding, I will do this by comparing it to the classical relational model. I will use examples from various SQL test problems found on the internet.

Innledning

Relational databases operate with tables and fields. In a functional database, instead of these, classes and functions will be used respectively. A field in a table with N keys will be represented as a function of N parameters. Instead of relationships between tables, functions will be used that return instances of the class to which they are linked. Instead of JOIN, function composition will be used.

Before moving directly to the tasks, I will describe the domain logic assignment. For DDL, I will use PostgreSQL syntax. For the functional aspect, I will use my syntax.

Tables and fields

A simple Sku object with fields name and price:

Relational

CREATE TABLE Sku
(
    id bigint NOT NULL,
    name character varying(100),
    price numeric(10,5),
    CONSTRAINT id_pkey PRIMARY KEY (id)
)

Functional

KLASSE Sku;
navn = DATA STRENG[100] (Sku);
pris = DATA NUMERISK[10,5] (Sku);

We declare two functions, which take one parameter Sku as input and return a primitive type.

It is assumed that in the functional DBMS, each object will have some internal code that is automatically generated and can be accessed when necessary.

Let’s set the price for the product/store/supplier. It may change over time, so we will add a time field to the table. I will skip the declaration of tables for directories in the relational database to reduce the code:

Relational

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)
)

Functional

KLASSE Sku;
KLASSE Butikk;
KLASSE Leverandør;
datoTid = DATA DATETIME (Sku, Butikk, Leverandør);
pris = DATA NUMERISK[10,5] (Sku, Butikk, Leverandør);

Indexes

For the last example, we will build an index on all keys and the date so that we can quickly find the price at a specific time.

Relational

CREATE INDEX prices_date
    ON prices
    (skuId, storeId, supplierId, dateTime)

Functional

INDEX Sku sk, Butikk st, Leverandør sp, dateTime(sk, st, sp);

Oppgaver

We will start with relatively simple tasks taken from the corresponding artikkel on Habr.

Først vil vi erklære domenelogikken (for en relasjonsdatabase gjøres dette direkte i den medfølgende artikkelen).

KLASSE Avdeling;
navn = DATA STRENG[100] (Avdeling);

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 listen over ansatte som har en lønn høyere enn sin direkte leder.

Relational

select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary

Functional

VELJ navn(Ansatt a) DER lønn(a) > lønn(sjef(a));

Oppgave 1.2

Vis listen over ansatte som har den høyeste lønnen i sin avdeling

Relational

select a.*
from employee a
where a.salary = ( select max(salary) from employee b
                    where b.department_id = a.department_id )

Functional

maxSalary 'Maksimal lønn' (Department s) = 
    GROUP MAX lønn(Employee e) IF avdeling(e) = s;
SELECT navn(Employee a) WHERE lønn(a) = maxSalary(avdeling(a));

// или если "заинлайнить"
SELECT name(Employee a) WHERE 
    salary(a) = maxSalary(GROUP MAX salary(Employee e) IF department(e) = department(a));

Begge implementeringene er ekvivalente. For den første saken kan vi i en relasjonsdatabase bruke CREATE VIEW, som på samme måte først vil kalkulere den maksimale lønnen for en spesifikk avdeling. For tydelighet vil jeg bruke den første saken videre, da den bedre reflekterer løsningen.

Oppgave 1.3

Vis listen over ID-er for avdelinger der antall ansatte ikke overstiger 3 personer.

Relational

select department_id
from employee
group by department_id
having count(*) <= 3

Functional

countEmployees 'Antall ansatte' (Department d) = 
    GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;

Oppgave 1.4

Vis listen over ansatte som ikke har noen tildelt leder og som jobber i samme avdeling.

Relational

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

Functional

VELG navn(Ansatt a) HVOR IKKE (avdeling(sjef(a)) = avdeling(a));

Oppgave 1.5

Finn listen over ID-er for avdelinger med den maksimale totale lønnen for ansatte.

Relational

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 )

Functional

salarySum 'Maksimal lønn' (Department d) = 
    GROUP SUM salary(Employee e) IF department(e) = d;
maxSalarySum 'Maksimal lønn for avdelinger' () = 
    GROUP MAX salarySum(Department d);
SELECT Department d WHERE salarySum(d) = maxSalarySum();

La oss gå videre til mer komplekse oppgaver fra et annet artikkel. Denne inneholder en detaljert gjennomgang av hvordan man implementerer denne oppgaven i MS SQL.

Oppgave 2.1

Hvilke selgere solgte mer enn 30 enheter av vare nr. 1 i 1997?

Domenelogikken (som før på RDBMS, utelater vi erklæringen):

CLASS Employee 'Selger';
lastName 'Etternavn' = 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 'Order Line';

order = DATA Order (Detail);
product = DATA Product (Detail);
quantity = DATA NUMERIC[10,5] (Detail);

Relational

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

Functional

solgt (Ansatt e, INTEGER produktId, INTEGER år) = 
    GRUPPE SUM mengde(OrderDetail d) HVIS 
        ansettelse(order(d)) = e OG 
        id(produkt(d)) = produktId OG 
        ekstraherÅr(dato(order(d))) = år;
VELG etternavn(Ansatt e) HVOR solgt(e, 1, 1997) > 30;

Oppgave 2.2

For hver kunde (navn, etternavn), finn to produkter (navn) som kunden brukte mest penger på i 1997.

Vi utvider domenelogikken fra forrige eksempel:

CLASS Customer 'Kunde';
contactName 'Navn' = DATA STRING[100] (Kunde);

kunde = DATA Kunde (Ordre);

enhetspris = DATA NUMMER[14,2] (Detalj);
rabatt = DATA NUMMER[6,2] (Detalj);

Relational

VELG KontaktNavn, ProduktNavn FRA (
VELG c.KontaktNavn, p.ProduktNavn
, RAD_NUMMER() OVER (
    PARTISJONERT ETTER c.KontaktNavn
    BESTILLING ETTER SUM(od.Kvantitet * od.Enhetspris * (1 - od.Rabatt)) DESC
) AS VurderingEtterBeløp
FRA Kunder c
BLI MED Ordre o PÅ o.KundeID = c.KundeID
BLI MED [Ordredetaljer] od PÅ od.OrdreID = o.OrdreID
BLI MED Produkter p PÅ p.ProduktID = od.ProduktID
HVOR ÅR(o.OrdreDato) = 1997
GRUPPER ETTER c.KontaktNavn, p.ProduktNavn
) t
HVOR VurderingEtterBeløp < 3

Functional

sum (Detail d) = quantity(d) * unitPrice(d) * (1 - discount(d));
kjøpt 'Kjøpt' (Kunde c, Produkt p, HELTALL y) = 
    GRUPPER SUM sum(Detalj d) HVIS 
        kunde(bestilling(d)) = c OG 
        produkt(d) = p OG 
        extractYear(date(order(d))) = y;
vurdering 'Vurdering' (Kunde c, Produkt p, HELTALL y) = 
    PARTITION SUM 1 ORDER DESC bought(c, p, y), p BY c, y;
VELG kontaktNavn(Kunde c), navn(Produkt p) HVOR vurdering(c, p, 1997) < 3;

PARTISJON-operatoren fungerer som følger: den summerer uttrykket angitt etter SUM (her 1), innenfor de angitte gruppene (her Kunde og År, men kan være et hvilket som helst uttrykk), og sorterer innenfor grupper i henhold til uttrykkene angitt i BESTILLING (her kjøpt, og hvis lik, etter interne produktkoder).

Oppgave 2.3

Hvor mange varer må bestilles fra leverandørene for å oppfylle gjeldende bestillinger.

Vi utvider domenelogikken igjen:

CLASS Supplier 'Leverandør';
companyName = DATA STRING[100] (Leverandør);

leverandør = DATA Leverandør (Produkt);

enheterPåLager 'Lagerbeholdning' = DATA NUMMER[10,3] (Produkt);
bestillingsnivå 'Salgsnorm' = DATA NUMMER[10,3] (Produkt);

Relational

velg s.FirmaNavn, p.ProduktNavn, sum(od.Kvantitet) + p.Bestillingsnivå - p.EnheterPåLager som ÅBestille
fra Ordre o
bli med [Ordredetaljer] od på o.OrdreID = od.OrdreID
bli med Produkter p på od.ProduktID = p.ProduktID
bli med Leverandører s på p.LeverandørID = s.LeverandørID
hvor o.SendtDato er null
gruppér etter s.FirmaNavn, p.ProduktNavn, p.EnheterPåLager, p.Bestillingsnivå
HAVING p.EnheterPåLager < sum(od.Kvantitet) + p.Bestillingsnivå

Functional

bestiltIkkeSendt 'Bestilt, men ikke sendt' (Produkt p) = 
    GRUPPE SUM mengde(OrderDetail d) HVIS produkt(d) = p;
tilBestilling 'Til bestilling' (Produkt p) = bestiltIkkeSendt(p) + bestillingsnivå(p) - enheterPåLager(p);
VELG firmanavn(leverandør(Produkt p)), navn(p), tilBestilling(p) HVOR tilBestilling(p) > 0;

Oppgave med stjerne

Og det siste eksemplet personlig fra meg. Det er logikk fra et sosialt nettverk. Folk kan være venner med hverandre og like hverandre. Fra et funksjonelt databaseperspektiv vil dette se ut som følger:

KLASSE Person;
liker = DATA BOOLEAN (Person, Person);
venner = DATA BOOLEAN (Person, Person);

Det er nødvendig å finne mulige kandidater for vennskap. Mer formalisert må man finne alle personer A, B, C slik at A er venn med B, 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:

VELG Person a, Person b, Person c HVOR 
    liker(a, c) OG IKKE venner(a, c) OG 
    venner(a, b) OG venner(b, c);

Leseren blir bedt om å løse denne oppgaven på SQL. Det antas at venner er mye færre enn de som blir likt. Derfor ligger de i separate tabeller. I tilfelle av vellykket løsning er det også en oppgave med to stjerner. I den er vennskap ikke symmetrisk. Fra det funksjonelle databasens perspektiv vil dette se slik ut:

VELG Person a, Person b, Person c HVOR 
    liker(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 på oppgaven med første og andre stjerne fra dss_kalika:

SELECT 
   pl.PersonAID
  ,pf.PersonAID
  ,pff.PersonAID
FROM Persons                 AS p
--Likes                      
JOIN PersonRelationShip      AS pl ON pl.PersonAID = p.PersonID
                                  AND pl.Relation  = 'Like'
--Friends                     
JOIN PersonRelationShip      AS pf ON pf.PersonAID = p.PersonID 
                                  AND pf.Relation = 'Friend'
--Friends of Friends              
JOIN PersonRelationShip      AS pff ON pff.PersonAID = pf.PersonBID
                                   AND pff.PersonBID = pl.PersonBID
                                   AND pff.Relation = 'Friend'
--Not Yet Friends         
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
--Likes                      
JOIN PersonRelationShipCollapsed  AS pl ON pl.PersonAID = p.PersonID
                                 AND pl.Relation  = 'Like'                                  
--Friends                          
JOIN PersonRelationShipCollapsed  AS pf ON pf.PersonAID = p.PersonID 
                                 AND pf.Relation = 'Friend'
--Friends of Friends                   
JOIN PersonRelationShipCollapsed  AS pff ON pff.PersonAID = pf.PersonBID
                                 AND pff.PersonBID = pl.PersonBID
                                 AND pff.Relation = 'Friend'
--Not Yet Friends                   
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 er viktig å merke seg at den presenterte syntaksen for språket bare er én av mulige implementeringer av den beskrevne konseptet. Den er basert på SQL, og målet har vært at den skal være så lik som mulig. Selvfølgelig kan noen være misfornøyde med navnene på nøkkelordene, ordrester og så videre. Hovedfokuset her er den faktiske konsepten. Hvis ønskelig, kan man lage en syntaks som er lik C++ og Python.

Den beskrevne databasen, etter min mening, har følgende fordeler:

  • Enkelhet. Dette er en relativt subjektiv indikator, som ikke er åpenbar i enkle tilfeller. Men hvis man ser på mer komplekse tilfeller (for eksempel oppgaver med stjerner), så mener jeg det er betydelig enklere å skrive slike forespørsel.
  • Kapsling. I noen eksempler har jeg deklarert mellomliggende funksjoner (for eksempel, sold, bought osv.), fra hvilke de påfølgende funksjonene ble bygget. Dette lar deg, om nødvendig, endre logikken til bestemte funksjoner uten å endre logikken til de avhengige funksjonene. For eksempel kan man gjøre det slik at salg sold de ble ansett som helt andre objekter, samtidig som den resterende logikken forblir uendret. Ja, i RDBMS kan dette gjennomføres ved hjelp av CREATE VIEW. Men hvis man skriver hele logikken på denne måten, vil den ikke være særlig lesbar.
  • Manglende semantisk gap. En slik database opererer med funksjoner og klasser (i stedet for tabeller og felt). Akkurat som i klassisk programmering (hvis man antar at en metode er en funksjon med den første parameteren som klassen den tilhører). Følgelig bør det være mye enklere å «venne» seg til universelle programmeringsspråk. I tillegg gjør dette konseptet det mulig å implementere mye mer komplekse funksjoner. For eksempel kan man integrere operatører av typen inn i databasen:

    KONSTRIKSJON sold(Employee e, 1, 2019) > 100 HVIS name(e) = 'Petya' MELDING 'Noe Petya selger for mye av ett produkt i 2019.';

  • Arv og polymorfisme. I en funksjonell database kan man implementere flergangs arv gjennom konstruksjonen CLASS ClassP: Class1, Class2 og realisere flergangs polymorfisme. Hvordan det kan gjøres, skriver jeg muligens i kommende artikler.

Til tross for at dette bare er et konsept, har vi allerede en viss implementering på Java som oversetter all funksjonell logikk til relasjonell logikk. I tillegg er det pent integrert med logikken for visninger og mye annet, som gjør at det blir en hel plattformer. I praksis bruker vi RDBMS (foreløpig bare PostgreSQL) som en «virtuell maskin». I denne oversettelsen oppstår det noen ganger problemer, ettersom spørringsoptimalisatoren i RDBMS ikke kjenner den spesifikke statistikken som FDBMS vet om. I teorien kan man implementere et databasesystem som vil bruke en struktur til lagring som er spesialtilpasset for den funksjonelle logikken.

Kilde: habr.com

Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere 🔥 Kjøp pålitelig hosting for nettsteder med DDoS-beskyttelse, VPS VDS-servere | ProHoster