将SELECT ... FROM改为FROM ... SELECT并不能 "修复 "SQL

727 阅读11分钟

时不时地,我看到有人感叹SQL语法在操作的词法顺序上的特殊脱节。

最近一次是在Youtube上对最近的jOOQ/kotlin讲座的评论回复中。让我们看看为什么jOOQ没有落入这个试图 "解决 "的陷阱,以及为什么这甚至是一个陷阱。

英文语言

SQL有一个简单的语法模型。所有的命令都以动词的形式开始,因为我们*"命令 "*数据库执行一个语句。常见的命令包括。

  • SELECT
  • INSERT
  • UPDATE
  • DELETE
  • MERGE
  • TRUNCATE
  • CREATE
  • ALTER
  • DROP

所有这些都是命令式的动词。想想看,在任何地方都要加一个感叹号,比如说INSERT [this record]!

操作的顺序

我们可以争辩说,自然语言对计算机编程语言来说是非常差的灵感,而计算机编程语言往往更倾向于数学化(有些比其他的更多)。关于SQL语言的很多批评意见是,它没有 "构成"(以其原始形式)。

我们可以争辩说,对于一个更有构成性的SQL语言来说,从FROM 开始会好得多,根据操作的逻辑顺序,它是SELECT 的第一个操作。比如说。

FROM book
WHERE book.title LIKE 'A%'
SELECT book.id, book.title

是的,这将是更好的,因为它更符合逻辑。首先,我们声明数据源、谓词等,最后才会声明投射。使用JavaStream API,我们会写。

books.stream()
     .filter(book -> book.title.startsWith("A"))
     .map(book -> new B(book.id, book.title))

这样做的好处是。

  • 语法和逻辑之间没有脱节
  • 因此。语法上没有混乱,特别是为什么你不能在WHERE 中引用SELECT 的别名,例如。
  • 更好的自动完成(因为你不会先写那些没有声明的东西)。

在某种程度上,这种排序与一些RDBMS在RETURNING DML语句的数据时实现的一致,例如。

  • Firebird
  • Oracle
  • PostgreSQL
INSERT INTO book (id, title)
VALUES (3, 'The Book')
RETURNING id, created_at

对于DML语句,命令("命令")仍然是INSERT,UPDATE,DELETE ,即一个动词,明确告诉数据库对数据 什么。而 "预测 "则更像是一种事后的考虑。一个偶尔有用的工具,因此RETURNING ,可以放在最后。

RETURNING 似乎是一个实用的语法选择,甚至不是标准的一部分。该标准定义了 ,由Db2和H2实现,其语法是。<data change delta table>

SELECT id, created_at
FROM FINAL TABLE (
  INSERT INTO book (id, title)
  VALUES (3, 'The Book')
) AS book

我的意思是,为什么不呢。我对这种或那种语法没有强烈的偏好(jOOQ同时支持这两种语法,并将它们模拟成彼此)。SQL Server发明了第三种变体,其语法可能是最不直观的(我总是要查一下OUTPUT 子句的确切位置)。

INSERT INTO book (id, title)
OUTPUT id, created_at
VALUES (3, 'The Book')

Cypher查询语言

在这里可能值得一提的是,存在一种现代查询语言,它足够流行,可以考虑用于此类讨论。来自neo4j的Cypher查询语言。通过一个简单的 "技巧",它既。

  • 保持了语言模型,即以命令式的动词开始语句(动词是MATCH ,与FROM ,但它是一个动词),所以它继承了SQL的 "优势",即对非程序员也很直观。
  • 颠覆了阅读语句中操作的逻辑顺序,采用MATCH .. RETURN 的形式,使RETURN 成为所有操作中投射事物的通用形式,而不仅仅是SELECT
  • 重用的MATCH ,也用于写操作,包括 [DELETE](https://neo4j.com/docs/cypher-manual/4.4/clauses/delete/)[SET](https://neo4j.com/docs/cypher-manual/4.4/clauses/set/)(对应于SQL的UPDATE)

虽然在不同的数据范式(相对于关系模型网络模型)上操作,但我总是发现Cypher查询语言在语法方面普遍优于SQL,至少在高层次上是这样。如果我不得不通过创建SQL 2.0来实际 "修复 "SQL,我会从这里得到启发。

在像jOOQ这样的API中修复它是不值得的

正如之前所讨论的,SQL有一些明显的缺点,而且还有像Cypher这样的更好的语言来解决同类问题。但是SQL在这里,它已经有50年的历史了,而且它将继续存在。它不会被修复。

这是必须要接受的事情。

SQL不会被修复

它将会被修正。它融入了新的想法,包括。

它总是以一种习惯性的、SQL风格的方式进行。如果你正在阅读SQL标准,或者你正在使用与标准非常接近的PostgreSQL,你会觉得SQL作为一种语言是相当一致的。或者说,它是一贯的怪异,这取决于你的口味。

对于jOOQ来说,主要的成功因素之一一直是在语法方面尽可能地接近SQL的真正含义。很多人在编写本地SQL时非常有效。因为Java有文本块,所以只需从你的SQL编辑器中复制粘贴一个静态SQL查询到你的Java程序中,然后用JDBC或jOOQ的普通SQL模板API来执行它,就变得更容易忍受了。

for (Record record : ctx.fetch(
    """
    SELECT id, title
    FROM book
    WHERE title LIKE 'A%'
    """
)) {
    System.out.println(record);
}

这种方法对于外面非常简单 的应用程序来说是足够的。如果你的 "应用程序 "总共运行5个不同的SQL查询,你可以只用JDBC来做(当然,一旦你开始掌握了jOOQ,你可能也会对这些应用程序使用jOOQ)。

但是,当你的应用程序有100多个查询,包括许多动态查询,而你的数据库有100多个表时,jOOQ才会真正发光,在这种情况下,类型安全和模型安全的好处真的很有帮助。然而,只有当你的SQL查询1:1转换为jOOQ API时,它才能大放异彩。在这个最重要的语句(SELECT )中,随机地对SQL进行某种程度的修正是不会有效果的。

因为。你在哪里会停止修复SQL?即使你切换到FROM .. SELECT ,SQL仍然很奇怪。例如, GROUP BY 的语义仍然很奇怪。或者 DISTINCTORDER BY 之间的关系。例如,这起初看起来会好得多(例如,将SELECTDISTINCT 分开,它们不应该如此紧密地位于一起)。

FROM book
WHERE book.title LIKE 'A%'
SELECT book.title
DISTINCT
ORDER BY book.title

但是奇怪的注意事项仍然不会消失,即在没有DISTINCT 的情况下,你可以ORDER BY ,但在有DISTINCT 的情况下就不能了(见我们之前关于这个问题的文章),这些表达式没有列在SELECT

其他DSL API中的替代语法

那么,SQL的 "修复 "在哪里停止?SQL什么时候才能被 "固定"?它永远不会被修复,因此,像jOOQ这样的API会比它应该的更难学。一些竞争性的API遵循这种模式,例如

这两个API都是基于这样的想法:SQL需要 "修复",而更 "原生"、更 "习惯 "的API感觉会好一些。一些例子。

Slick。

这是入门指南中的一个例子。

coffees.map(_.price).max

这对应于下面的SQL。

SELECT max(price)
FROM coffees

这可以说是一个更成文的例子。它看起来就像普通的Scala集合API的使用,去掉了SQL的感觉。毕竟,通常的map(x => y) 集合方法实际上对应于一个SQLSELECT 子句(一个 "投影")

暴露了。

这里有一个来自Baeldung的例子

StarWarsFilms
  .slice(StarWarsFilms.sequelId.count(), StarWarsFilms.director)
  .selectAll()
  .groupBy(StarWarsFilms.director)

该API引入了新的术语,例如

  • slice 意思与 或 相同,虽然对SQL或kotlin集合API都是陌生的。map() SELECT
  • selectAll,对应于关系代数术语 "选择",对应于SQLWHERE

合成方便的语法,而不是 "修复 "SQL

jOOQ没有走这条路,也永远不会走。SQL就是它,而jOOQ无法 "修复 "它。SQL语法和jOOQ API之间的1:1映射意味着,即使你想使用一些复杂的东西,比如。

  • [GROUPING SETS](https://www.jooq.org/doc/latest/manual/sql-building/column-expressions/grouping-functions/)
  • [CONNECT BY](https://www.jooq.org/doc/latest/manual/sql-building/sql-statements/select-statement/connect-by-clause/)
  • WITH (CTEs)
  • FOR XMLFOR JSON
  • 不管是什么

即使如此,jOOQ也不会让你失望,会让你完全 按照你心中的SQL功能来写。我是说,在Slick或Exposed中支持CONNECT BY ,真的有意义吗?可能不会。他们将不得不发明他们自己的语法,以提供对SQL递归的访问。但它会是完整的吗?这是一个jOOQ不会有的问题。

一些语法不能使用的唯一原因是它不可能实现(请发送一个功能请求)。FOR XML 的例子是一个很好的例子。SQL Server发明了这个FOR 子句,虽然它对简单的情况很方便,但对复杂的情况就不是很强大了。我更喜欢标准的SQL/XML和SQL/JSON语法,(jOOQ 支持这种语法)。但是,尽管不太喜欢这种语法,jOOQ 也不会评判。一个完全由jOOQ发明的第三种语法对用户有什么好处呢?正如我之前所说。

"修复 "何时才能停止?

它永远不会停止。我所提到的替代方案在他们开始添加更多的功能时,如果他们开始添加更多的功能,将会遇到非常困难的问题。虽然实现一个简单的SELECT .. FROM .. WHERE 查询生成器很容易,并使用任意的API支持该功能,声称SQL已经被 "修复",但要发展这个API,解决各种高级的SQL用例则难得多 。只要看看他们的问题跟踪器中的功能请求,如CTE。答案总是。"使用本地SQL"。

即使是 "简单 "的SQL功能,如UNION ,一旦基本的SQL语法被改变,也会变得更加复杂。在SQL中,语义已经足够棘手了(当然,这完全是SQL的错),但 "修复 "这些东西从来都不像一开始看起来那么简单。

现在,这个规则有两个例外。

合成句法

一个例外是:"合成语法"。jOOQ中最强大的合成语法是隐式连接。隐式连接不是在 "修复 "SQL,而是在用SQL本身可能具有的语法(希望最终会有)来 "增强 "SQL。就像存在着SQL方言,它 "增强 "了SQL标准,例如

jOOQ对这种合成语法非常保守。有很多好的想法,但很少有向前兼容的。这些语法中的每一个都使其他SQL转换功能更加复杂,而且每一个都有可能尚未解决的缺陷(例如,从jOOQ 3.16开始,隐式连接在DML语句中是不可能的,如UPDATE,DELETE ,即使它们在那里也有很大的意义。见问题#7508)。

方便的语法

另一种改进是我称之为 "便利语法 "的东西。例如,无论底层的RDBMS是什么,jOOQ都允许你写。

select(someFunction()); // No FROM clause
selectFrom(someTable);  // No explicit SELECT list

在这两种情况下,用户可以省略底层 SQL方言中可能是强制性的条款,而jOOQ则用合理的默认值来填充生成的SQL。

  • 一个FROM DUAL 表声明,或类似的东西
  • 一个SELECT * 投射声明,或类似的东西

结论

jOOQ应该在1:1的基础上坚持使用SQL语法的想法是我13年前做jOOQ时的一次赌博。我想把jOOQ设计成每个已经知道SQL的人在学习jOOQ时都不会有问题,因为一切都很直接。这里描述了这种API设计背后的技术

其他人试图通过使他们的API在考虑到目标语言的情况下非常习惯化,或者通过发明一种新的语言来 "解决 "SQL。

13年后,我发现1:1的模仿方法是唯一可行的方法,因为我不断发现新的、神秘的SQL特性。

我,制作jOOQpic.twitter.com/CyEHanYZBY

- Lukas Eder (@lukaseder)2022年5月16

创建一种语言是非常困难的(让我们把内部DSL的API看作是一种语言)。如果目标是支持几乎所有的底层SQL功能,那么几乎不可能进行正确的设计,除非设计者放下 "修复 "东西的梦想,开始拥抱 "支持 "东西的 "梦想"。所有的东西。

SQL是它的本质。而这意味着,语法是SELECT .. FROM ,而不是FROM .. SELECT

喜欢这个

Like Loading...