因为工作中需要同时面向 MySQL、Oracle 和 SQLServer 三种数据库进行开发,所以,大概从去年国庆节开始,我开始使用一个开源的数据库管理工具——DBeaver。

使用这个工具的初衷,是因为我不想在同一台电脑上安装三个客户端工具,尤其是 Oracle 和 SQLServer 这种令人恐惧的、动辄需要重装系统的应用程序。我不想再使用类似 Navicat 这样的软件,因为它的画风像是上个世纪 VB6.0 的产品一样,同理,我不喜欢用 PL/SQL,因为我每次都要瞪大眼睛,在它狭窄而拥挤的画面上找表、找视图,更有甚者,有时要去找触发器、找存储过程。直到我同事给我发了一个几十 M 的文档,我突然间意识到,这货居然还要安装 Oracle 的客户端,配置数据库连接要手动去改配置文件,我一点都不喜欢 PL/SQL。

除了这三种经典的关系型数据库,我们还会用 Memcache 和 Redis 这样的内存数据库,Mongodb 这样的非关系型数据库,所以,我希望有一个统一的入口来管理这些连接,毕竟我身边的同事会使用三种以上的工具,譬如SqlyogPL/SQLSQLServer等来处理这些工作,恰好 DBeaver 可以满足我 80%的工作需要。目前,DBeaver 企业版支持关系型数据库和非关系型数据库,而社区版仅支持关系型数据库。

可最近在写 Oracle 环境的触发器(存储过程和触发器都是万恶之源)时,我发现 DBeaver 和 PL/SQL 在面对同一段 SQL 脚本时,居然因为一点点语法上的差异而不兼容,这让我内心深处不由得想对 Oracle 吐槽一番。这是一个什么样的 SQL 脚本呢?我们一起来看下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE OR REPLACE TRIGGER "TRI_SYNC_ITEM_VALUE"
BEFORE DELETE ON "or_line"
FOR EACH ROW
DECLARE
v_item_value NUMBER(18,6);
BEGIN
SELECT ITEM_VALUE INTO v_item_value FROM "order_info" WHERE ORDER_GID = :OLD.ORDER_GID;
v_item_value := v_item_value - :OLD.PACKAGE_COUNT * NVL(to_number(:OLD.OL_UDF7),0);
IF v_item_value < 0 THEN
v_item_value:= 0;
END IF;

UPDATE "order_info" SET ITEM_VALUE = v_item_value WHERE ORDER_GID = :OLD.ORDER_GID;
END "TRI_SYNC_ITEM_VALUE";

INSERT INTO "sys_upgrade_history"(UPGRADE_TYPE,VERSION_NO,UPDATE_DATE,REMARK) VALUES('版本更新','10005',SYSDATE,'Normal');

这是实际业务中编写的一个简单触发器脚本,我们通常的编写习惯是,在写完触发器或者存储过程以及函数后,会在升级历史中插入一天新纪录,所以,这个脚本实际上由两部分组成。如果这段脚本分两次执行,那么在 DBeaver 和 PL/SQL 中效果是一样的。可如果我们希望一次执行整个脚本,根据 PL/SQL 的规范,一个 PL/SQL 脚本由如下结构组成:

1
2
3
4
5
6
DECLARE
[声明部分]
BEGIN
[过程部分]
END;
/

这个时候,我们就要在这两部分脚本间增加一个分隔符——/。可尴尬的是,这种写法在 DBeaver 中是无法编译执行的,因为它认为/是个无效的 SQL 关键字。我一直疑心这是个 Bug,因为 Github 上曾有人提过类似的 Issue,作者回复说,DBeaver 并没有完全实现 PL/SQL 语法的解析,而最近更新的 6.0 版本中提到:对 Oracle 环境的存储过程编译进行了强化。博主尝试升级到最新版本,发现这个问题依然存在,哪怕用 Ctrl+Enter 来执行一样会报错,于是我想从这件事吐槽下某数据库,从哪里说起呢,就从 PL/SQL 说起吧!

标准与私货

我想一开始学习 SQL 语法的时候,大家绝对不会想到,看起来和谐而统一的结构化查询语言,其实是貌合神离。为什么这样说呢?因为我真的不知道,一个时间函数居然可以有 SYSDATE、NOW()和 GETDATE()三种写法,我更不知道,有一天会因为不知道 ROWNUM 而被面试官鄙视,更不必说每种数据库都会定义一两种不一样的数据类型,这东西号称是有一个标准吗?比如 SQL92/99 这个标准定义了 DML(数据操作语言)、DDL(数据定义语言)、DCL(数据控制语言)和 TCL(事务控制语言)四种分类,所以,SQL 的定位其实更接近于交互式命令行,它是命令式的查询语言,而非过程式的声明语言。

可在标准化进程缓慢的大背景下,每一家数据库厂商都在往自家产品里夹藏私货,以甲骨文为首的 Oracle 发展出了 PL/SQL、以微软为首的 SQLServer 发展出了 T-SQL。其实,我很能理解这种标准跟不上时代发展需要的阵痛,就像我们的 Web 领域直到 10 年前后才提出了 HTML5 标准,在此之前,我们为不同的浏览器的兼容性煞费苦心,兼容 IE8 与否甚至成为了评价技术好坏的一个隐性标准,可说句实话,浏览器的 Bug 难道不应该让浏览器厂商来修复吗?关前端工程师什么事?同样的,数据库间的差异,让我们的脚本失去了可移植性,触发器、存储过程这种严重依赖数据库的东西,一旦更换了数据库,基本等于要重头再写一遍,如今的小程序让 Web 变成信息孤岛,甚至 Chrome 正在变成下一个 IE,这就是所谓“屠龙少年战胜恶龙,自身亦化为恶龙吗”?

这种不统一带来的弊端就是,我们永远写出可以完美“跨”数据库的 SQL,现在跨平台基本成为了大家的共识,因为操作系统间的差异越来越小,以我个人为例,我使用的大多数软件都可以找到对应的 Linux 版本,这样做的好处是,我可以在无差别地从 Windows 切换到 Linux。可现在,我们必须在 MySQL 里使用 VARCHAR、而在 Oracle 里使用 NVARCHAR,而在 SQLServer 里又要使用 NVARCHAR2,可明明它们都是表示一样的东西啊,类似的还有 MediumText 和 CLOB,是不是起一个不一样的名字会显得与众不同呢?更不必说在 DDL 中表约束相关的语法存在差别了。我被告知 Oracle 脚本中表名要用双引号括起来,理由是 Oracle 区分大小写,加上双引号就可以让它忽略大小写,忽略大小写不应该给 Oracle 一个设置吗?为什么要让我再写个多余的双引号呢?诸如此类,举不胜举。

SQL 是个好 DSL 吗

SQL 标准定义的 SQL,就是一个以集合论为基础的结构化查询语言,它天生适合的场景就是,你在命令行中输入 SQL 语句,然后它去执行你输入的 SQL 语句,它就像我们大多数情况下使用的交互式命令行,不然,为什么 MySQL 要提供命令行版本,主流的数据库管理工具都提供了输入 SQL 语句的窗口。可我们同样能意识到,SQL 的表达能力有限,它无法表达顺序、条件、循环这种基本的程序结构,所以,数据库厂商几乎都对 SQL 标准进行了扩展,像 PL/SQL 和 T-SQL 中都提供了这些语法,进而催生出函数、触发器、存储过程一系列“万恶之源”,可从编程语言的角度来看,SQL 算是个好 DSL 吗?

SQL 试图从编程语言中获得“灵感”的思路是正确的,但总给人一种买椟还珠的感觉,譬如使用大量的英文关键字来作为保留关键字,可你很难想象,像 GROUP BY 和 ORDER BY 这样的关键字,居然可以保留中间一个甚至多个空格,既然是关键字,为什么不选择一个单词,而选择一个组合词呢?这个世界上用 Begin 和 End 的编程语言,我使用过的有 Pascal 和 Basic,但现在我几乎不会再用它们,为什么呢?因为使用花括号({})更符合这个世界的发展趋势,你看 Python 居然用缩进代替花括号,是打算时刻用游标卡尺写代码吗?

全世界都默认用分号作为一个语句的结束,那么,当多个语句放在一起的时候,直接相互间用分号隔开,编译器或者解释器都能识别,就算不喜欢写分号的 JavaScript,最新的标准提案里不还是建议要写吗?可为什么到了 PL/SQL 这里,明明已经用分号作为结束符了,偏偏还要再用一个/作为分隔符。我们都知道/会被当做是注释的开始,那么如果我在 PL/SQL 里恰好在 End;后写上一句/,你告诉我,这到底代表什么意思?明明像&&、||、^等这样的运算符,都是有固定含义,并且大家所有编程需要都默认了这个原则,可偏偏有人用||来连接字符串,你告诉我,用+不好吗?就像从小到大,÷都会被认为表示一个除法运算,结果突然有一天,有人用这个符号来表示加法运算,你说你是不是有种被当做傻子的感觉。全世界都用=表示赋值运算,结果 PL/SQL 自作聪明地搞了个:=,我想说,你真的考虑过使用者的体验吗?

你甚至连分页、排序、分组这种事情,都无法在不同的数据库上获得一致的书写体验,读取指定数目的数据库记录,居然要纠结用到底用 Limit 还是 Top,像 Select Into 这样把指定列存储到指定变量中的操作,居然要求使用者来限制结果集的数目,从函数的角度来看,返回的必然是结果集中的一个元素,只有这样才可以赋值给指定的变量,可问题是存在多条记录的时候,你必须用游标去循环读取,而不能像大多数编程语言一样,直接 Map()到一个类型上然后 ToList(),可能是我对 SQL 的要求太高了吧,毕竟它就是个面向过程的语言,OO 不 OO 的没那么重要,可明明你可以抛出异常啊,可以对字符串做截取啊正则啊,可以在控制台里输出日志啊,可以调用各种有的没的的内部函数啊,elsif 可能是因为 e 不发音,就像 usr 绝对不是拼写错误……

Python 的缩进虽然为人所不齿,但它至少和大部分编程语言一样,单独一行的程序语句和由多行程序组成的程序块之间,并不需要明显的分割符号。可 MySQL 需要用 DELIMITER $$这种奇怪的符号,PL/SQL 需要用/这种奇怪的符号,SQLServer 需要用@这种奇怪的符号,还有大名鼎鼎的虚拟表 DUAL。也许这些东西写多了就可以记住,就像我现在可以分清 SYSDATE、NOW()和 GETDATE(),可它带来的问题是什么呢,大多数的触发器、存储过程、函数都是没有移植性可言的,很多年前,我们讲设计模式,最喜欢觉的例子就是,如果项目发生变动,需要更换数据库,我们要怎么设计能不改动代码,现在看起来,当时还是太天真了,真要换了数据库,估计就是重新做了,敢把全部业务写到数据库里,Web 就做一个展示层的项目,有生之年应该是不会换数据库啦!

多元与统一

这个世界的离奇之处在于,人们一边渴望在标准的庇护下幸福生活,又一边渴望可以超脱标准去发展独立的个性,如你我所见,多元与统一,构成了这个世界永恒的旋律,或许是因为那句名言——没有永远的敌人,只有永远的利益。可对比 Web 的标准化与 SQL 的标准化,我们却看到了截然不同的场景,虽然 Chrome 浏览器市场份额的不断提高,加上微软、Mozilla 等“浏览器巨头”一起推动,HTML5 和 CSS4,让大量的工作得到了简化,尤其像 WebSocket、Drag&drop、Canvas 等 API 的推出,这带来的好处是什么呢?大家不再去重点关注浏览器的兼容性问题,各种天花乱坠的炫酷特效不再通过 JavaScript 去控制。一个标准的 API + 一个支持降级的 profily,基本就可以覆盖到主流的浏览器,就算有小程序这种偏离标准的解决方案,回顾近几年整个前端领域的趋势,可以说,一切都在向着好的方向发展。

可数据库领域发生了什么,依稀记得甲骨文和 Google 因为 Android 使用了 Java 而官司连连,Google 不得不推出一种新的基于 JVM 的语言——Kotlin;依稀记得甲骨文在开源社区的强烈反对下收购了 MySQL,社区不得不继续维护 MySQL 的开源分支——MariaDB。从这两件事情,我完全提不起对甲骨文这家公司的好感,虽然大家都说 Oracle 品质卓越,可实际使用下来,经常出问题的 Oracle。从 LAMP 时代开始,MySQL 就以其免费、轻量的特点广泛应用在互联网产品中,直至今天有大量的云产品使用着 MySQL,而 Oracle 和 SQLServer 则被更多地使用在私有部署的场景中。虽然,我承认把数据掌握在自己手里会放心些,可当你没有能力去维护这些东西时,付出的时间和精力远远要比这多。甲骨文收购了那么多公司的产品,时至今日,对整个行业的标准化有什么推动呢?Oracle 数据库依然难装、难用,PL/SQL 同样难用得要命,可我们这世界一直都很奇怪,最流行的偏偏未必是最好的,据说 Oracle 的代码写得非常差,开发人员表示不会在为它继续开发新功能。

可能有时候,我们完全说不出来,一件东西是好还是坏,就像 JavaScript 能在前端开发流行,是因为没有其它的选择,你说这门语言没有缺点吗?当然有,JavaScript 里各种“骚操作”和“黑科技”,甚至吐槽三天三夜都说不完。同样,还有 Python 这门语言,大家都觉得它的解释器慢腾腾的,动态语言遇上大型项目简直就是火葬场,还有神来之笔—— 通过缩进来代替花括号。我最终还是在 PL/SQL 里执行了我的脚本,只要我在使用 DBeaver 的时候,人肉地区分/前后的 SQL 语句就可以了。果然,我骨子里还是一个不喜欢写 SQL 脚本的人,因为我认为这么别扭的东西简直不能称之为脚本,你看看 Lua,再看看 Python,有哪一门脚本语言有 SQL 脚本这样别扭呢?数据库对我而言,就是一个存取数据的“潘多拉魔盒”,索引啊,触发器啊,数据库任务啊,执行计划啊,存储过程啊,难道不属于暴露了太多细节给用户吗?我天天用这个数据库,我每天用哪些表,我每天用哪些字段,你作为一个成熟的数据库了,居然不能自己去解决这些问题,我对你很失望啊,请记住,程序员比任何人都喜欢偷懒。