Toimiva DBMS

Tietokantamaailma on pitkään vallannut relaatiotietokantajärjestelmät, jotka käyttävät SQL-kieltä. Niin paljon, että uusia lajikkeita kutsutaan NoSQL:ksi. He onnistuivat voittamaan itselleen tietyn paikan näillä markkinoilla, mutta relaatiotietokantajärjestelmät eivät kuole, vaan niitä käytetään edelleen aktiivisesti omiin tarkoituksiinsa.

Tässä artikkelissa haluan kuvata toiminnallisen tietokannan käsitettä. Paremman ymmärtämisen vuoksi teen tämän vertaamalla klassiseen relaatiomalliin. Esimerkkeinä käytetään tehtäviä erilaisista Internetistä löytyvistä SQL-testeistä.

Esittely

Relaatiotietokannat toimivat taulukoissa ja kentissä. Toiminnallisessa tietokannassa käytetään sen sijaan luokkia ja funktioita. Taulukon kenttä, jossa on N avainta, esitetään N parametrin funktiona. Taulukoiden välisten linkkien sijaan käytetään funktioita, jotka palauttavat sen luokan objektit, johon linkki menee. Funktiokokoonpanoa käytetään JOIN:n sijaan.

Ennen kuin siirryn suoraan tehtäviin, kuvailen domainlogiikan tehtävää. DDL:ssä käytän PostgreSQL-syntaksia. Toiminnallista varten sillä on oma syntaksi.

Pöydät ja kentät

Yksinkertainen Sku-objekti nimi- ja hintakentillä:

suhteellinen

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

Toimiva

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

Ilmoitamme kaksi tehtävät, jotka ottavat syötteenä yhden Sku-parametrin ja palauttavat primitiivisen tyypin.

Oletetaan, että toiminnallisessa DBMS:ssä jokaisella objektilla on sisäinen koodi, joka luodaan automaattisesti ja jota voidaan tarvittaessa käyttää.

Asetetaan hinta tuotteelle/myymälälle/toimittajalle. Se voi muuttua ajan myötä, joten lisätään aikakenttä taulukkoon. Ohitan relaatiotietokannan hakemistojen taulukoiden ilmoittamisen koodin lyhentämiseksi:

suhteellinen

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

Toimiva

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

Indeksit

Viimeistä esimerkkiä varten rakennetaan indeksi kaikille avaimille ja päivämäärälle, jotta voimme nopeasti löytää hinnan tietylle ajalle.

suhteellinen

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

Toimiva

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

tehtävät

Aloitetaan suhteellisen yksinkertaisista ongelmista, jotka on otettu vastaavasta Artikkeli osoitteessa Habr.

Ilmoitetaan ensin toimialueen logiikka (relaatiotietokannan osalta tämä tehdään suoraan yllä olevassa artikkelissa).

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 Tehtävä

Näytä luettelo työntekijöistä, jotka saavat suuremman palkan kuin välittömän esimiehen.

suhteellinen

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

Toimiva

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

1.2 Tehtävä

Näytä luettelo työntekijöistä, jotka ansaitsevat osastonsa korkeimman palkan

suhteellinen

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

Toimiva

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

Molemmat toteutukset ovat samanarvoisia. Ensimmäisessä tapauksessa relaatiotietokannassa voit käyttää CREATE VIEW -toimintoa, joka samalla tavalla laskee ensin tietyn osaston enimmäispalkan. Jatkossa käytän selvyyden vuoksi ensimmäistä tapausta, koska se kuvastaa paremmin ratkaisua.

1.3 Tehtävä

Näytä luettelo osastotunnuksista, joissa työntekijöiden määrä ei ylitä 3 henkilöä.

suhteellinen

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

Toimiva

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

1.4 Tehtävä

Näytä luettelo työntekijöistä, joilla ei ole määrättyä johtajaa työskentelemässä samalla osastolla.

suhteellinen

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

Toimiva

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

1.5 Tehtävä

Etsi luettelo osastotunnuksista, joissa on työntekijän enimmäispalkka.

suhteellinen

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 )

Toimiva

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

Siirrytään monimutkaisempiin tehtäviin toisesta Artikkeli. Se sisältää yksityiskohtaisen analyysin tämän tehtävän toteuttamisesta MS SQL:ssä.

2.1 Tehtävä

Mitkä myyjät myivät yli 1997 kappaletta tuotetta nro 30 vuonna 1?

Verkkotunnuksen logiikka (kuten ennenkin, ohitamme RDBMS:n ilmoituksen):

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

suhteellinen

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

Toimiva

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 Tehtävä

Etsi jokaisesta asiakkaasta (etunimi, sukunimi) kaksi tuotetta (nimi), joihin asiakas käytti eniten rahaa vuonna 1997.

Verkkotunnuksen logiikan laajentaminen edellisestä esimerkistä:

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

customer = DATA Customer (Order);

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

suhteellinen

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

Toimiva

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-operaattori toimii seuraavan periaatteen mukaisesti: se summaa SUM (tässä 1) jälkeen määritetyn lausekkeen määritettyjen ryhmien sisällä (tässä Asiakas ja Vuosi, mutta voi olla mikä tahansa lauseke), lajittelee ryhmien sisällä TARJOUS-kohdassa määritettyjen lausekkeiden mukaan ( täältä ostettu, ja jos ne ovat yhtä suuret, niin sisäisen tuotekoodin mukaan).

2.3 Tehtävä

Kuinka monta tavaraa on tilattava toimittajilta nykyisten tilausten täyttämiseksi.

Laajennetaan verkkotunnuksen logiikkaa uudelleen:

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

supplier = DATA Supplier (Product);

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

suhteellinen

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

Toimiva

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;

Tehtävä tähdellä

Ja viimeinen esimerkki on minulta henkilökohtaisesti. Siinä on sosiaalisen verkoston logiikka. Ihmiset voivat olla ystäviä keskenään ja pitää toisistaan. Toiminnallisen tietokannan näkökulmasta tämä näyttäisi tältä:

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

Ystävyyteen on löydettävä mahdolliset ehdokkaat. Muodollisemmin sinun on löydettävä kaikki ihmiset A, B, C siten, että A on ystävä B:n kanssa ja B on ystävä C:n kanssa, A tykkää C:stä, mutta A ei ole C:n ystävä.
Toiminnallisen tietokannan näkökulmasta kysely näyttäisi tältä:

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

Lukijaa pyydetään ratkaisemaan tämä ongelma itsenäisesti SQL:ssä. Oletetaan, että ystäviä on paljon vähemmän kuin niitä, jotka pitävät. Siksi ne ovat erillisissä taulukoissa. Onnistuneen ratkaisun tapauksessa ongelmana on myös kaksi tähteä. Hänen ystävyytensä ei ole symmetristä. Toiminnallisessa tietokannassa se näyttäisi tältä:

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: ongelman ratkaisu ensimmäisellä ja toisella tähdellä alkaen 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 

Johtopäätös

On huomattava, että yllä oleva kielen syntaksi on vain yksi vaihtoehdoista yllä olevan konseptin toteuttamiseksi. Se oli SQL, joka otettiin pohjaksi, ja tavoitteena oli tehdä siitä mahdollisimman samanlainen. Tietenkin joku ei ehkä pidä avainsanojen nimistä, sanojen kirjainkokoista ja niin edelleen. Pääasia tässä on itse konsepti. Halutessasi voit tehdä sekä C ++:sta että Pythonista samanlaisen syntaksin.

Kuvatulla tietokantakonseptilla on mielestäni seuraavat edut:

  • helppous. Tämä on suhteellisen subjektiivinen indikaattori, joka ei ole ilmeinen yksinkertaisissa tapauksissa. Mutta jos tarkastellaan monimutkaisempia tapauksia (esimerkiksi tähdillä varustettuja tehtäviä), tällaisten kyselyiden kirjoittaminen on mielestäni paljon helpompaa.
  • Инкапсуляция. Joissakin esimerkeissä olen ilmoittanut välifunktiot (esim. myyty, osti jne.), joista rakennettiin myöhempiä toimintoja. Tämän avulla voit tarvittaessa muuttaa tiettyjen toimintojen logiikkaa muuttamatta niistä riippuvien toimintojen logiikkaa. Voit esimerkiksi tehdä myyntiä myyty laskettiin täysin eri kohteista, kun taas muu logiikka ei muutu. Kyllä, RDBMS:ssä tämä voidaan tehdä CREATE VIEW -toiminnolla. Mutta jos kirjoitat kaiken logiikan tällä tavalla, se ei näytä kovin luettavalta.
  • Ei semanttista aukkoa. Tällainen tietokanta toimii funktioiden ja luokkien kanssa (taulukoiden ja kenttien sijaan). Samalla tavalla kuin klassisessa ohjelmoinnissa (olettaen, että menetelmä on funktio, jonka ensimmäinen parametri on luokan muodossa, johon se kuuluu). Näin ollen universaalien ohjelmointikielten "ystävystymisen" pitäisi olla paljon helpompaa. Lisäksi tämän konseptin avulla voit toteuttaa paljon monimutkaisempia toimintoja. Voit esimerkiksi upottaa tietokantaan tällaisia ​​lauseita:

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

  • Perinnöllisyys ja polymorfismi. Toiminnallisessa tietokannassa voit ottaa käyttöön useita perintöjä CLASS ClassP: Class1, Class2 konstruktien avulla ja toteuttaa useita polymorfismia. Kirjoitan todennäköisesti tulevissa artikkeleissa kuinka tarkalleen.

Vaikka tämä on vain käsite, meillä on jo Java-toteutus, joka kääntää kaiken toiminnallisen logiikan relaatiologiikaksi. Lisäksi esitysten logiikka ja monet muut asiat on kauniisti ruuvattu siihen, minkä ansiosta saamme kokonaisuuden foorumi. Käytämme käytännössä RDBMS:ää (toistaiseksi vain PostgreSQL:ää) "virtuaalikoneena". Tämä käännös aiheuttaa joskus ongelmia, koska RDBMS-kyselyn optimoija ei tunne tiettyjä tilastotietoja, joita FDBMS tietää. Teoriassa on mahdollista toteuttaa tietokannan hallintajärjestelmä, joka käyttää tallennustilana tiettyä rakennetta, joka on sovitettu erityisesti toiminnalliseen logiikkaan.

Lähde: will.com

Lisää kommentti