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
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
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
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
Lähde: will.com