功能性資料庫管理系統

資料庫世界長期以來一直由使用 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();

讓我們繼續處理另一個更複雜的任務 文章。 它包含如何在 MS SQL 中實現此任務的詳細分析。

任務 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:第一個和第二個星號問題的解決方案 dss_卡里卡:

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 中實現了一些實現,將所有功能邏輯轉換為關係邏輯。 另外,表示的邏輯和許多其他東西都完美地附著在它上面,因此我們得到了一個完整的 平台。 本質上,我們使用 RDBMS(目前僅使用 PostgreSQL)作為「虛擬機器」。 這種轉換有時會出現問題,因為 RDBMS 查詢最佳化器不知道 FDBMS 所知道的某些統計資料。 理論上,可以實現一個資料庫管理系統,該系統將使用特定的結構作為存儲,專門針對功能邏輯進行調整。

來源: www.habr.com

添加評論