功能性数据库管理系统

数据库世界长期以来一直由使用 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构造引入多重继承,实现多重多态。 我可能会在以后的文章中具体写出具体方法。

尽管这只是一个概念,但我们已经在 J​​ava 中实现了一些实现,将所有功能逻辑转换为关系逻辑。 另外,表示的逻辑和许多其他东西都完美地附着在它上面,因此我们得到了一个完整的 平台。 本质上,我们使用 RDBMS(目前仅使用 PostgreSQL)作为“虚拟机”。 这种转换有时会出现问题,因为 RDBMS 查询优化器不知道 FDBMS 知道的某些统计信息。 理论上,可以实现一个数据库管理系统,该系统将使用特定的结构作为存储,专门针对功能逻辑进行调整。

来源: habr.com

添加评论