Funksjonell DBMS

Verden av databaser har lenge vært dominert av relasjonelle DBMS-er, som bruker SQL-språket. Så mye at nye varianter kalles NoSQL. De klarte å skaffe seg en viss plass i dette markedet, men relasjonelle DBMS-er kommer ikke til å dø, og fortsetter å bli aktivt brukt til deres formål.

I denne artikkelen ønsker jeg å beskrive konseptet med en funksjonell database. For en bedre forståelse vil jeg gjøre dette ved å sammenligne det med den klassiske relasjonsmodellen. Problemer fra ulike SQL-tester funnet på Internett vil bli brukt som eksempler.

Innledning

Relasjonsdatabaser opererer på tabeller og felt. I en funksjonell database vil henholdsvis klasser og funksjoner brukes i stedet. Et felt i en tabell med N nøkler vil bli representert som en funksjon av N parametere. I stedet for relasjoner mellom tabeller vil det brukes funksjoner som returnerer objekter av klassen som koblingen er gjort til. Funksjonssammensetning vil bli brukt i stedet for JOIN.

Før jeg går direkte til oppgavene, vil jeg beskrive oppgaven med domenelogikk. For DDL vil jeg bruke PostgreSQL-syntaks. For funksjonell har den sin egen syntaks.

Tabeller og felt

Et enkelt Sku-objekt med navn og prisfelt:

Relasjonell

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

Funksjonell

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

Vi annonserer to funksjoner, som tar én parameter Sku som input og returnerer en primitiv type.

Det antas at i et funksjonelt DBMS vil hvert objekt ha en eller annen intern kode som genereres automatisk og kan nås om nødvendig.

La oss sette prisen for produktet/butikken/leverandøren. Det kan endre seg over tid, så la oss legge til et tidsfelt i tabellen. Jeg vil hoppe over å deklarere tabeller for kataloger i en relasjonsdatabase for å forkorte koden:

Relasjonell

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

Funksjonell

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

Indekser

For det siste eksemplet vil vi bygge en indeks på alle nøkler og datoen slik at vi raskt kan finne prisen for et bestemt tidspunkt.

Relasjonell

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

Funksjonell

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

oppgaver

La oss starte med relativt enkle problemer hentet fra de tilsvarende artikler på Habr.

Først, la oss erklære domenelogikken (for relasjonsdatabasen gjøres dette direkte i artikkelen ovenfor).

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

Oppgave 1.1

Vis en liste over ansatte som mottar høyere lønn enn den nærmeste lederen.

Relasjonell

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

Funksjonell

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

Oppgave 1.2

List opp de ansatte som får maksimal lønn i sin avdeling

Relasjonell

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

Funksjonell

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 implementeringene er likeverdige. For det første tilfellet kan du i en relasjonsdatabase bruke CREATE VIEW, som på samme måte først vil beregne maksimal lønn for en bestemt avdeling i den. I det følgende, for klarhets skyld, vil jeg bruke det første tilfellet, siden det bedre gjenspeiler løsningen.

Oppgave 1.3

Vis en liste over avdelings-IDer, hvor antall ansatte ikke overstiger 3 personer.

Relasjonell

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

Funksjonell

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

Oppgave 1.4

Vis en liste over ansatte som ikke har en utpekt leder som jobber i samme avdeling.

Relasjonell

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

Funksjonell

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

Oppgave 1.5

Finn en liste over avdelings-IDer med maksimal totallønn for ansatte.

Relasjonell

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 )

Funksjonell

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

La oss gå videre til mer komplekse oppgaver fra en annen artikler. Den inneholder en detaljert analyse av hvordan du implementerer denne oppgaven i MS SQL.

Oppgave 2.1

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

Domenelogikk (som før på RDBMS hopper vi over erklæringen):

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

Relasjonell

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

Funksjonell

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;

Oppgave 2.2

For hver kjøper (navn, etternavn), finn de to varene (navn) som kjøperen brukte mest penger på i 1997.

Vi utvider domenelogikken fra forrige eksempel:

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

customer = DATA Customer (Order);

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

Relasjonell

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

Funksjonell

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 etter følgende prinsipp: den summerer uttrykket spesifisert etter SUM (her 1), innenfor de spesifiserte gruppene (her Customer og Year, men kan være et hvilket som helst uttrykk), sorterer innenfor gruppene etter uttrykkene spesifisert i ORDER ( her kjøpt, og hvis lik, så i henhold til den interne produktkoden).

Oppgave 2.3

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

La oss utvide domenelogikken igjen:

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

supplier = DATA Supplier (Product);

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

Relasjonell

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

Funksjonell

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;

Problem med en stjerne

Og det siste eksemplet er fra meg personlig. Det er logikken i et sosialt nettverk. Folk kan være venner med hverandre og like hverandre. Fra et funksjonelt databaseperspektiv vil det se slik ut:

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

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

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

Leseren oppfordres til å løse dette problemet i SQL på egen hånd. Det antas at det er mye færre venner enn folk du liker. Derfor er de i separate tabeller. Hvis vellykket, er det også en oppgave med to stjerner. I den er vennskap ikke symmetrisk. På en funksjonell database vil det se slik ut:

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 på problemet med den første og andre stjernen 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 

Konklusjon

Det skal bemerkes at den gitte språksyntaksen bare er ett av alternativene for å implementere det gitte konseptet. SQL ble lagt til grunn, og målet var at den skulle være mest mulig lik den. Selvfølgelig kan det hende at noen ikke liker navnene på nøkkelord, ordregistre osv. Hovedsaken her er selve konseptet. Hvis ønskelig, kan du lage både C++ og Python lignende syntaks.

Det beskrevne databasekonseptet har etter min mening følgende fordeler:

  • lette. Dette er en relativt subjektiv indikator som ikke er åpenbar i enkle tilfeller. Men hvis du ser på mer komplekse saker (for eksempel problemer med stjerner), er det etter min mening mye enklere å skrive slike spørsmål.
  • innkapsling. I noen eksempler erklærte jeg mellomfunksjoner (f.eks. solgt, kjøpt etc.), som påfølgende funksjoner ble bygget fra. Dette lar deg endre logikken til visse funksjoner, om nødvendig, uten å endre logikken til de som er avhengige av dem. Du kan for eksempel gjøre salg solgt ble beregnet fra helt andre objekter, mens resten av logikken ikke vil endre seg. Ja, dette kan implementeres i en RDBMS ved å bruke CREATE VIEW. Men hvis all logikken er skrevet på denne måten, vil den ikke se særlig lesbar ut.
  • Ingen semantisk gap. En slik database opererer på funksjoner og klasser (i stedet for tabeller og felt). Akkurat som i klassisk programmering (hvis vi antar at en metode er en funksjon med den første parameteren i form av klassen den tilhører). Følgelig burde det være mye lettere å "bli venner" med universelle programmeringsspråk. I tillegg tillater dette konseptet at mye mer kompleks funksjonalitet kan implementeres. Du kan for eksempel bygge inn operatører som:

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

  • Arv og polymorfisme. I en funksjonell database kan du introdusere multippel arv gjennom CLASS ClassP: Class1, Class2-konstruksjonene og implementere multippel polymorfisme. Jeg kommer nok til å skrive nøyaktig hvordan i fremtidige artikler.

Selv om dette bare er et konsept, har vi allerede en del implementering i Java som oversetter all funksjonell logikk til relasjonslogikk. I tillegg er logikken til representasjoner og mange andre ting vakkert knyttet til den, takket være at vi får en helhet plattform. I hovedsak bruker vi RDBMS (bare PostgreSQL foreløpig) som en "virtuell maskin". Det oppstår noen ganger problemer med denne oversettelsen fordi RDBMS-søkeoptimereren ikke kjenner til visse statistikker som FDBMS kjenner til. I teorien er det mulig å implementere et databasestyringssystem som vil bruke en viss struktur som lagring, tilpasset spesifikt for funksjonell logikk.

Kilde: www.habr.com

Legg til en kommentar