PLSQL最佳实践

  • 存在性检查
--正确姿势:
SELECT COUNT(*) FROM dual WHERE EXISTS (SELECT 1 FROM yxxc_gzb);
--不正确姿势:
SELECT COUNT(*) FROM yxxc_gzb;
  • 提防 DDL 提交事务
DDL 语句的第一步就是 COMMIT,然后才是执行 DDL 本身,无任命令是否执行成功,
都已经提交。所以不要在事务中使用 DDL 语句
  • 减少对 sysdate 的调用
sysdate 函数在经常会被使用,但它是函数,一定要注意将 sysdate 移出循环,先赋予
变量,然后在循环中引用变量。
  • 把静态表达式迁出循环和 SQL 语句
PROCEDURE show_customers(prefix_in IN VARCHAR2, state_in IN VARCHAR2) IS 
c_state CONSTANT mfe_customers.state%TYPE := upper(state_in);
c_output_prefix CONSTANT VARCHAR2(32767) := to_char(SYSDATE, 'Mon DD, YYYY') || ' ' ||
                                            upper(prefix_in) || ' ' ||
                                            c_state;
CURSOR customers_cur IS
  SELECT last_name, first_name, city
    FROM mfe_customers
   WHERE state = c_state;
BEGIN
  FOR customer_rec IN customers_cur LOOP
    dbms_output.put_line(c_output_prefix || ' ' || customer_rec.first_name || ' ' ||
                         customer_rec.last_name);
  END LOOP;
END show_customers;
  • 与NULL值进行比较或逻辑运算的时候千万不要使用“=”,“<>”操作符,要用IS NULL操作符
    (NULL值并不等于其它的所有值,甚至不等于另外的一个NULL值,程序代码应该显式的处理NULL值)
--假设业务:两个为空的字符相等
DECLARE
  v_first_name  VARCHAR2(100);
  v_second_name VARCHAR2(100);
BEGIN
  v_first_name  := '';
  v_second_name := '';
  --不正确姿势
  IF v_first_name = v_second_name THEN
    dbms_output.put_line('v_first_name和v_second_name相等');
  ELSE
    dbms_output.put_line('v_first_name和v_second_name不相等');
  END IF;
  --正确姿势
  IF v_first_name = v_second_name THEN
    dbms_output.put_line('v_first_name和v_second_name相等');
  ELSIF v_first_name IS NULL AND v_second_name IS NULL THEN
    dbms_output.put_line('v_first_name和v_second_name相等');
  ELSE
    dbms_output.put_line('v_first_name和v_second_name不相等');
  END IF;
END;
--IF模版
DECLARE
 v_action VARCHAR2(100);
BEGIN
 IF v_action = 'ADD' THEN
   --增加操作
 ELSIF v_action = 'UPDATE' THEN
   --修改操作
 ELSIF v_action IS NULL THEN
   --处理NULL值
 ELSE 
   --处理其它情况
 ENF IF;
END;
  • 避免在声明部分指定默认值,特别这些值是函数调用返回的值
块的异常部分只会处理块的执行部分抛出的异常,假如声明块中抛出异常,是不会被异常处理部分捕获的,而会往调用方抛出。
  • 清理PLSQL块中的数据
比如:动态游标使用完记得关闭,很多情况是出现异常没关闭。
  • 最好不要在包说明中定义变量,每个对包有执行权限的人都可以看到甚至修改
如果有需要,建议在包体定义变量,并且通过set/get控制对包数据的访问(意味着在过程或函数中,不要直接引用变量,而是通过get方法)
  • 在复杂的IF ELSIF ELSE 语句中,可以建议使用case语句来替代(不要忘记else语句),增强可读性。

  • PL/SQL的异常处理机制

image
  • 避免在异常部分中嵌入应用程序逻辑
  • 避免通过错误编码引用异常,若Oracle没给该异常命名,自己给它命一个
    • 前:


      image
    • 后:


      image
  • PL/SQL查询
    • 把查询的值写入记录中,即使用%rowtype
    • 只有当需要检索多行数据时,才使用游标for循环
  • PL/SQL修改
    • 要始终显式列出INSERT语句中所使用的数据库表的列
    • SQL%属性总是会话中最近执行的隐式游标
注意:当更新或者删除那些“本应该”存在的数据时,要检查SQL%ROWCOUNT属性,以验证所完成动作的正确性。

SQL%ROWCOUNT:受DML语句影响的数据行的数量
SQL%FOUND:该语句影响至少一行数据时,返回TRUE
SQL%NOTFOUND:该语句影响至少一行数据时,返回FALSE
  • 动态SQL

    • 把动态SQL字符串赋给一个变量,需要时尽可能用占位符,即绑定变量
    • 避免SQL串联,防止SQL注入
    • 对于存在动态SQL语句的程序(模式级别或者包内过程)来说,最好使用AUTHID CURRENT_USER定义为调用者模式,可最大程度降低风险
  • 函数的要点就是能够返回一个值(标量、集合、记录),如果通过使用OUT、IN OUT参数列表返回值的话,则函数的功能用途显得不明显了,此时可以考虑使用过程。

如果需要同时返回多项信息,可采用如下方式:
1、返回一个记录或集合
2、将函数变成过程
3、将独立的函数分解成复合函数,并全部返回标量值??
  • 对于子程序而言,参数列表应该明确地描述程序实现其功能时需要的参数,以及有可能返回的值。
当在一个主过程包含子过程或子函数时,如果子过程等需要的参数包括主过程传递进来的某些参数时,
最好在子过程的参数中也进行定义下,而不要说直接拿主过程的参数,
之前写代码时一直在考虑是否要定义形参,犹豫不决,导致有些子过程有定义,有些则没有,不太规范。
  • 如果需要往已有的程序增加参数时,要考虑原有调用程序能够不受影响,可使用以下方式:
1、确保新增的IN参数都有默认值,当然,如果参数是OUT或IN OUY的话,则不行
2、增加重载函数/过程,这时要注意,千万不要将原有函数/过程的代码COPY过来,而应创建另一个“核心”程序供其调用
  • 当一个过程实现逻辑比较复杂时,最好对其进行功能拆分(逐步求精法、自顶向下设计法),使用本地子程序,使主程序执行部分可进行调用,使得逻辑更清晰,把实现细节进行封装。
写代码时,可以先进行程序接口设计,将实现逻辑进行拆分,跟JAVA的接口设计类似,
最后再对其具体实现详细编码,让编码者或者他人看到程序执行部分时,能够很好的理解其实现意图
  • 尽可能把更多的业务规则封装、隐藏在函数中,供其它过程调用。
所有软件项目的一个特性是:永远不会变化的是事物一直在变化,如业务需求、数据结构或用户界面等等。
我们最好能够编写出能够容易适应这些变化的代码。
  • 不要在函数中出现多个出口,遵循“一个入口只能有一个出口”,另外,在异常处理部分也要保留RETURN语句。
  • 返回布尔值的函数绝对不能返回NULL(确保返回TRUE或者FALSE),这点在进行条件判断的时候尤其要注意。
-- 是否包含指定数字(6或者8)

--不好的函数:
CREATE OR REPLACE FUNCTION is_contains_special_number(i_str IN VARCHAR2)
  RETURN BOOLEAN IS
BEGIN
  RETURN instr(i_str, '6') > 0 OR instr(i_str, '6') > 0;
END is_contains_special_number;
--当传入NULL时,输出contains special number,明显有问题
BEGIN
  IF NOT is_contains_special_number('') THEN
    dbms_output.put_line('not contains special number');
  ELSE
    dbms_output.put_line('contains special number');
  END IF;
END;
--姿势换一下,调用方作处理,但治标不治本,不能确保所有调用方都这么处理:
BEGIN
  IF NOT NVL(is_contains_special_number(''),FALSE) THEN
    dbms_output.put_line('not contains special number');
  ELSE
    dbms_output.put_line('contains special number');
  END IF;
END;
--修改函数(确保返回TRUE或者FALSE):
CREATE OR REPLACE FUNCTION is_contains_special_number(i_str IN VARCHAR2)
  RETURN BOOLEAN IS
  v_result BOOLEAN;
BEGIN
  IF instr(i_str, '6') > 0 OR instr(i_str, '6') > 0 THEN
    v_result:=TRUE;
  ELSE --当i_str为空时,也返回FALSE
    v_result:=FALSE;
  END IF;
  RETURN v_result;
END is_contains_special_number;
  • 包的最佳实践:

    • 使用程序包组合功能相关的程序,避免编写模式别的过程或函数
    总是从程序包开始,即时此时包中仅仅只有一个程序,将来也很有可能会有很多程序
    
    • 让程序包的关注面范围小而窄
  • 尽可能使用bull colect和forall进行批量提取数据,减少上下文切换

    • 关于bull colect的建议

      • 更多内存,空间换时间
      注意:
      PL/SQL集合消耗的内存来自于PGA,而不是SGA,即内存消耗是以每个会话为基础的。
      假如程序中bull collect需要耗费5M内存,而并发连接有1000个,则你的应用程序需要耗费5G内存,这不容忽视。
      
      • LIMIT子句
      当使用bull colect时,应该要同时使用LIMIT子句,生产环境的数据量可能会大幅度增长。
      可尝试不同的LIMIT值,以平衡优化的性能和内存消耗的关系。
      
      • 没有游标属性
      当与显式游标和循环一起使用LIMIT子句时,不要使用SQL%NOTFOUND和NO_DATA_FOUND等来终止循环。
      而应该要检查bull colect填充的集合,假如没有数据返回了,集合数量为0。
      
      • 非字符串索引的集合,集合索引值始终从1开始
    • 关于forall的建议

      • 异常,考虑添加save exception
      默认情况下,当循环中红的DML语句出现问题时导致SQL引擎抛出异常时,
      会把这个异常传回PL/SQL引擎,此时会终止程序,
      可给forall头部增加save exception子句,保存执行过程中抛出的异常,并在结束时返回所有的异常。
      
      • 每个forall语句中,只可以执行一条DML语句
  • 缓存实践(尽可能在最快的内存位置,缓存静态数据)

    备注:
    SGA:数据库实例保留一份,所有会话均可使用
    PGA:每个会话保留一份
    
    • 包缓存,保存在PGA,消耗内存大
    1、一般后台应用程序会使用连接池技术,需要确保的是:缓冲池中每个连接都包含相同的被缓冲数据,
    避免前端应用程序在不同的会话间切换时,操作不同的数据集。
    
    2、应该在包体内部声明数据结构(私有,只能在包内使用),以便于管理其内容,保证完整性。
    
    CREATE OR REPLACE PACKAGE BODY emplu2
    IS
       TYPE tab_tabtype IS TABLE OF employee%ROWTYPE INDEX BY BINARY_INTEGER;
       loadtab tab_tabtype;
    
       FUNCTION onerow (
          employee_id_in IN employee.employee_id%TYPE
          )
       RETURN employee%ROWTYPE
       IS
       BEGIN
          RETURN loadtab (employee_id_in);
       EXCEPTION
          WHEN NO_DATA_FOUND
          THEN
             RETURN emplu1.onerow (employee_id_in);
       END;
    
    BEGIN
       FOR rec IN (SELECT * FROM employee)
       LOOP
          loadtab (rec.employee_id) := rec;
       END LOOP;
    END;
    
    • 结果缓存,保存在SGA,消耗内存小
  • 管道函数

  • 当集合和记录是OUT或IN OUT参数时,可以考虑使用NOCOPY降低负载

函数或过程参数模式:
1、IN:读(传引用)
2、OUT:写,初始值默认null(传值)
3、IN OUT:读写(传值)

传值:复制进,复制出。参数的值会被复制到应用程序的数据结构,
传引用:程序中参数变量指向值所在内存区域。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容