likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 39

本文特设有 V2EX 讨论区(中文)、HN 讨论区(英文)。

(作为 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)(P\vee\neg P)(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 <bool>{};
{}
edgedb> SELECT True AND <bool>{};
{}

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

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

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

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

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

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

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

edgedb> WITH empty_set_expr := <int64>{}
....... SELECT empty_set_expr ?? {1, 2};
{1, 2}
edgedb> WITH empty_set_expr := <int64>{}
....... 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.<movie[IS Review] {
            body,
            rating,
            author: {
                name,
                image,
            }
        }
        ORDER BY .creation_time DESC
    ),
};

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

写在最后

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

我们在 Gitee 开设了中文沟通交流讨论区,欢迎前来开新帖(Issue)。

Footnotes

  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. ↩2

  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. ↩2

  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.

转载自:https://juejin.cn/post/7057547778087469086
评论
请登录