又一门国产数据库语言诞生了,比SQL还好用!
时间:2023-03-13 09:50:02 | 来源:电子商务
时间:2023-03-13 09:50:02 来源:电子商务
数据库语言的目的
为了明确这个目标,我们必须首先了解数据库是做什么的。
这个数据库软件,名字里带着“库”字,会让人觉得它主要是用来存储的。其实数据库有两个重要的功能:
计算和事务!也就是我们常说的OLAP和OLTP,数据库的存储是为这两样东西服务的,简单的存储不是数据库的目标。
我们知道SQL是目前数据库的主流语言。那么,用SQL做这两件事方便吗?
事务函数主要解决写和读的数据一致性问题。实现这个并不难,但是应用程序的界面很简单,操纵数据库读写的代码也很简单。假设目前关系数据库的逻辑存储方式是合理的(即使用表和记录来存储数据,其合理性是另一个复杂的问题,这里就不展开了),那么SQL在描述事务类的功能方面没有大的问题,因为不需要描述复杂的动作,复杂性在数据库中就解决了。
但是计算类的功能不一样。
这里说的计算是一个更宽泛的概念,不仅仅是简单的加减乘除,搜索和关联都可以看作是某种计算。
什么样的计算系统好?
两点:简单、快速。
写的简单易懂,就是程序员写代码快,单位时间就能做更多的工作;跑得越快,越容易理解。当然,我们希望在更短的时间内得到计算结果。
其实SQL中的Q就是查询的意思。发明它的初衷是做查询(也就是计算),这是SQL的主要目标。但是SQL在描述计算任务的时候很难说是称职的。
为什么说SQL不称职呢?
我们经常看到SQL只有两三行的代码,这些SQL确实是写着简单的,但是如果尝试一些复杂化的问题呢?比如:一支股票最长连续上涨了多少天?用SQL写是这样的:
这个语句的工作原理就不解释了,反正有点绕。
这是润乾公司的招聘考题,通过率不足 20%;因为太难,后来被改成另一种方式:把 SQL 语句写出来让应聘者解释它在算什么,通过率依然不高。
这说明什么?说明情况稍有复杂,SQL 就变得即难懂又难写!再看跑得快的问题,还是一个经常拿出来的简单例子:1 亿条数据中取前 10 名。这个任务用 SQL 写出来并不复杂:
但是这个语句对应的执行逻辑是先把所有数据大排序,然后把前10个拿出来,把后10个留出来。众所周知,排序是一个缓慢的动作,会多次遍历数据。如果数据量太大,内存装不下,就需要外部存储作为缓存,性能会进一步急剧下降。如果严格遵循这句话SQL所体现的逻辑,这个操作无论如何也跑不快。但是很多程序员都知道,这个操作不需要大排序,也不需要外存缓存,用一点内存就可以完成一次遍历,也就是有更高性能的算法。可惜用SQL写不出这样的算法。我们只能希望数据库的优化器足够聪明,把这句话SQL转换成高性能的算法来执行,但是情况复杂的时候数据库的优化器不一定可靠。
看来SQL两方面都不够好。这两个不复杂的问题都是这样的。在现实中,几千行SQL代码充满了难以编写和快速运行的情况。
为什么SQL为什么不行?要回答这个问题,我们需要分析我们在用程序代码做什么。本质上,编程的过程就是将解决问题的思想转化为计算机可执行的精确形式语言的过程。举个例子,就像一个小学生解一道应用题,分析完问题,想出解决方法后,要列出四个运算表达式。程序计算也是如此。不仅需要制定出问题的解决方案,而且需要在解决方案完成之前将其转化为计算机能够理解和执行的动作。
用于描述计算方法的形式语言的核心在于所采用的代数系统。所谓代数系统,简单来说就是一些数据类型及其运算规则,比如小学学过的算术,也就是整数、加减乘除。有了这套东西,我们就可以用这个代数系统约定的符号,也就是代码,写出我们想要做的运算,然后计算机就可以执行了。
如果这个代数系统的设计不周到,提供的数据类型和运算不方便,就会使描述算法非常困难。这时候就会出现一个奇怪的现象:将解翻译成代码的难度远远超过解问题本身。
比如从小就学会用阿拉伯数字做日常计算,做加减乘除非常方便。大家都想当然的认为数值运算应该是这样的。其实不一定!估计很多人都知道有个东西叫罗马数字。你知道罗马数字的加减乘除吗?古罗马人是怎么去购物的?
写代码的难度很大程度上是一个代数问题。
看看你跑不快的原因。
软件改变不了硬件的性能,CPU和硬盘能多快就多快。但是我们可以设计一个低复杂度的算法,也就是计算量更少的算法,这样计算机执行的动作会更少,自然会更快。但是光想出算法是不够的,还需要用某种形式的语言把算法写出来,否则计算机是不会执行的。而且写起来比较简单,耗时又麻烦,也没人会用。所以,对于一个程序来说,跑得快和写得简单其实是同一个问题,背后是这种形式语言使用的代数。如果这个代数不好,那么高性能的算法就很难甚至不可能实现,也就不可能快速运行。如上所述,SQL写不出我们期待的小内存单遍历算法,只能寄希望于优化器能跑得快。
我们再打个比方:
上过小学的同学大概都知道高斯计算1+2+3+…+100的故事。普通人只是一步一步推100次。高斯的孩子很聪明。他们发现1+100=101,2+99=101,…,50+51=101,结果是50乘以101,很快就吃完了家里的午饭。
听完这个故事,我们都觉得高斯很聪明,能想到这么巧妙的办法,简单又快捷。这没有错,但是很容易忽略一点:在高斯时代,人类的算术体系(也是一种代数)中就已经有乘法了!前面说过,我们从小学习四则运算的时候,会觉得乘法是理所当然的,其实不是!乘法是加法之后发明的。如果高斯的年龄没有相乘,即使有聪明的高斯,也没有办法快速解决这个问题。
目前主流的数据库是关系数据库,之所以这么叫是因为它的数学基础叫关系代数,SQL是从关系代数理论发展而来的形式语言。
现在我们可以回答了,为什么SQL在预期的两个方面做得不够好?问题出在关系代数上。关系代数就像一个只有加法没有乘法的算术系统。很多事情做不好是必然的。
关系代数已经发明了50年了。50年前的应用需求和硬件环境与今天大不相同。如果继续套用50年前的理论来解决今天的问题,听起来是不是太老套了?然而,这就是现实。因为存量用户太多,目前还没有成熟的新技术出现,所以基于关系代数的SQL仍然是当今最重要的数据库语言。虽然近几十年有了一些改进和提高,但根子没有变。面对当代复杂的需求和硬件环境,SQL是无法胜任的。
而且,很遗憾,这个问题是理论上的,工程上再怎么优化也无济于事。只能有限的改善,无法根除。但是,大多数数据库开发人员并没有想到这一层,或者说为了照顾现有用户的兼容性,不打算想到这一层。于是,主流数据库社区一直在这个圈子里徘徊。
SPL为什么能行
那么如何让计算变得更简单更快捷呢?
发明新的代数!代数与乘法。然后在此基础上设计新的语言。
这就是SPL的起源。它的理论基础不再是关系代数,而是离散数据集。基于这种新的代数设计的形式语言被命名为SPL(结构化过程语言)。
SPL创新了/kloc-0的缺点/(更准确的说,离散数据集是针对关系代数的各种缺陷)。SPL重新定义和扩展了许多结构化数据中的运算,增加离散性,加强有序计算,实现彻底聚合,支持对象引用,提倡分步运算。
由于篇幅所限,我们无法在此介绍SPL(离散数据集)的全貌。这里我们列出了SQL(关系代数)的SPL(离散数据集)的一些差分改进:
游离记录离散数据集中的记录是一种基本的数据类型,可以独立存在,不依赖于数据表。数据表是记录的集合,组成一个数据表的记录也可以用来组成其他数据表。比如过滤操作就是利用原数据表中符合条件的记录组成新的数据表,这样无论是空间占用还是操作性能都更有优势。
关系代数没有表示记录的操作数据类型。单个记录实际上是只有一行的数据表,不同数据表中的记录不能共享。比如过滤操作时,会复制新的记录,形成新的数据表,增加了空间和时间的成本。
特别是因为有自由记录,离散数据集的字段值允许记录是某一条记录,这样可以更方便地实现外键连接。
有序性关系代数是基于无序集合设计的,集合成员没有序号的概念,也不提供位置计算和相邻引用的机制。SQL在实践中做了一些工程上的局部改进,让现代SQL可以方便的进行一些有序的操作。
离散数据集中的集合是有序的,集合的成员都有序号的概念,可以用来访问成员,定义定位操作返回集合中成员的序号。离散数据集提供符号实现集合运算中的相邻引用,支持集合中某个序数位置的计算。
有序操作很常见,但一直是SQL的难题,即使有窗口函数也还是很繁琐。SPL已经大大改善了这种情况,以前股票上涨的例子就能说明问题。
离散性和集合化富集合运算是在关系代数中定义的,即集合可以作为一个整体参与运算,如聚合、分组等。这是SQL比Java等高级语言更方便的地方。
但是关系代数的离散性很差,没有自由记录。而Java等高级语言在这方面没有问题。
离散数据集相当于离散性和聚集性的结合。不仅有聚合数据类型和相关操作,还有聚合成员在聚合之外独立操作或重新组合成其他聚合。可以说SPL结合了SQL和Java两者的优点。
有序操作是离散性和聚集性的典型结合。秩序的概念只有在集合中才有意义,个体成员没有秩序,体现了集体化。有序计算需要对某个成员及其相邻成员进行计算,需要离散性。
在离散性的支持下,可以得到更彻底的聚合,解决有序计算类型等问题。
离散数据集是一个既有离散性又有聚集性的代数系统,关系代数只是聚集。
分组理解分组运算的初衷是将一个大的集合按照一定的规则划分成若干个子集。关系代数中没有可以表示集合的集合的数据类型,所以分组后被迫做聚集运算。
离散数据集中的一组允许集可以代表合理的分组运算结果。分组和分组聚合分为独立的两步操作,这样可以对分组子集进行更复杂的操作。
关系代数中只有一种等价分组,即根据分组键值对集合进行划分,等价分组是完全划分。
离散数据集认为任何拆分大集合的方法都是分组运算。除了传统的等价分组,它还提供了与有序性相结合的有序分组,以及可能导致不完全划分的准分组。
聚合理解关系代数中没有明确的集合数据类型,聚集计算的结果都是单值,分组聚集运算也是如此,只有SUM、COUNT、MAX、MIN等等。尤其是关系代数不能把TOPN运算看成是聚合。用于成套的TOPN只能取结果集排序后的前n项,而用于分组子集的TOPN很难实现,需要改变思路,拼出序号才能完成。
离散数据集提倡泛集,聚合运算的结果不一定是单个值,也可能仍然是一个集合。在离散数据集中,TOPN运算具有与求和计数相同的地位,也就是说,它可以用于完整集或分组子集。
SPL将TOPN理解为聚合运算后,还可以避免工程实现中总数据的排序,从而实现高性能。但是SQL的TOPN总是伴随着ORDER BY的动作,理论上需要大排序才能实现,需要寄希望于在项目实现中对数据库进行优化。
有序支持的高性能离散数据集特别强调有序集,很多高性能算法都可以利用有序特征来实现。这是因为基于无序集的关系代数什么都做不了,只能寄希望于工程优化。
以下是在部分使用有序特征后可以实现的低复杂度操作:
1)数据表对主键是有序的,相当于自然有了索引。关键字段的过滤通常可以快速定位,以减少外部存储器的遍历量。二进制方法也可以用来定位随机键值,当同时检索多个键值时,索引信息可以重用。
2)通常的分组操作是通过哈希算法实现的。如果确定知道数据对的键值都是有序的,就可以只做邻居比较,避免计算哈希值,不会有哈希冲突,非常容易并行。
3)数据表是键对齐有序的,两个大表之间的对齐连接可以执行更高性能的合并算法,只需要遍历一次数据,不需要缓存,占用内存少;然而,传统的哈希值堆方法不仅复杂,而且需要大内存和外部缓存,还可能因哈希函数不当造成二次哈希重新缓存。
4)大型表作为外键表的连接。当事实表很小时,可以从外键表中快速提取关联键值对应的数据,以实现连接,而不需要HASH堆动作。当事实表也很大时,可以用分位数将外键表分成多个逻辑段,然后将事实表按照逻辑段进行堆叠。这样只需要堆叠一张表,哈希堆的时候不会有可能二次堆,计算复杂度可以大大降低。
其中,3和4利用了离散数据集的变换来进行连接操作。如果仍然扩展关系代数的定义(可能会产生多对多的结果),这种低复杂度的算法是很难实现的。
除了理论上的差异,SPL还有很多工程上的优势,比如更容易编写并行代码,大内存预相关提高外键连接的性能,独特的列存储机制支持任意分段并行。
和SPL一起把之前的问题重写一遍,直接感受一下。
一只股票能连续上涨多少天?
计算思路和前面的 SQL 相同,但因为引入了有序性后,表达起来容易多了,不再绕了。
1 亿条数据中取前 10 名:
SPL 有更丰富的集合数据类型,容易描述单次遍历上实施简单聚合的高效算法,不涉及大排序动作。