Bota e të dhënave është pushtuar prej kohësh nga DBMS-të relacionale, ku përdoret gjuha SQL. Ka arritur në një pikë sa që variacione të reja quhen NoSQL. Atyre iu është mundësuar të gjejnë një vend në këtë treg, por DBMS-të relacionale nuk po shuhen dhe vazhdojnë të përdoren aktivisht për qëllimet e tyre.
Në këtë artikull dëshiroj të përshkruaj konceptin e një baze të dhënash funksionale. Për një kuptim më të mirë, do ta bëj këtë nëpërmjet krahasimit me modelin klasik relacionale. Si shembuj do të përdoren detyra nga teste të ndryshme për SQL, të gjetura në internet.
Hyrje
Baza të dhënash relacionale operojnë me tabela dhe fusha. Në një bazë të dhënash funksionale, ato do të zëvendësohen me klasa dhe funksione përkatësisht. Një fushë në tabelë me N çelësa do të përfaqësohej si një funksion me N parametra. Në vend të lidhjeve ndërmjet tabelave do të përdoren funksione që kthejnë objekte të klasës në të cilën po ndodhet lidhja. Në vend të JOIN do të përdoret kompozita e funksioneve.
Para se të kalojmë drejtpërdrejt te detyrat, do të përshkruaj logjikën e domenit. Për DDL do të përdor sintaksën e PostgreSQL. Për funksional, do të përdor sintaksën time.
Tabelat dhe fushat
Një objekt të thjeshtë Sku me fushat emri dhe çmimi:
Relacional
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
Funksionale
KLASA Sku;
emri = STRINGA TË DHËNASH[100] (Sku);
çmimi = NUMRI TË DHËNASH[10,5] (Sku);
Ne shpallim dy funksione, të cilat pranojnë një parametrin Sku dhe kthejnë një tip primitiv.
Supozimi është se në DBMS-në funksionale, çdo objekt do të ketë një kod të brendshëm, i cili gjenerohet automatikisht dhe i cili mund të merret nëse është e nevojshme.
Do të caktojmë çmimin për produktin / dyqanin / furnizuesin. Ai mund të ndryshojë me kalimin e kohës, prandaj do të shtojmë në tabelë një fushë kohe. Do ta anashkaloj shpalljen e tabelave për referencat në bazën e të dhënave relacionale për të shkurtuar kodin:
Relacional
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)
)
Funksionale
KLASA Sku;
CLASS Dyqani;
CLASS Furnizues;
dateTime = TË DHËNAT DATETIME (Sku, Dyqani, Furnizues);
çmimi = TË DHËNAT NUMERIK[10,5] (Sku, Dyqani, Furnizues);
Indeksat
Për shembullin e fundit, do të ndërtojmë një indeks sipas të gjitha çelësave dhe datës, në mënyrë që të mund të gjejmë shpejt çmimin në një kohë të caktuar.
Relacional
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
Funksionale
INDIKS Sku sk, Dyqani st, Furnizuesi sp, dataKoha(sk, st, sp);
Detyrat
Le të fillojmë me detyra relativisht të thjeshta, të marrë nga përkatësia në Habr.
Fillimisht do të shpallim logjikën e domenit (për bazën relacionale kjo është bërë drejtpërdrejt në artikullin e dhënë).
Departamenti CLASS;
emri = STRING DATA[100] (Departamenti);
CLASS Employee;
department = DATA Department (Employee);
chief = DATA Employee (Employee);
name = DATA STRING[100] (Employee);
salary = DATA NUMERIC[14,2] (Employee);
Detyra 1.1
Shfaqni listën e punonjësve që marrin pagë më të madhe se ajo e shefit të tyre të drejtpërdrejtë.
Relacional
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
Funksionale
Zgjidh emrin (Punonjës a) KU paga(a) > paga(kreu(a));
Detyra 1.2
Shfaqni listën e punonjësve që marrin pagën maksimale në departamentin e tyre.
Relacional
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
Funksionale
maxSalary 'Paga maksimale' (Departamentet) =
GRUPO MAX paga (Punonjësi e) NËSE departamenti(e) = s;
Zgjidh emrin (Punonjësi a) KU paga(a) = maxSalary(departamenti(a));
// или если "заинлайнить"
SELECT name(Employee a) WHERE
salary(a) = maxSalary(GROUP MAX salary(Employee e) IF department(e) = department(a));
Të dy implementimet janë ekuivalente. Në rastin e parë në bazën relacionale mund të përdoret CREATE VIEW, i cili do të llogarisë në të njëjtën mënyrë pagën maksimale për një departament të veçantë. Në vazhdim, për ilustrim do të përdor rastin e parë, pasi ai tregon më mirë zgjidhjen.
Detyra 1.3
Shfaqni listën e ID-ve të departamenteve, numri i punonjësve në të cilët nuk kalon 3 persona.
Relacional
select department_id
from employee
group by department_id
having count(*) <= 3
Funksionale
numriPunonjësve 'Numri i punonjësve' (Departamenti d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Departamenti d KUJDES numriPunonjësve(d) <= 3;
Detyra 1.4
Shfaqni listën e punonjësve, të cilët nuk kanë një shef të caktuar duke punuar në të njëjtin departament.
Relacional
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
Funksionale
Zgjidh emrin(Empeloyee a) KUJDESI (nuk janë (departamenti(kreu(a)) = departamenti(a));
Detyra 1.5
Gjeni listën e ID-ve të departamenteve me pagën totale maksimale të punonjësve.
Relacional
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 )
Funksionale
pagaMaksimale 'Paga Maksmale' (Departamenti d) =
GROUP SUM salary(Employee e) IF department(e) = d;
pagaMaksimaleDepartamentesh 'Paga Maksmale e Departamentit' () =
GRUPI MAX pagaMaksimale(Departamenti d);
Zgjidh Departamentin d KUJ pagatMaksimale(d) = pagaMaksimaleDepartamentesh();
Të kalojmë në detyra më të vështira nga një tjetër . Ajo ofron një analizë të detajuar mbi mënyrën se si të realizoni këtë detyrë në MS SQL.
Detyra 2.1
Cilët shitës e shitën në vitin 1997 mbi 30 copë të produktit nr. 1?
Logjika e domenit (ashtu si më parë në DBMS, do të anashkaloj shpalljen):
CLASS Employee 'Shitës';
lastName 'Mbiemri' = DATA STRING[100] (Punonjës);
CLASS Product 'Produkt';
id = DATA INTEGER (Product);
name = DATA STRING[100] (Product);
CLASS Order 'Porosi';
date = DATA DATE (Order);
employee = DATA Employee (Order);
CLASS Detail 'Rreshti i porosisë';
order = DATA Order (Detail);
product = DATA Product (Detail);
quantity = DATA NUMERIC[10,5] (Detail);
Relacional
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
Funksionale
shitur (Punonjësi e, INTEGER idProdukti, INTEGER vit) =
GRUPI SUM sasia(OrderDetail d) NËSE
punonjës(order(d)) = e DHE
id(produkti(d)) = idProdukti DHE
ekstraktoVitin(data(order(d))) = vit;
ZGJIDH lastName(Punonjësi e) KU shitur(e, 1, 1997) > 30;
Detyra 2.2
Për çdo klient (emri, mbiemri) gjej dy produkte (tituj), për të cilat klienti ka shpenzuar më shumë para në vitin 1997.
Zgjasim logjikën e domenit nga shembulli i mëparshëm:
CLASS Customer 'Klienti';
contactName 'Emri dhe MBIEMRI' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
Relacional
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
Funksionale
shuma (Detaj d) = sasia(d) * çmimiNjësisë(d) * (1 - zbritje(d));
bleu 'Blerë' (Kundër c, Produkt p, INTEGER y) =
GRUPI SHUMË SUM (Detaj d) NËSE
klienti(order(d)) = c DHE
produkti(d) = p DHE
nxirrVit(inorders(d)) = y;
vlerësimi 'Vlerësimi' (Kundër c, Produkt p, INTEGER y) =
SHUMA E PARTISIONIT 1 RENDIT DESC blerë(c, p, y), p NGA c, y;
ZGJEDH emrinKontaktil(Customer c), emrin(Produkt p) KU vlerësimi(c, p, 1997) < 3;
Оператор PARTITION работает по следующему принципу: он суммирует выражение, указанное после SUM (здесь 1), внутри указанных групп (здесь Customer и Year, но может быть любое выражение), сортируя внутри групп по выражениям, указанным в ORDER (здесь bought, а если равны, то по внутреннему коду продукта).
Задача 2.3
Сколько товаров нужно заказать у поставщиков для выполнения текущих заказов.
Опять расширяем доменную логику:
KOMPANIA Furnizues 'Furnizues';
companyName = DATA STRING[100] (Furnizues);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
Relacional
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
Funksionale
ePorositur 'Porositur, por nuk është dërguar' (Product p) =
GRUPI SHUMA sasia(OrderDetail d) NSE produkt(d) = p;
përPorositur 'Këtu për porosinë' (Product p) = ePorositur(p) + niveliRiporosis(p) - njësitëNëStok(p);
Zgjidh emrinKompany(suppliers(Product p)), emri(p), përPorositur(p) KU përPorositur(p) > 0;
Задача со звездочкой
И последней пример лично от меня. Есть логика социальной сети. Люди могут дружить друг с другом и нравится друг другу. С точки зрения функциональной базы данных это будет выглядеть следующим образом:
KLASA Person;
pëlqen = TË DHËNA BOOLEAN (Person, Person);
miq = TË DHËNA BOOLEAN (Person, Person);
Необходимо найти возможных кандидатов на дружбу. Более формализовано нужно найти всех людей A, B, C таких, что A дружит с B, а B дружит с C, A нравится C, но A не дружит с C.
С точки зрения функциональной базы данных запрос будет выглядеть следующим образом:
Zgjidh Person a, Person b, Person c ku
pëlqen(a, c) DHE NUK janë miq(a, c) DHE
janë miq(a, b) DHE janë miq(b, c);
Читателю предлагается самостоятельно решить эту задачу на SQL. Предполагается, что друзей гораздо меньше чем тех, кто нравится. Поэтому они лежат в отдельных таблицах. В случае успешного решения есть также задача с двумя звездочками. В ней дружба не симметрична. На функциональной базе данных это будет выглядеть так:
Zgjidh Person a, Person b, Person c ku
pëlqen(a, c) DHE NUK janë miq(a, c) DHE
(miqte(a, b) OSE miqte(b, a)) DHE
(miqte(b, c) OSE miqte(c, b));
UPD: решение задачи с первой и второй звездочкой от :
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
Përfundimi
Следует отметить, что приведенный синтаксис языка — это всего лишь один из вариантов реализации приведенной концепции. За основу был взят именно SQL, и целью было, чтобы он максимально был похож на него. Конечно, кому-то могут не понравится названия ключевых слов, регистры слов и прочее. Здесь главное — именно сама концепция. При желании можно сделать и C++, и Python подобный синтаксис.
Описанная концепция базы данных, на мой взгляд обладает следующими преимуществами:
- Thjeshtësia. Это относительно субъективный показатель, который не очевиден на простых случаях. Но если посмотреть более сложные случаи (например, задачи со звездочками), то, на мой взгляд, писать такие запросы значительно проще.
- Инкапсуляция. В некоторых примерах я объявлял промежуточные функции (например, sold, bought и т.д.), от которых строились последующие функции. Это позволяет при необходимости изменять логику определенных функций без изменения логики зависящих от них. Например, можно сделать, чтобы продажи sold считались от совершенно других объектов, при этом остальная логика не изменится. Да, в РСУБД это можно реализовать при помощи CREATE VIEW. Но если всю логику писать таким образом, то она будет выглядеть не очень читабельной.
- Отсутствие семантического разрыва. Kjo bazë të dhënash operon me funksione dhe klasa (përveç tabelave dhe fushave). Po ashtu si në programimin klasik (nëse konsiderohet se metoda është një funksion me parametrin e parë si klasën që i përket). Si rezultat, është shumë më e lehtë ta “bashkëpunosh” me gjuhët universale të programimit. Për më tepër, kjo koncept i lejon të implementohen funksione shumë më komplekse. Përshembull, është e mundur të integrohen operatorë të tillë në bazën e të dhënave:
KUSHTESË shes(Employee e, 1, 2019) > 100 NËSE emri(e) = 'Petja' MESAZH 'diçka Petja po shes shumë nga një mall në vitin 2019';
- Trashëgimia dhe polimorfizmi. Në një bazë të dhënash funksionale mund të futet trashëgimia e shumëfishtë përmes konstruktimeve CLASS ClassP: Class1, Class2 dhe të implementohet polimorfizmi i shumëfishtë. Si pikë dhe mënyrë, ndoshta do të shkruaj në artikujt e ardhshëm.
Megjithëse kjo është vetëm një koncept, ne tashmë kemi një implementim të caktuar në Java, i cili përkthen logjikën funksionale në logjikën relacionale. Plus, është e lidhur bukur logjika e paraqitjeve dhe shumë gjëra të tjera, për shkak të të cilave rezulton një e tërë . Në thelb, ne përdorim RDBMS (deri tani vetëm PostgreSQL) si një "makinë virtuale". Me këtë përkthim, ndonjëherë paraqiten probleme, pasi optimizuesi i kërkesave RDBMS nuk di njohuritë e caktuara që e di FSDBMS. Në teori, është e mundur të implementohet një sistem menaxhimi të bazës së të dhënave, i cili do të përdorë si depo një strukturë të përshtatur saktësisht për logjikën funksionale.
Burimi: habr.com
