因为工作中需要同时面向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脚本这样别扭呢?数据库对我而言,就是一个存取数据的“潘多拉魔盒”,索引啊,触发器啊,数据库任务啊,执行计划啊,存储过程啊,难道不属于暴露了太多细节给用户吗?我天天用这个数据库,我每天用哪些表,我每天用哪些字段,你作为一个成熟的数据库了,居然不能自己去解决这些问题,我对你很失望啊,请记住,程序员比任何人都喜欢偷懒。