我们可以做得比SQL更好(2019)

2020-08-09 12:12:58

我们经常听到的问题是“为什么要创建一种新的查询语言?”和“SQL有什么问题?”这篇文章包含了这两个问题的答案。

在我们开始之前,让我们先来概述一下关系模型是如何产生的,以及SQL是如何创建的。

关系模型是由Edgar F.Codd在1970年的一篇开创性论文“A Relational Model of Data for Large Shared Data Bank”[4]中引入的。在那里,Codd假设数据库中的所有数据都可以用称为关系的元组集来表示。Codd还发明了一种一阶谓词逻辑来描述数据库查询:元组关系演算。

科德的想法是革命性的。第一次,用简洁、一致的数学模型描述了数据库和查询数据库的通用方法。这自然引起了人们对进一步研究的兴趣,更重要的是,对关系模型的实际实现产生了浓厚的兴趣。

1974年,唐纳德·张伯林和雷蒙德·博伊斯发表了一篇论文[2],介绍了“一组关于表格结构的简单操作,[…]。与一阶谓词演算的幂相当“。Chamberlin和Boyce认为当时提出的正式关系查询语言对于“没有接受过正式数学或计算机编程培训的用户”来说太难理解了,并认为“该语言的主要用途将是特别查询”[3]。最初,作者并不认为SQL是一种“严肃”的语言。尽管如此,对关系模型商业应用的极大兴趣推动了IBM迅速采用SQL并将其生产出来,这也被他们迅速崛起的竞争对手-Oracle所采用。

IBM在当时对技术市场有压倒性的巨大影响,因此SQL成为关系数据库的事实上的标准,然后随着1989年第一个ANSI标准的发布成为一个适当的标准,该标准基本上限制了SQL的最重要的现有实现。该标准的后续版本继续主要受商业供应商的影响。

今天,SQL是迄今为止使用最广泛的数据库语言。但这并不一定意味着它代表了我们所能做的最好的事情。事实上,SQL作为一种“简单、特别”的语言,再加上竞争厂商提供的“按实现进行设计”,已经给该语言留下了严重问题的包袱。

SQL,特别是它的早期版本,受到了专家们的严厉批评,包括Codd本人[6],以及C.J.Date,他发表了大量关于这个主题的论文和书籍([7],[9],[10])。虽然许多早期的缺陷在后来的标准版本中得到了修复,但一些严重的问题只是更加根深蒂固。

这里的一些抱怨适用于整个SQL,而另一些则针对特定的实现。我们在示例中主要使用PostgreSQL。

系统内聚力差-SQL与应用程序语言和协议的集成不够好。

编程语言中的正交性意味着一组相对较小的原语构造可以以相对较少的方式组合在一起。具有良好正交性的语言更小、更一致,并且更容易学习,这是因为整个规则集几乎没有例外。相反,糟糕的正交性会导致具有许多例外和警告的大型语言。

编程语言中正交性的一个很好的例子是能够用变量或函数调用替换表达式的任意部分,而不会对最终结果产生任何影响。

在SQL中,这种泛型替换是不可能的,因为有两种互不兼容的表达式:

表表达式是生成表的SQL表达式:SELECT*FROM TABLE。

标量表达式是生成单个标量值的SQL表达式:SELECT COUNT(*)FROM TABLE。

表表达式只能在FROM子句、函数中使用,或与明确要求表表达式作为输入的运算符一起使用。更糟糕的是,标量表达式和表表达式可能具有完全相同的语法,这可能会导致进一步的混淆。

例如,假设我们需要列出一家公司所有部门负责人的名字。此查询将执行以下工作:

现在,假设我们需要将此位添加到一个更大的查询中,该查询提取关于一个部门的信息。一种直观的方法是简单地将上述内容作为子查询添加到我们的较大查询的目标列表中:

选择部门名称。。。(从emp中选择名称,其中Role=';Department Head';和destno=Department。否)来自部门。。。

这是合法的,但前提是子查询返回的行数不超过一行。否则,将在运行时引发错误。要考虑多个部门负责人,我们需要使用联接重写查询:

选择部门名称。。。来自部门内部的HEAD.NAME将emp作为Head On加入(HEAD.DEPTNO=Department.。否和负责人。角色=';部门负责人';)。。。

结构上的差异足够大,使得任何类型的源级查询重用都是不切实际的。

很少有人声称SQL是一种优雅的语言,其特点是正交性。有人称它为泥脚上的大象。每添加一次,它的身体就会生长,而且变得不那么稳定。SQL标准化在很大程度上是数据库供应商的领域,而不是没有商业利益的学术研究人员或有用户利益的用户。[1]。

SQL不是一种小语言。在编写本文时,PostgreSQL实现包含469个关键字。仅SQL:2016标准的第2部分(共14页)就有1732页。

主要原因是SQL本着最初的目标,致力于成为一种类似英语的语言,迎合“非专业人士”的需求。然而,随着语言的发展,这种繁琐对编写和理解SQL查询的能力造成了负面影响。我们用COBOL学到了这一课,世界早已转向更新、更简洁的编程语言。

除了关键字扩散之外,上面讨论的正交性问题使查询更加冗长且更难阅读。

SQL在语法和语义上都是任意不一致的。更糟糕的是,不同的数据库有自己的SQL版本,通常与其他SQL变体不兼容。

SELECT子串(';PostgreSQL';from 8 for 3);SELECT子串(';PostgreSQL';,8,3);--仅PostgreSQL语法SELECT TRIM(FROM';yxSQLxx';,';xyz&39;);SELECT EXTRACT(DAY FROM TIMESTAMP';2001-02-16 20:38:40';);

有两种语法指定聚合函数中输入集的顺序:

像这样的不一致的清单不会在这里结束,还可以继续,但这超出了本文的范围。

在某些对丢失信息处理不充分的情况下,问题被错误地认为是关系模型的问题。事实上,问题源于SQL的不足及其与关系模型的不一致。[6]。

人们广泛认为NULL是SQL的最大缺点([5]、[8]、[9])。事实上,当代SQL实现中对NULL的处理是如此令人惊讶、不一致和危险,因此本主题需要一个单独的部分。

事实上,几乎所有对NULL的操作都会返回NULL,其效果可能非常微妙:

Postgres=#create table x(a int,b text);create TABLEpostgres=#INSERT INTO x(a,b;one';),(2,';Two';),(NULL,#39;Three';)返回a,b;a|b-+-1|one 2|Two|Three(3行)postgres=#select*from x WHERE a NOT IN(。

在存在NULL的情况下,许多传统的逻辑和布尔代数规则不能安全地应用于SQL布尔表达式。例如,如果p为空,则排除中间定律p或not p不会求值为TRUE:

Postgres=#SELECT COUNT(*)FROM x WHERE a=1或a!=1;COUNT-2(1行)。

Postgres=#select b,case When a=1 Then';one';don';not one';end from x;b|case-+-一|一二|不是一三|不是一(3行)postgres=#select b,case When a!=1则';不是一个';Else';one';end from x;B|案例-+-一|一二|不是一三|一(3行)。

包含b=3的行会被归类为';One&39;或';Not';,即使CASE表达式的构造在这两种情况下看起来都是相同的。

Postgres=#select count(A)from x;count-2(1行)postgres=#select cardinality(array_agg(A))from x;cardinality-3(1行)postgres=#select count(*)from x;count-3(1行)。

Postgres=#select(null,1)is null;?column?-f(1行)postgres=#select(null,1)is not null;?column?-f(1行)

更糟糕的是,数据库通常使用NULL来指示错误条件,因此您的查询可能包含NULL,即使您并不期望它们:

MySQL>;SELECT 1/0;+-+|1/0|+-+|NULL|+-+集合中的1行,1个警告(0.00秒)。

类似的情况还有很多,单个SQL实现中没有一致性,更不用说跨所有实现了。

好了,我们已经强调了SQL的缺点。它们为什么重要呢?这都是关于人体工程学的。正交性、紧凑性和一致性都是易于学习和有效使用的编程语言的基本特征,无论专业水平、团队规模和项目复杂性如何。

我们已经习惯了编程语言的不断改进和重新想象。SWIFT、Rust、Kotlin、Go,仅举几例,是工程师人体工效学和生产力进步的很好例子。但是,通常隐藏在ORM和框架层后面的SQL仍然是占主导地位的数据语言。

NoSQL运动的诞生在一定程度上是因为人们对SQL数据库的停滞和不足感到失望。不幸的是,在抛弃SQL的过程中,NoSQL方法也抛弃了关系模型和RDBMS的其他好部分。

关系模型仍然是表示数据的最普遍、最有效的方法。SQL作为一种声明性的、与存储无关的查询语言的概念是强大和通用的。我们也不需要放弃。我们需要的是一种“更好的SQL”:一种向用户提供更强大功能的查询语言,但它也更简单、更一致。

这正是我们通过EdgeQL努力实现的目标。我们花了数年时间进行研究和开发,在不影响正确性的情况下专注于可用性和性能。在我们早先的博客文章中,我们描述了该语言的一些重要特性,但是这里有必要详细说明一下我们是如何解决这篇文章中提出的问题的。

在EdgeQL中,每个值都是一个集合,每个表达式都是集合上的函数,返回一个集合。这意味着,从语法上讲,EdgeQL表达式的任何部分都可以分解为视图或函数,而无需更改查询的其他部分。

SELECT(AVG:=MATH::Mean(Movie{Number_of_Review:=Count(.Review)}.Number_of_Review),max:=max(Movie{Number_of_Review:=Count(.Review)}.Number_of_Review));

这有点笨拙,让我们通过将Movie表达式分解到视图中来使查询更清晰:

With MovieReviewCount:=Movie{Number_of_Review:=Count(.Review)}SELECT(avg:=MATH::Mean(MovieReviewCount.Number_of_Review),max:=max(MovieReviewCount.Number_of_Review),);

因为一切都是集合上的函数,所以EdgeQL查询中只有少数几个关键字,它们主要用于描述查询的主要部分。

在EdgeQL中,丢失数据的概念很简单:它始终是空集,对空集的任何元素操作也同样是空集:

在EdgeQL中,集合是平坦的,即一个集合(包括空集合)不能是另一个集合的元素:

上面的集合构造函数表示法实际上等同于UNION操作,这更好地突出了它的集合性质:

在EdgeDB中,数据模式是以更接近当代应用程序数据模型的方式制定的。这使得数据库-应用程序模式反射变得简单而高效。

选择电影{描述,导演:{FULL_NAME,IMAGE,}ORDER BY.last_NAME,演员:{FULL_NAME,IMAGE,}ORDER BY.LAST_NAME,评论:=(SELECT Movie.<;Movie.[is Review]{Body,Rating,Author:{name,image,}}ORDER BY.Creation_Time DESC),};

再加上广泛的JSON支持,这使得编写REST和GraphQL后端变得简单了一个数量级。

SQL最初的目标是使非程序员能够有效地使用关系数据。尽管它有缺点,但可以说它取得了巨大的成功,大多数数据库都实现或模仿了它。然而,与任何解决方案一样,SQL在支持新的需求、使用模式和用户生产力方面面临着越来越多的不足。现在是我们做点什么的时候了。

[1]Atzeni P.等人,关系模型死了,SQL死了,我自己感觉不太好。ACM SIGMOD记录,42(2):64-68,2013。

[2]Chamberlin D.D,Boyce R.F.,“Sequel:a Structural English Query Language”,ACM SIGFIDET 1974,第249-264页。

[3]Chamberlin D.D,《SQL的早期历史》,IEEE计算史年鉴,34(4):78-82,2012。

[4]Codd E.F.,《大型共享数据库的数据关系模型》,ACM CACM通信,13(6):377-387,1970

[5]Codd E.F.,“关于缺失信息(适用和不适用信息)的更多评论”,ACM SIGMOD Record 16(1):42-47,1987。

[6]Codd E.F.,“数据库管理的关系模型:版本2”,马萨诸塞州Addison-Wesley。1990年。