Erlo

【译】SQL 就这?我们能做得更好

2022-01-27   165   开源中国
页面报错/反馈
点赞

原文作者 Elvis Pranskevichus(@elprans)是我老板,原文《We Can Do Better Than SQL》发布于 2019 年 5 月 9 日。EdgeDB 将在农历虎年正月初十举办 1.0 版本线上发布会,欢迎在掘金和 OSCHINA 关注我们,共迎后 SQL 时代的黎明。

SEQUEL

(作为 EdgeDB 的开发者,)我们经常碰到这样的问题:“(你们)为什么要创造一种新的查询语言?”“SQL 它不香吗?”答案就在这篇文章里。开始之前,我们先回顾一下关系模型和 SQL 的诞生历史。

关系模型与 SQL 简史

埃德加·科德在 1970 年的论文《大型共享数据池中的关系模型》[1]中正式引入了“关系模型”的概念,假定一种叫做“关系”的元组集合足以表达数据库中的所有数据。为了描述数据库查询,他还发明了一种一阶谓词逻辑:元组关系演算。

科德的想法在当时是颠覆性的,因为这是有史以来第一次,人们可以用一种简洁一致的数学模型,来描述一个数据库,和一种通用查询语言。自然而然地,越来越多的人对此产生了兴趣并展开了进一步的研究。更重要的是,人们开始有意义地尝试实现关系模型。

1974 年,唐纳德·钱柏林和雷蒙德·博伊斯发表了一篇论文[2],引入了“与一阶谓词逻辑有同样效力的……用于表结构的一组简易运算”(译注:也就是 SQL,论文里还是 SEQUEL)。当时他们感觉到,对于“没有接受过正规数学或计算机编程训练的用户”来说,学界提出的严格关系查询语言太晦涩难懂,并认为“该语言(SQL)大概八成是要用来做临时查询”[3],也就是说,他俩起初并没有把 SQL 当做是一门“正儿八经”的语言。然而无独有偶,对关系模型的商业应用抱有极大兴趣的 IBM 公司,迅速拾起了这门技术,并快速将其产品化;其迅速崛起的竞争对手 Oracle 公司也紧随其后地采用了 SQL 技术。

在当时,IBM 对科技市场有着举足轻重的影响力,所以 SQL 很快就成为了关系数据库的事实标准,并在随后的 1989 年成为了美国国家标准学会(ANSI)发布的正式标准。ANSI 的标准基本上是按照当时的几大 SQL 实现划定的,该标准的后续版本也没能逃脱商业公司的决定性影响。

现如今,SQL 已然成为使用最广泛的数据库语言,但那并不意味着我们应止步于此。实际上,这门语言有着一箩筐的严重问题,因为其最初就是按照“简易临时”的用途来设计的,再加上后来竞争厂商们本着“先做出来再设计”的原则,对 SQL 标准的设计更是雪上加霜。

对 SQL 的批评

包括科德本人[4]在内的许多专家都对 SQL——尤其是其早期版本——提出了严厉的批评,还有克里斯托佛·达特也发表了大量批评 SQL 的论文和书籍[5][6][7]。在后续版本的标准中,虽然许多早期缺点得到了弥补,但是另一些严重的问题却变得更加根深蒂固。

以 PostgreSQL 为例,SQL 的“罪状”主要可归为以下四类,其中一些适用于所有 SQL,另一些则只是针对特定实现:

  • 正交性设计不合理——SQL 语句很难写;
  • 不够紧凑——SQL 语言里废话太多;
  • 一致性太差——SQL 在语法和语义上不够一致;
  • 集成度不足——SQL 不能很好地融入应用程序语言与通讯协议。

正交性设计不合理

在编程语言中“正交”的意思是,你可以用相对固定的几种不同的方式,来拼装一个相对精简的基础语法集。如果一门语言的正交性设计得好,那么它应该是小巧的、有规律可循的,并且因为语法规则的特殊情况较少,所以十分容易学习和上手。相反地,如果正交性的设计不合理,你就会看到一个臃肿的语言,充满了各种特殊用法和注意事项。

举一个正面的例子,如果你把一个表达式中的任意部分,替换成一个变量或是函数调用,最后结果却没有任何影响,那么这种编程语言就算是有良好的正交性。

但是在 SQL 语言中,实现类似的替换却遥不可及,因为 SQL 定义了两种互相不兼容的表达式:

  • Table 表达式返回一个表:SELECT * FROM table
  • Scalar 表达式返回一个单独的标量值:SELECT count(*) FROM table

Table 表达式只能用于 FROM 从句、函数或某些仅针对 Table 表达式的运算符,而更糟糕的是,Scalar 表达式往往有着与 Table 表达式一模一样的语法,让人感觉是糊涂他妈给糊涂开门——糊涂到家了。比如说,现在我们要列出全公司部门领导的姓名,查询语句可以这样写:

SELECT name
FROM emp
WHERE role = '部门领导'

接着需求变了,要求把领导姓名列表合并到部门信息的查询中。那么按照直觉,就应该把上面的姓名列表做成一个子查询,添加到部门查询的父语句中:

SELECT
    dept.name,
    ...
    (SELECT name FROM emp
     WHERE role = '部门领导' AND deptno = dept.no)
FROM
    dept
    ...

这看似没什么毛病,也能跑出结果,但仅限于每个部门只有一个领导的情况。当子查询返回了多个结果时,这条 SQL 就挂了。为了应对多个领导(的情况),我们只能用 JOIN 来重写这条查询:

SELECT
    dept.name,
    ...
    head.name
FROM
    dept
    INNER JOIN emp AS head
        ON (head.deptno = dept.no
            AND head.role = '部门领导')
    ...

而此时的结构已经与最初相差甚远了,要想达到 SQL 源码级别的重用,基本不太现实。

不够紧凑

没人会说 SQL 是一门优雅的正交性语言。更有甚者把 SQL 比作是泥足巨象——每次增加新功能,大象就越胖、越不稳定。SQL 的标准化就是数据库厂商的游乐场,他们对无利益瓜葛的学者或有兴趣参与的用户不屑一顾。[8]

————保罗·阿切尼等人

SQL 语言十分臃肿,在撰写本文时(2019 年中),PostgreSQL 实现了 469 个关键字,而这只是 SQL:2016 标准中的第二部分,而完整的标准一共有 14 个部分,1732 页

而这其中最主要的原因是,SQL 想要成为一种类似英语的语言,以达到“非专业人士”也能使用的最初设想。然而随着 SQL 语言的发展,这种语言上冗长的模仿,逐渐变成了编写和理解 SQL 语言的障碍。COBOL 已经给我们上过这一课了,人们早就转向更新颖、更简洁的编程语言了。

SQL 如此啰嗦难读,除了因为关键字的“增生症”,之前提到的正交性问题也做出了不小的贡献。

一致性太差

SQL 语言在语法和语义上都很任性地缺乏一致性,而更糟糕的是,不同的数据库还创造了他们自己版本的 SQL,而这些 SQL 变种之间通常又互不兼容。

来看几个完全不一样的调用方式:

SELECT substring('PostgreSQL' from 8 for 3);
SELECT substring('PostgreSQL', 8, 3); -- PostgreSQL 特有语法

SELECT trim(both from 'yxSQLxx', 'xyz');
SELECT extract(day from timestamp '2001-02-16 20:38:40');

在聚合函数中指定排序方式,可以这样:

SELECT array_agg(foo ORDER BY bar)

也可以这样:

SELECT rank(foo) WITHIN GROUP (ORDER BY bar)

此等不一致罄竹难书,本文就到此打住了。

NULL——惊不惊喜意不意外

在某些数据缺失的情况下,人们会把缺少完美的处理方式,错误地归罪与关系模型本身。但其实,问题的根源在于 SQL 自己的缺陷,及其别扭地操作关系模型的方式。[4]

——埃德加·科德

很多人都说 NULL 是 SQL 设计的一大败笔[6][9][10],但实际情况更糟糕,以至于要用一个单独的章节来解释,现今这些 SQL 实现对 NULL 的处理是多么的惊人、危险和不一致。

NULL 十分特殊,特殊到它不等于任何值,包括它自己:

postgres=# select NULL = NULL;
 ?column?
----------

(1 row)

实际上,对 NULL 做几乎任何操作都会返回 NULL,并且对结果产生微妙的影响:

postgres=# CREATE TABLE x (a int, b text);
CREATE TABLE
postgres=# INSERT INTO x(a, b)
           VALUES (1, 'one'), (2, 'two'), (NULL, 'three')
           RETURNING a, b;
 a |   b
---+-------
 1 | one
 2 | two
   | three
(3 rows)

postgres=# SELECT * FROM x WHERE a NOT IN (1, null);
 a | b
---+---
(0 rows)

但在另一些情况下,NULL 觉得自己又行了,比如在 DISTINCT 中:

elvis=# SELECT DISTINCT *
        FROM (VALUES (1), (NULL), (NULL)) AS q;
 column1
---------

       1
(2 rows)

或是 UNION

elvis=# VALUES (1), (NULL) UNION VALUES (2), (NULL);
 column1
---------

       1
       2
(3 rows)

在 SQL 的真假表达式中,大部分传统逻辑和布尔代数碰到 NULL 都会出幺蛾子。比如排中律 P∨¬P 就拿 NULL 没办法:

postgres=# SELECT count(*) FROM x WHERE a = 1 OR a != 1;
 count
-------
     2
(1 row)

然而没有最糟,只有更糟:

postgres=# SELECT
               b,
               CASE WHEN a = 1
               THEN '是 1'
               ELSE '不是 1'
               END
           FROM x;
   b   |  case
-------+---------
 one   | 是 1
 two   | 不是 1
 three | 不是 1
(3 rows)

postgres=# SELECT
               b,
               CASE WHEN a != 1
               THEN '不是 1'
               ELSE '是 1'
               END
           FROM x;
   b   |  case
-------+---------
 one   | 是 1
 two   | 不是 1
 three | 是 1
(3 rows)

于是乎 b = three 这一行就变成了薛定谔的 1,尽管两个 CASE 表达式看起来是等价的。

含有 NULL 的数据对统计学来说也是一个神秘的存在:

postgres=# SELECT count(a) FROM x;
 count
-------
     2
(1 row)

postgres=# SELECT cardinality(array_agg(a)) FROM x;
 cardinality
-------------
           3
(1 row)

postgres=# SELECT count(*) FROM x;
 count
-------
     3
(1 row)

你无法比较含有 NULL 值的两行数据:

postgres=# SELECT (NULL, 1) = (NULL, 1);
 ?column?
----------

(1 row)

甚至连 IS NULL 都不管用:

postgres=# SELECT (NULL, 1) IS NULL;
 ?column?
----------
 f
(1 row)

postgres=# SELECT (NULL, 1) IS NOT NULL;
 ?column?
----------
 f
(1 row)

更糟糕的是,数据库经常会用 NULL 来指示一种错误状态,有时候你不想 NULLNULL 了:

postgres=# SELECT (ARRAY[1, 2])[3];
 array
-------

(1 row)

postgres=# SELECT to_char(timestamp '2001-02-16 20:38:40', '');
 to_char
---------

(1 row)

除数为零,PostgreSQL 还会抛出一个异常,MySQL 索性直接 NULL 了:

mysql> SELECT 1 / 0;
+-------+
| 1 / 0 |
+-------+
|  NULL |
+-------+
1 row in set, 1 warning (0.00 sec)

以上只是冰山一角,而且 NULL 的行为在同一 SQL 实现中都无法做到一致,更别提不同的实现了。

即便这样又如何呢?

OK,讲完了 SQL 的种种缺点,回头看我们也与它们共存了这么多年了,那到底有什么要紧的呢?其实这是一个人类工效学的问题(译注:虽然是同一个词“ergonomics”,但这里不是指鼠标电竞椅这类人体工学硬件,而是符合人类思维直觉的、能提高开发效率的软件)。对于一种编程语言,要想让任何水平的开发者、任何大小的团队以及任何复杂度的项目,都能轻松上手和高效利用,语言的正交性、紧凑性与一致性都是必不可少的先决条件。

我们已经习惯了编程语言令人耳目一新的快速发展,诸如 Swift、Rust、Kotlin 和 Go 这样的榜样都极大地推动了工程师的开发效率和生产力,然而 SQL 语言却能持续霸榜近 50 年,通常还有层层 ORM 和各种框架为它打掩护。

在对 SQL 数据库停滞不前的抱怨中——从一定程度上讲——人们发起了 NoSQL 运动。但不幸的是,NoSQL 不光丢弃了 SQL 语言,还放弃了关系数据库中许多有价值的东西,比如关系模型。

EdgeQL:查询语言的进化

对于表示数据,关系模型仍然是最普遍和有效的方法;而在 SQL 中,声明式的语法和与存储无关的概念依然闪闪发光。对于这两条,我们并不需要彻底放弃,我们真正需要的是一个“更好的 SQL”:一种更加简洁一致,同时又能让使用者如虎添翼的查询语言。

这就是我们创造 EdgeQL 所追寻的目标,在不影响正确性的同时,专注于易用性和综合性能,不惜花费数年时间用来做研发。下面我会展开介绍一些亮点,看我们是如何解决前面提到的各种问题的。

正交性、一致性与紧凑性

在 EdgeQL 里,任何值都是一个集合,任何表达式都是一个集合到集合的函数。这意味着从语法上来讲,在不改动其他部分的前提下,EdgeQL 语句中的任何表达式都可以提炼并替换成一个视图或函数调用。

比如说一个用来渲染电影列表及其评论数量的查询:

SELECT Movie {
    description,
    number_of_reviews := count(.reviews)
};

如果需要所有电影的平均评论数量,可以这么组合:

SELECT math::mean(
    Movie {
        description,
        number_of_reviews := count(.reviews)
    }.number_of_reviews
);

如果还需要一个单部电影的最高评论数量,可以继续组合:

SELECT (
    avg := math::mean(
        Movie {
            number_of_reviews := count(.reviews)
        }.number_of_reviews
    ),
    max := max(
        Movie {
            number_of_reviews := count(.reviews)
        }.number_of_reviews
    )
);

略显笨重?那就重构一下,把 Movie 表达式提取为一个视图:

WITH
    MovieReviewCount := Movie {
        number_of_reviews := count(.reviews)
    }
SELECT (
    avg := math::mean(MovieReviewCount.number_of_reviews),
    max := max(MovieReviewCount.number_of_reviews),
);

因为一切都是集合上的函数,所以 EdgeQL 仅有屈指可数的几个关键字,勾勒出查询语句的主体结构。

缺失数据的处理

EdgeQL 对于缺失数据的处理方式十分简单:“没有”就是一个空集,对空集的操作结果同样还是一个空集:

edgedb> SELECT True OR {};
{}
edgedb> SELECT True AND {};
{}

聚合函数的行为也是一致的:

edgedb> SELECT count({});
{0}
edgedb> SELECT array_agg({});
{[]}

在 EdgeQL 中,集合是一维的,也就是说,集合中不能有集合,包括空集:

edgedb> SELECT {1, {2, 3}, {}, {}};
{1, 2, 3}

上面这种集合构造的方法等价于使用 UNION 运算符,下面的例子更能凸显其集合的本质:

edgedb> SELECT {1} UNION {2, 3} UNION {} UNION {};
{1, 2, 3}

空集也可以通过结合运算,变成非空集:

edgedb> WITH empty_set_expr := {}
....... SELECT empty_set_expr ?? {1, 2};
{1, 2}
edgedb> WITH empty_set_expr := {}
....... SELECT {1, 2, 3} IF EXISTS empty_set_expr ELSE 42;
{42}

系统集成

EdgeDB 使用了一种十分接近现代应用开发风格的数据模型定义方式,这让应用程序与数据库之间数据模型的映射关系变得直观高效。

与 SQL 不同,EdgeQL 可以轻松地封装出任意的数据结构:

SELECT Movie {
    description,

    directors: {
        full_name,
        image,
    }
    ORDER BY .last_name,

    cast: {
        full_name,
        image,
    }
    ORDER BY .last_name,

    reviews := (
        SELECT Movie.

配合上大量 JSON 功能的加持,REST 和 GraphQL 后端的开发将变得异常简单。

写在最后

为了让非开发人员也能高效地操作关系数据,人们创造了 SQL。抛开缺点来看,SQL 语言可以说是取得了巨大的成功,大部分数据库都实现或模拟了 SQL。然而技术的进步是不可阻挡的,随着不断增长的新型需求、使用方式和开发效率,SQL 本身也面临着后劲不足的问题。是时候做点什么了。

  1. Codd E. F., “A relational model of data for large shared data banks”, Communications of the ACM CACM, 13(6):377-387, 1970
  2. Chamberlin D. D, Boyce R. F., “SEQUEL: A Structured English Query Language”, ACM SIGFIDET 1974, pp 249-264.
  3. Chamberlin D. D, “Early History of SQL”, IEEE Annals of the History of Computing, 34(4):78-82, 2012
  4. Codd E. F., “The relational model for database management: version 2” Addison-Wesley, Mass. 1990.
  5. Date C. J., “A critique of the SQL database language”, ACM SIGMOD Record 14(3):8-54, 1984.
  6. Date C. J., “Where SQL falls short”, Datamation 33(9):83-86, 1987.
  7. Date C. J., “SQL and Relational Theory”, O’Reilly, 2009.
  8. Atzeni P. et al., The relational model is dead, SQL is dead, and I don’t feel so good myself. ACM SIGMOD Record, 42(2):64-68, 2013.
  9. Codd E. F., “More commentary on missing information (applicable and inapplicable information)”, ACM SIGMOD Record 16(1):42-47, 1987.
  10. Date C. J., “Null Values in Database Management. In Relational Databases: Selected Writings”, Addison-Wesley, Mass. 1986.

评论留言

还没有评论留言,赶紧来抢楼吧~~

每日一笑关闭

返回顶部

给这篇文章打个标签吧~

棒极了 糟糕透顶 好文章 PHP JAVA JS 小程序 Python SEO MySql 确认