資料庫世界長期以來一直由使用 SQL 語言的關聯 DBMS 主導。 以至於新興的變體被稱為NoSQL。 他們成功地在這個市場上為自己贏得了一席之地,但關係型 DBMS 不會消亡,並且會繼續積極地用於他們的目的。
在本文中,我想描述功能資料庫的概念。 為了更好地理解,我將透過將其與經典關係模型進行比較來做到這一點。 將使用在互聯網上找到的各種SQL測試的問題作為範例。
介紹
關係資料庫對資料表和欄位進行操作。 在函數式資料庫中,將分別使用類別和函數。 表中具有 N 個鍵的欄位將表示為 N 個參數的函數。 將使用函數傳回所連接的類別的對象,而不是表之間的關係。 將使用函數組合代替 JOIN。
在直接討論任務之前,我將描述域邏輯的任務。 對於 DDL,我將使用 PostgreSQL 語法。 對於函數式它有自己的語法。
表和字段
一個帶有名稱和價格欄位的簡單 Sku 物件:
關係型
CREATE TABLE Sku
(
id bigint NOT NULL,
name character varying(100),
price numeric(10,5),
CONSTRAINT id_pkey PRIMARY KEY (id)
)
功能性的
CLASS Sku;
name = DATA STRING[100] (Sku);
price = DATA NUMERIC[10,5] (Sku);
我們宣布兩個 功能,它將一個參數 Sku 作為輸入並傳回一種原始類型。
假設在功能性 DBMS 中,每個物件都會有一些自動產生的內部程式碼,並且在必要時可以存取。
讓我們設定產品/商店/供應商的價格。 它可能會隨著時間的推移而改變,所以讓我們在表中新增一個時間欄位。 我將跳過聲明關係資料庫中目錄的表以縮短程式碼:
關係型
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)
)
功能性的
CLASS Sku;
CLASS Store;
CLASS Supplier;
dateTime = DATA DATETIME (Sku, Store, Supplier);
price = DATA NUMERIC[10,5] (Sku, Store, Supplier);
指數
對於最後一個例子,我們將在所有按鍵和日期上建立一個索引,以便我們可以快速找到特定時間的價格。
關係型
CREATE INDEX prices_date
ON prices
(skuId, storeId, supplierId, dateTime)
功能性的
INDEX Sku sk, Store st, Supplier sp, dateTime(sk, st, sp);
任務
讓我們從相應的相對簡單的問題開始
首先,讓我們聲明域邏輯(對於關聯式資料庫,這在上面的文章中直接完成)。
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
顯示薪資高於其直接主管的員工清單。
關係型
select a.*
from employee a, employee b
where b.id = a.chief_id
and a.salary > b.salary
功能性的
SELECT name(Employee a) WHERE salary(a) > salary(chief(a));
任務 1.2
列出本部門領取最高薪資的員工
關係型
select a.*
from employee a
where a.salary = ( select max(salary) from employee b
where b.department_id = a.department_id )
功能性的
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));
兩種實作是等效的。 對於第一種情況,在關聯式資料庫中您可以使用CREATE VIEW,它以相同的方式首先計算其中特定部門的最高工資。 在下文中,為了清楚起見,我將使用第一種情況,因為它更好地反映了解決方案。
任務 1.3
顯示部門ID列表,其中員工人數不超過3人。
關係型
select department_id
from employee
group by department_id
having count(*) <= 3
功能性的
countEmployees 'Количество сотрудников' (Department d) =
GROUP SUM 1 IF department(Employee e) = d;
SELECT Department d WHERE countEmployees(d) <= 3;
任務 1.4
顯示沒有指定經理在同一部門工作的員工清單。
關係型
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
功能性的
SELECT name(Employee a) WHERE NOT (department(chief(a)) = department(a));
任務 1.5
尋找員工薪資總額最高的部門 ID 清單。
關係型
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 )
功能性的
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();
讓我們繼續處理另一個更複雜的任務
任務 2.1
1997 年哪些賣家銷售了超過 30 件 1 號商品?
域邏輯(與之前在 RDBMS 上一樣,我們跳過聲明):
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);
關係型
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
功能性的
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
對於每位買家(名字、姓氏),找出該買家在 1997 年花最多錢的兩種商品(姓名)。
我們擴展前面範例的域邏輯:
CLASS Customer 'Клиент';
contactName 'ФИО' = DATA STRING[100] (Customer);
customer = DATA Customer (Order);
unitPrice = DATA NUMERIC[14,2] (Detail);
discount = DATA NUMERIC[6,2] (Detail);
關係型
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
功能性的
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 運算子的工作原理如下:它對指定群組(此處為Customer 和Year,但可以是任何表達式)內SUM 後指定的表達式(此處為1)求和,並按ORDER 中指定的表達式式在群組內排序(這裡買的,如果相等,則按內部產品代碼)。
任務 2.3
需要從供應商訂購多少商品才能完成目前訂單。
我們再擴充一下領域邏輯:
CLASS Supplier 'Поставщик';
companyName = DATA STRING[100] (Supplier);
supplier = DATA Supplier (Product);
unitsInStock 'Остаток на складе' = DATA NUMERIC[10,3] (Product);
reorderLevel 'Норма продажи' = DATA NUMERIC[10,3] (Product);
關係型
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
功能性的
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;
星號的問題
最後一個例子是我個人的。 這是社群網路的邏輯。 人們可以互相成為朋友並互相喜歡。 從功能資料庫的角度來看,它看起來像這樣:
CLASS Person;
likes = DATA BOOLEAN (Person, Person);
friends = DATA BOOLEAN (Person, Person);
有必要找到可能的友誼候選人。 更正式地說,你需要找到所有的人 A、B、C,使得 A 是 B 的朋友,B 是 C 的朋友,A 喜歡 C,但 A 不是 C 的朋友。
從功能資料庫的角度來看,查詢將如下所示:
SELECT Person a, Person b, Person c WHERE
likes(a, c) AND NOT friends(a, c) AND
friends(a, b) AND friends(b, c);
鼓勵讀者自己用 SQL 解決這個問題。 假設你的朋友比你喜歡的人少很多。 因此它們位於不同的表中。 如果成功的話,還有一個兩顆星的任務。 其中,友誼並不是對稱的。 在功能資料庫上,它看起來像這樣:
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:第一個和第二個星號問題的解決方案
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
結論
應該注意的是,給定的語言語法只是實現給定概念的選項之一。 以SQL為基礎,目標是盡可能與其相似。 當然,有些人可能不喜歡關鍵字、單字暫存器等的名稱。 這裡最主要的是概念本身。 如果需要,您可以讓 C++ 和 Python 的語法相似。
在我看來,所描述的資料庫概念具有以下優點:
- 簡單。 這是一個比較主觀的指標,在簡單的情況下並不明顯。 但是,如果您查看更複雜的情況(例如,星號問題),那麼在我看來,編寫此類查詢要容易得多。
- Инкапсуляция。 在一些範例中,我聲明了中間函數(例如, 出售, 買 等),從中建構後續功能。 這允許您在必要時更改某些函數的邏輯,而無需更改依賴它們的函數的邏輯。 例如,您可以進行銷售 出售 是從完全不同的物件計算出來的,而其餘的邏輯不會改變。 是的,這可以使用 CREATE VIEW 在 RDBMS 中實作。 但如果所有的邏輯都這麼寫的話,看起來可讀性就不太好。
- 無語意差距。 這樣的資料庫對函數和類別(而不是表格和欄位)進行操作。 就像在經典程式設計中一樣(如果我們假設一個方法是一個函數,其第一個參數為其所屬類別的形式)。 因此,與通用程式語言「交朋友」應該更容易。 此外,這個概念允許實現更複雜的功能。 例如,您可以嵌入以下運算子:
CONSTRAINT sold(Employee e, 1, 2019) > 100 IF name(e) = 'Петя' MESSAGE 'Что-то Петя продает слишком много одного товара в 2019 году';
- 繼承和多態性。 在函數式資料庫中,可以透過CLASS ClassP:Class1、Class2構造引入多重繼承,實現多重多態。 我可能會在以後的文章中具體寫出具體方法。
儘管這只是一個概念,但我們已經在 Java 中實現了一些實現,將所有功能邏輯轉換為關係邏輯。 另外,表示的邏輯和許多其他東西都完美地附著在它上面,因此我們得到了一個完整的
來源: www.habr.com