关系数据库大家都听说过了,随着 EdgeDB 1.0 的发布,图-关系模型的概念逐渐成形。本文将从关系模型讲起,为大家介绍第一款图-关系数据库背后独特的的设计。
本文部分翻译了我同事 Colin McDonnell(@colinhacks)尚未发布的文章《Defining Graph-relational》,因为一来我不想全文翻译他的内容,二来我想夹带我自己的示意图,把我曾经也觉得困惑的地方说清楚。
更新:Colin 的文章已经发布:《The graph-relational database, defined》。
关系模型
“关系数据库”这个名字我们(绝大多数人)从学编程就开始叫了,但这里的关系到底指的是什么?
有人说关系就是数据库里的表——对,也不对,因为关系模型确实是以表为基础的,但这并没有解释清楚为什么关系就是表。那我们就来看一张表:
这张表表示了表示金额的数字大小写之间的转换关系,所以这是一张数字大小写关系表,也可以说是一种两类数字间的关系,所以表是一种关系。
因为我们对数据库表的概念更熟悉,那干脆就反过来定义关系好了:不同列上的值组成了同一行数据,就意味着这些值必定满足某种关系,比如例子中 1
和壹
的对应关系;而如果每一行都遵循同样的关系,那整张表就能代表这种关系本身了——或者至少是这种关系的一部分例子。比如我们的大小写转换关系表就只列出了壹贰叁
,虽然三个数足以示意这是一种大小写转换关系,但却不能表示所有的关系,比如231
与 贰佰叁拾壹
的转换关系。
这种用表格的形式来描述一组有关系的数据的建模方式,就叫关系模型。除了关系数据建模,关系模型还包括了一组可以变着花样查询和修改关系数据的计算方法,以及一系列制定数据完整性约束条件的策略。再举一个更实际的例子来说明:
这张图里有三种关系(三张表):
- 电影名和年份的关系,表示电影是哪一年发行的;
- 人名和年份的关系,表示这人是哪一年出生的;
- 电影名、演员名和角色名的关系,表示该演员在这部电影中饰演的角色。
这三张表之间又有一些“关系”(此关系非彼关系!),因为电影名和人名有相同的。基于这一点,我们可以很容易地计算出,演员在参演的电影发行时是多大岁数:
要保证这种计算是可行的,我们就需要一些保证数据完整的约束条件:
- 年份取值范围为 0 - 9999(约束表达式);
- 角色表中的电影名和演员名必须出现在电影表和演员表中(外键);
- 电影名在电影表中不能重复(主键);
- 人名在演员表中不能重复(主键)。
这就是关系模型的三要素:关系数据建模、关系代数演算和完整性约束。
关系数据库
关系数据库是基于关系模型设计开发的一类数据库,我们平常用的 MySQL、PostgreSQL 和 SQL Server 等都是关系数据库。
与关系模型不同的是,关系数据库都不约而同的使用了 SQL 来表示三要素中的关系代数演算,这一点从他们的名字里就能看得出来。SQL 并不是关系模型的一部分,只不过因为历史原因成为了最流行的查询语言,毕竟谁都不想用数学表达式来写代码(隐约能听到有不服气的声音)。关于 SQL 的各种吐槽我就不再重复了,有兴趣的同学可以看看之前翻译的《SQL 就这?我们能做得更好》一文。
有了关系模型超强的表达力和查询能力,加上对实现细节的完美抽离,关系数据库得以连续数十年“销量遥遥领先”,甚至在 NoSQL 运动的冲击下都岿然不动。提到 NoSQL——虽然有一些场景特别适合用 NoSQL 来做,但终究还是抛弃了关系模型的许多优点,最终并不能取而代之,跟我们要讲的图-关系数据库也没什么关系。
尽管关系数据库风采依然,但毕竟是上个世纪 70 年代的产物,再加上 SQL 拖后腿,是时候革新换代了。
扩展关系模型
对一个成功的模式加以扩展,通常是一种聪明且省力的办法。对扩展关系模型的尝试,我们并不是历史上的第一个。
上世纪 90 年代,随着面向对象概念的盛行,人们也把矛头指向了数据库领域,在关系数据库中使用对象仿佛是一种完美的组合,于是对象关系数据库(object-relational database,简称 ORD)的概念应运而生。可惜的是,介于当时技术硬件和软件的限制,这一概念并没有为开发者带来太多的好处,最终成为了 SQL 的手下败将,变成了 SQL 庞大标准中的一部分,也就是你可能都不怎么用的 SQL 用户自定义类型。
有趣的是,EdgeDB 曾经以 ORD 自居,但因为太容易与关系数据库的 ORD 特性混淆了,因此放弃了这个称号。
究其原因,ORD 并没有从根本上对关系模型进行扩展,而是在小范围内增加了一些新功能,再加上 SQL 强大的阻力,ORD 沦为一种关系数据库的特性也不足为奇。要想釜底抽薪地改变现状,我们必须看到事物的本质,从概念上做出改革。关系数据库的核心就是表和 SQL,那不如干脆把表和 SQL 都干掉吧?
仍然保留核心的“关系”概念,用更符合现代编程直觉的概念来重新建模——于是我们把目光投向了图结构。
图数据库
在图结构模型中,节点和边构成了数据关系的主要结构,具体数据则以属性的形式存在于边和节点上,如下图所示:
这种建模方式让人耳目一新,感觉是十分符合直觉的逻辑。然而,以此模型为基础开发的图数据库却不温不火(相较于关系数据库而言),跟其他类型的 NoSQL 数据库一样,图数据库在特定领域能把其特点发挥到极致,但作为通用基础常规数据库来讲,还是拼不过关系数据库。图数据库的劣势也正是它的优势——对非结构化数据及其关联关系的处理极为高效,但同时也就意味着处理结构化数据时,必然无法像关系数据库那般便捷。我对图数据库了解不多,举个可能不恰当的例子:找出所有姓张的电影角色,算出其演员当时的平均年龄。熟悉 SQL 的你可能已经知道怎么 JOIN 怎么加索引了,但是如何在图数据库中高效完成这一任务,应该至少是属于高级技巧之一了。
既然图数据库的路子走不通,那我们就试试看,能否把图模型中让人觉着舒服的那一部分,移花接木到关系模型上。
图-关系模型
究其根本,图模型无非有三点让人倍感舒适:
- 节点就是很明确的一个数据对象放在那里,可以有多个属性;
- 基于 1,可以很轻松的创建节点之间的关联关系(边),连一条线就好了;
- 关联关系(边)有名字有属性,根据同一个名字就能找出所有的边,并且这些边有哪些属性也是一致的。
那我们就一条一条来看,为什么关系数据库缺少这些特性。
首先,关系数据库中一行数据对应图模型里的一个节点,但关系数据库并不要求每一行数据必须有一个 ID。虽然在实践中,我们大多会给每个表加一个 ID 字段,但这毕竟不是数据库本身的设计。所以第二点就无法自然地从数据库层面上实现,你必须定义一个外键(或者“软外键”)来表达这种节点之间的关联关系,对于多对多关系,还得依托于一张额外的关系表,就如同前面的演员表一样。最后,关系数据库连这种“关系”都没有,更别提内置多对多关系了。
诚然,精通关系数据库的你,可以毫无障碍的飞速实现上述逻辑,并且 SQL 写的风生水起。可是如果我们能把这些更符合人类直觉的图的概念,融合到关系数据库中,在配以远远超越 SQL 的查询语言和声明式建模,那为何不试他一试呢?
下面我们就以世界上第一款图-关系数据库——EdgeDB 的建模语言为例,一步一步地扩展关系模型,设计出我们理想中的图-关系模型。
0、改口
既然不要表了,那么我们先得给“关系”起个新的名字。因为面向对象仍然是现代编程语言中主流的思想,也是最符合思维直觉的概念,那么我们就用这个体系来起名字吧:
关系模型 | 图-关系模型 |
---|---|
表(“relation”) | 对象类型(type) |
列或字段(“attribute”) | 属性(property)或链接(link) |
行(“tuple”) | 对象(object) |
用这些新概念来定义一个关系,大概是这样的:
type 人物 {
required property 姓名 -> str;
}
type 电影 {
required property 名称 -> str;
link 导演 -> 人物;
multi link 演员 -> 人物 {
property 角色 -> str;
};
}
这里展示的是 EdgeQL 的 schema 定义语言(SDL),但正如我说过的,这里只是拿 EdgeDB 作为一个例子来说明图-关系模型的概念,EdgeDB 在将来也不会是唯一的图-关系数据库。
查询语句大概长这样:
select 电影 {
名称,
演员: { 姓名, @角色 }
} filter .名称 = "甲方乙方"
师出有名了,我们就可以依次把图的三种特性扩展到关系模型中。
1、内置唯一 ID
既然是内置 ID,那自然不需要用户来定义了。在 EdgeDB 中,内置 ID 是一个必选的、只读的、唯一的、叫做 id
的属性,在创建数据时自动分配一个 UUID 的值,以保证不会出现重复。在 SDL 语言中的定义如下:
required property id -> uuid {
constraint exclusive;
readonly := true;
default := uuid_generate_v1mc();
}
在将来,其他图-关系数据库可以有不同的 ID 实现,但关键在于,图-关系模型要求所有对象必须有一个 ID。因为有了 ID,我们才可以进行第二步:
2、链接对象
在关系模型中,表之间的关系并不是一等公民,就是用加装了外键的普通字段来表示的。但在图-关系模型中,我们要给对象之间的关系升一级,直接用“链接”的概念来把不同的对象关联在一起,就像图数据库中连一条线那么简单:
对于链接的定义来说,不再需要跟外键过不去了,更无需定义关系表,直接用 link
关键字定义即可,图-关系模型自己会知道该怎么用 ID 去创建关联关系:
type 电影 {
required property 名称 -> str;
+ link 导演 -> 人物;
+ multi link 演员 -> 人物;
}
跟图数据库一样,图-关系模型也允许在链接上定义属性:
type 电影 {
required property 名称 -> str;
link 导演 -> 人物;
multi link 演员 -> 人物 {
+ property 角色 -> str;
};
}
是不是有内味了?
3、属性增加基数概念
链接也算是对象的一种特殊属性。为了支持多对多链接(以及一些其他一致性原因),图-关系模型允许一个属性有多个值,在 EdgeQL 中则是用 multi
关键字来表示,比如 multi link
或 multi property
。这么一来,有些属性是一个值,有些是多个值,有些可能干脆没有值,这个怎么统一一下呢?
图-关系模型直接上狠招了:把所有东西都当做是一个集合来对待。也就是说,电影
的 名称
是一个只有一个字符串的集合,导演
是一个最多有一个 人物
的集合,而 演员
则是由多个 人物
组成的一个集合。那么“基数”的定义就有了,就是一个集合中能有几个元素。这里的几个元素并不是任意数字都行,而是以下五种组合中的一种(对于 EdgeDB 来说):必须为空
、最多有一个
、有且仅有一个
、最少有一个
或 几个都行
。
在关系数据库中,我们知道一个字段有两种属性:字段名、字段类型。在图-关系模型中,对象的属性则有三个维度:属性名、属性类型和属性基数。比如说,如果我们的 电影
有四个属性:
type 电影 {
property 介绍 -> str;
required property 名称 -> str;
multi property 别名 -> str;
required multi link 演员 -> 人物 {
property 角色 -> str;
};
}
那么这四个属性就是:
属性名 | 属性类型 | 属性基数 |
---|---|---|
介绍 | str | 最多有一个 |
名称 | str | 有且仅有一个 |
别名 | str | 几个都行 |
演员 | 人物 | 最少有一个 |
值得一提的是,关系模型中也有一种办法来设置基数:not null
约束。用 EdgeDB 的话来说,not null
就是把 最多有一个
改成 有且仅有一个
。
万物皆集合
我们这里说的集合,就是数学意义上的集合,在 EdgeDB 中就没有独立存在的标量,大家都是一个集合,只是基数不同罢了:
edgedb> select "hi";
{'hi'}
edgedb> select {"hi"};
{'hi'}
edgedb> select {"hi", "there"};
{'hi', 'there'}
edgedb> select "hi" union "there";
{'hi', 'there'}
而这带来了一个巨大的好处,就是在 SQL 中作恶多端的 NULL
也被干掉了。没有东西就是一个空集,可以和其他集合无差别地进行一致的集合运算。
然而通过不同编程语言的 EdgeDB 官方客户端库来使用 EdgeDB 时,空集会被翻译成相应语言中原生的类型:如果空集出现时基数大于 1(
几个都行
或者最少有一个
),接口会返回一个空数组,而其他基数则会返回null/nil/None
(对应不同语言)。
结语
我们相信,这种新的概念——连同其具体设计一起——开启了数据库的新篇章,将会正面挑战关系模型的地位。另外,上周我们发布了 EdgeDB 的第一个稳定版,超级棒,值得一试!
也欢迎关注我们的 OSCHINA 项目主页、知乎专栏和掘金专栏,获取更多中文资讯。
我们在 Gitee 开设了中文沟通交流讨论区,欢迎前来开新帖(Issue)。