SQL 是声明式语言吗?

我们在学习 SQL 时,常常会听到这样的说法:SQL 是一种声明式语言。你只需要告诉它要做什么,而不需要告诉它怎么做,它会自己找到实现方法。也就是说,你要只用它描述任务目标,而不需要说明计算过程,这和常规的过程式语言有本质的差别。显然,这种程序语言听起来要易学好用得多。
真有这么好?

看一个例子,我们用 SQL 来查询员工中销售部女员工的数量,这是写出来的 SQL:

SELECT COUNT(*) FROM employee WHERE department='Sales' AND gender='Female'

看起来是这样,我们不需要关心具体的计算过程(遍历员工表中每一条记录,碰到符合条件的则计数加 1,不符合条件者略过,最后看计数),只要说清要查询的目标就可以了。
再举一例,按部门统计 30 岁以上员工的平均工资:

SELECT department,AVERAGE(salary) FROM employee WHERE age>30 GROUP BY department

看起来也不错,在这里我们确实不必关心到底如何分组和计算平均。
尽管 SQL 仍然是一种语法严格的语言,我们经过一定的学习才能写出正确的语句,但如果能不关心计算过程,那还是会省很多事的。

我们再看一个例子:找出销售额贡献度在前一半的大客户。如果设计一下计算过程大概是这样:
1. 计算所有客户的总销售额
2. 把客户按销售额倒排序,大的在前小的在后
3. 按这次序从 0 累加每个客户的销售额,超过总销售额的一半时停止,则已经遍历过后客户则是目标客户
那么,用 SQL 写出来是什么样的呢?这是我能想出来的最简单写法了:

SELECT customer, amount, cum_amount
FROM ( SELECT customer,amount,SUM(amount) OVER (ORDER BY amount DESC) cum_amount FROM ordersummary )
WHERE cum_amount < (SELECT SUM(amount) FROM ordersummary)/2

仔细看一下这句 SQL,它基本上就是在描述上述过程,有个子查询计算总销售额,再有个子查询按销售额倒排序,利用窗口函数计算出排序后列表的每一行的累计销售额,主查询再过滤出累计销售额小于总销售额一半的客户。和上述过程不同的只是书写次序,SQL 把开始计算总销售额写在了后面。还有一个微小的逻辑差异,SQL 的有序和分步运算不好,要把所有的累计销售额都计算出来再找出排在前面的。
这句 SQL 妥妥地是在描述这样的一个过程。说好的只要描述任务目标而不必关心计算过程呢?

再看简单一些的例子:查询销售额最大的 10 名客户。
某些 SQL 写出来是这样:

SELECT TOP 10 customer FROM ordersummary ORDER BY amount DESC

如果用某著名数据库来做,还得用子查询:

SELECT customer
FROM ( SELECT rownumber rn,customer FROM ordersummary ORDER BY amount DESC )
WHERE rn<=10

这两个 SQL 都明白无误地告诉我们计算过程:按销售额倒排序之后取前面 10 个。而且在这个著名数据库的语法中,还要人为造一个序号,也就是需更明确地告诉数据库应该如何算。

如果再找个数百行的 SQL(存储过程)来看,则可以更清楚地看到 SQL 照样也在老老实实地描述计算过程,而且不同的计算过程还会带来截然不同的计算性能甚至计算结果。

其实,任何程序设计语言都可以在某种程度说成是声明性语言:即只需要关心目标而不必关心过程。
用 Java 写程序,你只要关心变量如何变化,而不必关心 CPU 中寄存器的动作,但用汇编语言就要关心了;同样,而用汇编语言时,你要关心寄存器的取值了,但却不必关心 CPU 里面与非门是如何动作的;
用 SQL 写代码时一般不用再关心变量、循环的具体动作,它就没有变量这些概念,但它有表、字段这些概念及相关的计算方法,你一样要关心这个层面的过程。从这个意义上讲,SQL 和其它程序设计语言在描述问题的方法上只是抽象层次不同,对于过程的说明并没有任何本质的不同。

为什么有这么多人会觉得 SQL 是所谓的声明式语言呢?这是因为 SQL 的设计时刻意弱化了“过程性”的特征,它为了让语句更像英语,把基本运算放进一条语句的各个子句。当计算任务涉及的环节都是 SQL 抽象层次内的基本运算,那就可以书写成一条语句中,貌似向 SQL 描述任务目标就行。
而常规程序语言通常不会把多个基本运算设计到一条语句的子句或函数参数中,而且提倡由程序员组合,这样人就会觉得需要描述过程,不具有“声明式”的特征了。
但是,一条语句的子句结构再复杂也是有限的,当任务复杂到超过这个结构的范围时,必须使用嵌套子查询或中间结果才能描述时,SQL 这个所谓“声明式”假象就会露馅了,还是要老老实实描述过程思路了。回想前面的例子就可以清楚地看出这一点。

SQL 在面对一些基本查询任务时确实时比 Java 这些高级语言易学好用,但并不是因为它比 Java 有更强的“声明式”特征,而是因为它在结构化数据计算方面的抽象程度比 Java 更高。
SQL 刻意弱化了“过程性”特征,比如没有中间变量这些东西,这导致它描述过程的能力非常弱,面对复杂的任务会难度陡增,甚至于有些任务根本无法只用 SQL 实现,因为缺乏过程能力的 SQL 就不是图灵完备的。所以,数据库厂商后来又只好再补充发明了存储过程以及 CTE 语法。
如果我们发明一种对数据计算有较高抽象程度的语言,同时保留“过程式”的特征,你会发现它会比 SQL 更易学好用,特别是面对复杂的业务逻辑时候。嗯,这就是 esProc SPL。