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