如何高效使用YashanDB PL语言?这5点建议值得收藏

01 背景

PL语言(Procedural Language) 是一种程序语言, 又称过程化结构查询语言,它是一种建立在普通SQL语言之上的编程语言。

讨论PL语言是一个有意思的话题,因为软件行业被大数据、组件化的思想浪潮捶打过多年,所以多数从业人员一旦提及PL语言,都会在心里冒出一句疑问,“那不是已经过时的语言?”

那为什么YashanDB还要选择开发PL语言?正所谓仁者乐山,智者乐水,笔者的观点是:在技术领域,没有绝对的对错之分,关键在于是否合适。物竞天择,在软件行业,最合适应用业务实现的技术会用行动来投票决定。

如电信、证券、银行等传统行业,强调业务的高并发和高可用,通过PL语言来实现业务应用逻辑是主流选择。而在互联网行业,选择PL语言来实现业务逻辑的几乎绝迹,替代的是,业务广泛通过各种CRUD技术中间件 + JDBC,将业务的逻辑实现转移到数据库之外。

PL语言之所以成为很多业务的选择,在于其独特的特性价值:

  • PL语言具备高级语言的可编程属性,支持面向过程、面向对象的编程实现,具备业务逻辑直接在数据库中实现的条件;

  • 区别于不同组件间交互方案,大大减少了网络传输开销明显降低业务功能端到端实现复杂度,提升业务稳定性和可靠性;

  • 高度集成SQL,在数据库批量SQL操作下具有明显的性能优势;

  • Oracle兼容性重要组成,在传统行业大量存量业务的迁移时,PL语言的兼容性成为降低成本的关键因素。

因此笔者认为除非发生巨大的技术变更,PL语言特性将在相当长的一段时间内会持续保持很强的竞争力。

在之前一篇论述Oracle兼容性的文章中,笔者以整个数据库视角来论述过国产数据库在这个方面应该达到的4个标准。_(可点击此处查看文章:__《论Oracle兼容性,我们需要做什么?》)_当这个范围缩小到PL语言时,Oracle兼容性的视角该如何呈现呢?

image

Oracle兼容性是目前国产数据库最主要的一个工作任务,这个任务直接决定了数据库在做商业迁移时的成本,是商业竞争力的一个重要指标。类比于SQL语言,在PL语言视角上也做了四层划分。

  • 第一层要求是做到PL语言的语法完全兼容,即Oracle实现的PL语言元素,如变量定义、函数定义、循环、控制、SQL调用、函数调用、异常语句等,从语法格式上完全对应;

  • 第二层要求是做到PL语言的语义完全一致,即各类语法承载的语义要求,还有块、数据区、语句区、子过程、异常处理等,从语义实现上完全对应;

  • 第三层要求是做到PL语言的高级特性相同,包括承载PL语言的对象,包括匿名块、过程、函数、触发器、高级包等在数据字典管理、触发机制、工作原理上全面兼容;同时提供好主流的Oracle系统自带的高级包功能,避免存量业务代码的修改;

  • 第四层要求是做到PL语言的生态支持,如PL语言要具备易用的调试工具、承载安全特性的PL语言加密工具等,实现生态上的完整。

Oracle兼容性不是一个简单的模仿行为,而是一个非常复杂和工程量庞大的逆向工程。目前国产数据库应该秉持务实的态度,专注于核心功能的实现和完善,同时确保高度的兼容和已实现功能的稳定性。

YashanDB作为一款全自研的数据库,从Oracle兼容性角度上技术具备一定优势。经过几年不断的打磨,在PL语言特性上已初露头角。目前已推出的YashanDB数据库版本PL特性具备以下优点:

  • 高度SQL集成;

  • 完整的可编程逻辑;

  • 高性能;

  • 便捷的可调试性。

以下将围绕上述优点,展开描述YashanDB PL语言实现范围。

02 YashanDB PL语言优点

高度SQL集成

image

上图为YashanDB PL语言特性实现架构。图示可见,PL引擎与SQL引擎在层次上是完全解耦的,通过SQL引擎绑定参数特性完成SQL语句的编译和执行,SQL产生结果集通过sender接口输出。通过缓存机制,完成PL和SQL编译体的缓存,便于使用时可以快速执行。在此架构下,PL语言是可以完全发挥SQL引擎支持的所有能力。

PL语言与结构化查询语言SQL的结合非常紧密,具体表现在以下方面:

  • 允许静态SQL操作,即直接使用所有的DQL、DML数据操作,事务控制语句,语句中完全支持内置函数、高级包的子函数、运算符和伪列;

  • 允许通过动态SQL方式进行所有SQL操作。该特性主要是由静态SQL支持范围进一步放开了DDL语句特性;

  • 完全支持SQL中定义的所有数据类型,包括数值、字符串、RAW、BOOLEAN、大对象等数据类型;

  • 支持游标变量,提供了灵活的游标OPEN、FETCH、CLOSE、赋值、入参、出参、提前返回结果集等操作SQL的能力;

  • 支持%TYPE,%ROWTYPE等类型继承能力,而无需显式指定该数据类型

  • 运行DQL查询时普通游标将会一次处理查询结果集的一行,BULK游标可以支持一次处理一批

  • SQL操作产生异常时,均可以通过异常模块的编程进行捕获

完整的可编程逻辑

image

SQL语言是一种数据描述语言,PL语言则是极大的扩展了数据库的可编程逻辑。PL语言源于元老级的Ada语言,SQL/PSM为业界标准,Oracle的PL/SQL语法形式为当前事实标准。可以实现面向过程、面向对象两种编程形式。

  • 基础要素为语句块BLOCK,可以分为数据区和语句区两部分。多个语句块顺序或叠加,在运行时形成一种栈式的调用;

  • 数据区支持类型定义、变量定义、缺省表达式声明、异常变量定义、子过程定义等功能;

  • 除了支持SQL所有的数据类型,可以支持自定义类型,可定义数组、OBJECT、NEST TABLE等向量形式,类型可支持对应的方法。通过UDT可支持面向对象方法编程;

  • 语句区提供了循环、条件、跳转、SQL调用、函数调用、异常处理等可编程语言逻辑;

  • 在对象持久化层面提供了存储过程、自定义函数、触发器、自定义高级包、匿名块等多种数据库对象形态,提供了不同的触发时机和持久化机制,适用于不同的使用场景;

  • YashanDB PL语言完全遵循Oracle兼容性,以Oracle实现的PL特性移植修改代价小。

高性能

PL语言通过可编程逻辑和SQL集成,可以带来以下明显的好处:

  • 高效地数据批处理;

  • 显著降低客户端和数据库服务端的交互次数;

  • 减少网络流量损耗;

  • 数据库可实现的业务逻辑能力;

  • 提高业务处理的可靠性。

便捷的可调试性

使用PL语言程序很大阻力在难以调试PL语言程序。YashanDB支持断点、STEP INTO、STEP OUT、变量查看、调用栈查看、源码查看等功能。极大方便用户进行PL语言的跟踪定位。

与业界常见的调试器实现方案不同,YashanDB实现了一个轻量级的DEBUGGER,不需要组织不同的调试语句,不存在查看会话看板,不需要去ATTACH操作;一键启动调试,使用复杂度大大降低。

YashanDB PL语言的调试器特性,近期即将发布。

image

03 如何高效地使用YashanDB PL语言

笔者从基于PL语言开发者的角度,给出若干条建议如下:

  • 根据业务应用选择合适的PL对象,确保PL对象的规模适中;

  • 根据业务处理逻辑选择高效地语句;

  • SQL查询相关的PL特性选择;

  • 减少对象的级联调用,合适的使用递归或嵌套调用;

  • 减少在线DDL操作,避免失效。

以下根据提供建议,进行逐个展开。

建议1:根据业务应用选择合适的PL对象,确保PL对象规模适中

如下表,笔者给出PL语言对象推荐使用场景和高效使用建议,提供大家参考。

PL语言对象

使用场景

高效使用建议

匿名块

一般适用只需要临时使用的计算脚本,或者比较简单的调用存储过程、自定义函数等其他PL对象的入口。

  1.  匿名块不适合过长。因为不持久化,所以不适合复杂逻辑,需要用户自己保存代码。

  2. 简单的逻辑判定,过程/函数调用,业务计算场景。

存储过程

将复杂逻辑用过程进行封装,比如数据清洗过程,可以通过存储过程中检查表的不合规数据,然后进行清洗后处理。

  1. 遵循公共和抽象原则,相似的功能复用公共过程。每个过程都会占用数据库的资源。

  2. 过程代码不适宜过长,要遵循编码的逻辑分层思想。

  3. 过程的形参和调用实参类型建议一致,避免隐式转换带来的问题。

  4. 过程不会产生返回值,但可以通过出参携带需要返回信息。可以被其他PL对象调用,也可以递归调用。

自定义函数

体现一个用户自定义的函数功能,也可以通过C/JAVA的外置自定义函数去调用外置的LIB库,比如用户可以封装一个ARRAY_SPLIT函数,提供返回数组形式。

  1. 同存储过程一样,要确保函数的规模和公共度。

  2. 调用点除了可以在其他PL对象,还可以直接在SQL语句使用。但不同调用点要遵循对应的约束条件。

  3. 外置语言的LIB库需确保安全可靠,通过参数传递信息,尽量减少使用驱动再次连接数据库获取数据操作。

触发器

DML的语句级、行级触发时机,适用于数据检查、审计等场景。

  1. 触发器中过于复杂的逻辑会降低DML的性能。

  2. 要遵循触发器的使用约束。

  3. 不建议通过触发器直接修改DML要操作的数据,DBA维护。

自定义高级包

进一步体现封装的思路,一个高级包相当于一个完整namespace,可以有独立的变量,函数,过程,异常等命名。适用于业务模块级的封装。

  1. 优先使用系统自带的高级包和内置函数。

  2. 高级包不适合过多。使用高级包较多情况下,需要对应扩大缓存资源,避免内存不足。

建议2:根据业务处理逻辑选择简洁高效地语句

这个章节,笔者将通过举1个游标特性相关例子给大家一个直观的感受:

DECLARE  
  cursor cur1 is select column1 c1 from table1;
  result cur1%rowtype;
BEGIN
   open cur1;
   loop
       fetch cur1 into result;
       exit when cur1%notfound;
       process(result);
   end loop;
   close cur1;
END;
/

其实通过例子的分析,这个特性是完成游标的遍历,所以实际上选择FOR语句,可以更为简洁的完成相应功能。改写后例子如下:

BEGIN
   for result in (select column1 c1 from table1) then
       process(result); 
   end for;
END;
/

是不是整体简洁度高了很多?实际在PL语言中提供了很多逻辑行语句,语句间并不存在好坏,需要从业务逻辑角度选择合适的语句去实现。

建议3:SQL查询相关的PL特性选择

PL语言中常见的使用SQL的方式,有静态SQL特性、游标、动态SQL等。常见的业务逻辑是通过SQL获取数据后,需要进一步加工处理,然后返回处理后结果。

建议优先选择静态SQL特性,有以下原因:

  • 相对于动态SQL,PL编译器是感知静态SQL语句,有错误将在编译期就指出;

  • 静态SQL语句编译完成后,可以被PL编译体引用,执行阶段不需要触发编译,这样执行更为高效;

  • 静态SQL语句可以使用隐式游标属性来获取SQL执行状态。

image

其次推荐使用游标特性。游标是非常灵活的查询方式,而且存在多种游标形态,常用的为显式游标和系统游标。但相对于其他SQL查询特性,游标是需要变量形式承载,额外占用变量资源,同时遵循PL语言中变量的栈生命周期管理。

最后再是使用动态SQL特性。动态SQL适用于资源动态生成、动态拼接SQL语句和执行DDL语句,PL编译阶段难以检测的,需要到执行阶段进行编译执行,灵活度高但执行效率较低。

以上是三种常见的SQL查询相关PL特性比较,当然PL特性没有绝对的选择好坏,只有合适业务逻辑实现的才是最好的。

建议4:减少对象的级联调用,合适的使用递归或嵌套调用

合理规划的函数调用,可以减少编译复杂度。如下举例,给了一个较为复杂的嵌套调用,从调用关系上形成了一个有向环图。

CREATE FUNCTION F1 RETURN INT IS
  result INT := 1;
BEGIN
  result := F2();
  result := result + F3();
  result := result + F1();
  return result;
END;
/
 
CREATE FUNCTION F2 RETURN INT IS
  result INT;
BEGIN
  result := result + F4();
  return result;
END;
/
 
CREATE FUNCTION F3 RETURN INT IS
  result INT;
BEGIN
  result := result + F2();
  result := result + F4();
  return result;
END;
/
 
CREATE FUNCTION F4 RETURN INT IS
  result INT;
BEGIN
  result := result + F1();
  return result;
END;
/

一个有向环图按级联编译方式,通过深度遍历优先方式进行编译时,会优先按调用链寻找叶子节点,级联展开编译。

如果深度过深,会使得编译链过深,占用大量编译资源。如图所示,我们在进行级联编译时,针对递归、嵌套、相同编译流程多次重复调用的函数等各种情形,会进行检测并及时剪枝。

image

当然笔者认为函数调用不可避免会出现递归和嵌套调用的情形出现,所以选择如何在合适的时机选用递归和嵌套调用,这是编程关键。但不可以滥用,必须有合适的退出条件,避免对资源产生极大损耗。

建议5:减少在线DDL操作,避免失效

如果有一个数据库对象的编译使用了另一个对象的元数据信息,两个对象间就存在了依赖关系。如果被依赖对象发生了元数据信息一旦发生变更(元数据变更发生,即产生了DDL操作),依赖对象编译信息就失效了。

因为PL对象常见的实现逻辑,是封装大量的SQL调用,PL对象调用等,所以一个PL对象会产生大量的依赖对象。当依赖对象发生DDL,比如一个表动态增删了列,那么依据这个表的查询绑定的游标,其继承属性可能就会发生变化。再举一个例子,比如实现了一个自定义公共的字符串替换函数,当这个函数的实现发生变更,那么所有依赖这个公共函数的PL对象、SQL语句等都应该发生失效重编译的动作,否则原编译结构中包含的实现逻辑就是错误的。

在PL对象实现时,会根据PL对象的依赖关系构造依赖链。如果一个对象发生元数据变更,那么这个依赖链上所有的对象都会被病毒似的传染失效。如果有大量的对象失效,那么在调用时可能会产生大量的重编译动作,极端情况会导致资源的突发损耗,甚至耗尽报错,性能也会产生极大的波动。

所以建议一个PL对象的依赖对象适当要控制规模,而且通过预先执行DDL方式,确保缓存中编译体有效。如果必须有DDL操作,那么建议在DDL操作完成后,通过ALTER RECOMPILE的命令,将PL对象提前编译为有效状态。

此外在YashanDB PL语言实现过程中,我们发现这种问题,也及时做出了一些应对措施,比如通过松耦合操作,及时剪断病毒式的传染。

尽管如此,PL语言仍存在部分不足:

  • PL语言的编写质量看DBA能力,难以用质量手段衡量;

  • PL语言直接运行在数据库上,难以做好资源隔离,可能会影响主业务;

  • PL语言的安全、审计、运维等多个角度对DBA要求比较高;

  • PL语言在不同数据库间差异很大,难以移植。

如何有计划、有节奏地实现Oracle的PL语言特性,并能进一步克服PL语言的缺点,这是国产数据库在PL语言特性上面对的主要问题。比如第一点,PL语言是缺乏其他高级语言的UT测试框架、静态检查工具、内存工具等各种开发者生态工具,此外覆盖率报告、内存泄露检查等完全缺失。

YashanDB作为一款全自研的数据库,我们希望通过自身的努力,在不断追赶Oracle脚步上,可以青出于蓝胜于蓝,在已知这些缺点上做出自身的思考和努力,为用户提供更加优质、可靠的服务,展现我们的诚意与实力。

posted @ 2024-07-10 17:54  YashanDB  阅读(9)  评论(0编辑  收藏  举报