Funktionelt DBMS

Databaseverdenen er længe blevet overtaget af relationelle DBMS'er, der bruger SQL-sproget. Så meget, at nye varianter kaldes NoSQL. De formåede at vinde en bestemt plads til sig selv på dette marked, men relationelle DBMS vil ikke dø, og fortsætter med at blive brugt aktivt til deres egne formål.

I denne artikel vil jeg beskrive begrebet en funktionel database. For en bedre forståelse vil jeg gøre dette ved at sammenligne med den klassiske relationsmodel. Som eksempler vil vi bruge opgaver fra forskellige SQL-tests fundet på internettet.

Indledning

Relationelle databaser opererer på tabeller og felter. I en funktionel database vil der i stedet blive brugt henholdsvis klasser og funktioner. Et felt i en tabel med N nøgler vil blive repræsenteret som en funktion af N parametre. I stedet for links mellem tabeller vil der blive brugt funktioner, der returnerer objekter fra den klasse, som linket går til. Funktionssammensætning vil blive brugt i stedet for JOIN.

Inden jeg går direkte videre til opgaverne, vil jeg beskrive opgaven med domænelogik. Til DDL vil jeg bruge PostgreSQL-syntaks. For funktionel sin egen syntaks.

Tabeller og felter

Et simpelt Sku-objekt med navn og prisfelter:

relationelle

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

funktionelle

CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);

Vi annoncerer to funktioner, som tager én Sku-parameter som input og returnerer en primitiv type.

Det antages, at i et funktionelt DBMS vil hvert objekt have en eller anden intern kode, der automatisk genereres og kan tilgås, hvis det er nødvendigt.

Lad os fastsætte prisen for produktet / butikken / leverandøren. Det kan ændre sig over tid, så lad os tilføje et tidsfelt til tabellen. Jeg vil springe erklæringen af ​​tabeller for mapper over i en relationsdatabase for at forkorte koden:

relationelle

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

funktionelle

CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);

Indekser

For det sidste eksempel, lad os bygge et indeks på alle nøgler og dato, så vi hurtigt kan finde prisen for et bestemt tidspunkt.

relationelle

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

funktionelle

INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);

opgaver

Lad os starte med relativt simple problemer taget fra de tilsvarende Artikel paa Habr.

Lad os først erklære domænelogikken (for en relationel database gøres dette direkte i ovenstående artikel).

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

1.1 udfordring

Vis en liste over medarbejdere, der modtager højere løn end den nærmeste leders.

relationelle

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

funktionelle

SELECT name(Employee a) WHERE salary(a) > salary(chief(a));

1.2 udfordring

Vis en liste over medarbejdere, der tjener den højeste løn i deres afdeling

relationelle

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

funktionelle

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 implementeringer er ækvivalente. Til det første tilfælde i relationsdatabasen kan du bruge CREATE VIEW, som på samme måde først vil beregne den maksimale løn for en bestemt afdeling i den. I fremtiden vil jeg for klarhedens skyld bruge det første tilfælde, da det bedre afspejler løsningen.

1.3 udfordring

Vis en liste over afdelings-id'er, hvor antallet af medarbejdere ikke overstiger 3 personer.

relationelle

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

funktionelle

countEmployees 'Количество сотрудников' (Department d) = 
    GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;

1.4 udfordring

Vis en liste over medarbejdere, der ikke har en tilknyttet leder, der arbejder i samme afdeling.

relationelle

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

funktionelle

SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));

1.5 udfordring

Find listen over afdelings-id'er med den maksimale samlede medarbejderløn.

relationelle

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 )

funktionelle

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

Lad os gå videre til mere komplekse opgaver fra en anden Artikel. Den indeholder en detaljeret analyse af, hvordan denne opgave implementeres i MS SQL.

2.1 udfordring

Hvilke sælgere solgte mere end 1997 styk af vare #30 i 1?

Domænelogik (som før springer vi erklæringen på RDBMS over):

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

relationelle

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

funktionelle

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;

2.2 udfordring

For hver kunde (fornavn, efternavn) skal du finde de to varer (navn), som kunden brugte flest penge på i 1997.

Udvidelse af domænelogikken fra det foregående eksempel:

CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);

customer = DATA Customer (Order);

unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);

relationelle

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

funktionelle

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 efter følgende princip: den summerer udtrykket angivet efter SUM (her 1) inden for de angivne grupper (her Kunde og År, men kan være et hvilket som helst udtryk), sorterer inden for grupperne i henhold til de udtryk, der er angivet i ORDRE (her købt, og hvis lig, så efter den interne produktkode).

2.3 udfordring

Hvor mange varer skal bestilles fra leverandører for at opfylde aktuelle ordrer.

Lad os udvide domænelogikken igen:

CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);

supplier = DATA Supplier (Product);

unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);

relationelle

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

funktionelle

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;

Opgave med en stjerne

Og det sidste eksempel er fra mig personligt. Der er logikken i et socialt netværk. Folk kan være venner med hinanden og holde af hinanden. Fra et funktionelt databaseperspektiv vil dette se sådan ud:

CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);

Det er nødvendigt at finde mulige kandidater til venskab. Mere formelt skal du 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.
Fra et funktionelt databasesynspunkt vil forespørgslen se sådan ud:

SELECT Person a, Person b, Person c WHERE 
    likes(a, c) AND NOT friends(a, c) AND 
    friends(a, b) AND friends(b, c);

Læseren opfordres til selvstændigt at løse dette problem i SQL. Det antages, at der er langt færre venner end dem, der kan lide. Derfor er de i separate tabeller. I tilfælde af en vellykket løsning er der også et problem med to stjerner. Hendes venskab er ikke symmetrisk. På en funktionel database vil det se sådan ud:

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 af problemet med den første og anden stjerne fra dss_kalika:

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 

Konklusion

Det skal bemærkes, at ovenstående syntaks for sproget kun er en af ​​mulighederne for at implementere ovenstående koncept. Det var SQL, der blev lagt til grund, og målet var at gøre det så lig som muligt. Selvfølgelig kan nogen ikke lide navnene på søgeord, store bogstaver i ord og så videre. Det vigtigste her er selve konceptet. Hvis det ønskes, kan du lave både C ++ og Python lignende syntaks.

Det beskrevne databasekoncept har efter min mening følgende fordele:

  • Ease. Dette er en relativt subjektiv indikator, der ikke er indlysende i simple tilfælde. Men hvis du ser på mere komplekse sager (for eksempel opgaver med stjerner), så er det efter min mening meget lettere at skrive sådanne forespørgsler.
  • Инкапсуляция. I nogle eksempler erklærede jeg mellemliggende funktioner (f.eks. solgt, købte osv.), hvorfra efterfølgende funktioner blev bygget. Dette giver dig mulighed for at ændre logikken for visse funktioner, hvis det er nødvendigt, uden at ændre logikken for dem, der afhænger af dem. For eksempel kan du lave salg solgt blev beregnet ud fra helt andre objekter, mens resten af ​​logikken ikke ændres. Ja, i RDBMS kan dette gøres ved hjælp af CREATE VIEW. Men hvis du skriver al logikken på denne måde, så vil den ikke se særlig læsbar ud.
  • Ingen semantisk kløft. En sådan database opererer med funktioner og klasser (i stedet for tabeller og felter). På samme måde som i klassisk programmering (forudsat at en metode er en funktion med den første parameter i form af en klasse, den tilhører). Derfor burde det være meget nemmere at "blive venner" med universelle programmeringssprog. Derudover giver dette koncept dig mulighed for at implementere meget mere komplekse funktioner. For eksempel kan du indlejre udsagn som dette i databasen:

    CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE  'Что-то Петя продает слишком много одного товара в 2019 году';

  • Arv og polymorfi. I en funktionel database kan du introducere multipel nedarvning gennem CLASS ClassP: Class1, Class2-konstruktionerne og implementere multipel polymorfi. Hvordan præcist vil jeg måske skrive i de følgende artikler.

Selvom dette kun er et koncept, har vi allerede en vis implementering i Java, der oversætter al funktionel logik til relationel logik. Plus, logikken i repræsentationer og en masse andre ting er smukt skruet sammen, takket være hvilket vi får en helhed platform. Grundlæggende bruger vi en RDBMS (kun PostgreSQL indtil videre) som en "virtuel maskine". Denne oversættelse forårsager nogle gange problemer, fordi RDBMS-forespørgselsoptimeringsværktøjet ikke kender visse statistikker, som FDBMS gør. I teorien er det muligt at implementere et databasestyringssystem, der vil bruge en bestemt struktur som lager, tilpasset specifikt til funktionel logik.

Kilde: www.habr.com

Tilføj en kommentar