抱歉用了这么渣的标题,其实是一个很简单而且很常见的需求:假设我们有一个学生表,它有一个状态字段:

  1. create table T_STU
  2. (
  3. STU_ID VARCHAR2(36) not null,
  4. NAME VARCHAR2(255),
  5. CODE VARCHAR2(255),
  6. STATE NUMBER(10),
  7. START_YEAR NUMBER(10)
  8. );
  9. alter table T_STU2 add constraint PK_STU2 primary key (STU_ID);
  10. create index IX_STU21 on T_STU2 (STATE);

由一个数字代表学生的各种状态,例如 1 表示“在校”,2 表示“休学”,3 表示“肄业”,5表示“毕业”。
现在想要创建一个查询学生表的存储过程,我们希望它能灵活点儿,可以查询出某几个状态下的学生。例如,假设存储过程叫做 SP_QUERY_STU_BY_STATES,
SP_QUERY_STU_BY_STATES('2,3')
将获得所有休学和肄业的学生。我们从最简单的方法开始,寻求一种相对较好的解决方案。

【方法1】将参数直接放在 in 表达式里面(不可用)

  1. create or replace procedure SP_QUERY_STU_BY_STATES1(
  2. cur_OUT out SYS_REFCURSOR,
  3. p_states varchar2
  4. ) is
  5. begin
  6. open cur_OUT for
  7. select t.* from t_stu t
  8. where t.state in (p_states);
  9. end SP_QUERY_STU_BY_STATES1;

调用 SP_QUERY_STU_BY_STATES1('2') 没有问题,但是调用 SP_QUERY_STU_BY_STATES1('2,3')  会报“ORA-01722:无效数字”的错误。因为 Oracle 不能把字符串直接转换为列表。

【方法2】使用动态SQL语句(可用,但是有许多缺点)

将 p_states 作为SQL语句的一部分拼接起来之后再执行,就不会报 ORA-01722 错误了。

  1. create or replace procedure SP_QUERY_STU_BY_STATES2(
  2. cur_OUT out SYS_REFCURSOR,
  3. p_states varchar2
  4. )
  5. is
  6. query_string VARCHAR2(4000);
  7. begin
  8.  
  9. query_string := 'select /*+RULE*/ t.* from t_stu t
  10. where t.state in (' || p_states || ')';
  11.  
  12. dbms_output.put_line(query_string); -- for debug
  13.  
  14. open cur_OUT for query_string;
  15.  
  16. end SP_QUERY_STU_BY_STATES2;

注意这里使用增加 /*+RULE*/ 标记的方式强制使用RBO优化器,可以让Oracle利用STATE字段上的索引而提高效率。实测查询状态为2,3的学生(从3,000,000条里取出154条数据),不加 /*+RULE*/ 标记时CBO会使用全表扫描,耗时2.3秒(即使刚刚做完表分析也要耗时0.6秒);加上 /*+RULE*/ 标记利用STATE字段上的索引,耗时0.172秒。所以后面的所有查询都会加上/*+RULE*/ 标记。

虽然方法2可以得到正确的结果,但是它有好几个让人抓狂的缺点。
1. 动态SQL的效率要比静态SQL稍低。
2. 可读性差。SQL语句本身可读性就不好。想象一下,读一个上百行的复杂查询本身就很让人头大了,如果里面还有大量判断语句和字符串拼接,整个代码会丑得让人想吐。
3. 语法错误要到运行时才会暴露出来。
4. 想看执行计划挺不方便的,要使用 dbms_output.put_line() 把生成的SQL输出才能查看执行计划。

总之,动态SQL是非常灵活同时又是非常恶心的方法。记得有一天吃完午饭,突然感觉实在受不了动态SQL了,就憋了半小时,想到了下面这个方法。

【方法3】使用反着的LIKE语句(可用,但效率低下)

  1. create or replace procedure SP_QUERY_STU_BY_STATES3(
  2. cur_OUT out SYS_REFCURSOR,
  3. p_states varchar2
  4. ) is
  5. begin
  6. open cur_OUT for
  7. select /*+RULE*/ * from t_stu t
  8. where ',' || p_states || ',' like '%,' || t.state || ',%';
  9.  
  10. end SP_QUERY_STU_BY_STATES3;

上面这段代码可以这么理解:假设 p_states 参数为 '2,3',对于 STATE 字段为 2 或 3 的数据, '2,3' like '%2%' 和 '2,3' like '%3%' 都会被判定为真;对于STATE字段为5的数据,'2,3' like '%5%' 会被判定为假,这样自然就筛选出了状态为2和3的数据。之所以拼接了许多 “,” ,是因为如果有一个状态是12的话,'12,3' like '%2%'也会被判定为真,这样就错误地把不需要的数据也包含进来了。

这个方法除了写法有些奇怪之外,最大的缺点是性能很差——这种写法会造成全表扫描,类型转换和字符串匹配也要耗费不少的时间。实测获取状态为2,3的数据耗时2.3秒。

【方法4】将字符串转换为数组(可用,而且性能好)

先来看一下最终结果

  1. create or replace procedure SP_QUERY_STU_BY_STATES4(
  2. cur_OUT out SYS_REFCURSOR,
  3. p_states varchar2
  4. ) is
  5. begin
  6. open cur_OUT for
  7. select /*+RULE*/ * from t_stu t
  8. where t.state in (select column_value from TABLE(f_cstr_to_list(p_states)));
  9. end SP_QUERY_STU_BY_STATES4;

这种方法可以利用STATE上的索引,性能很好。实测获取2,3状态的数据耗时0.171秒。

这个方法的重点在于如何把逗号分隔的字符串状态列表转换为数组。首先,需要定义一个内容为字符串的数组类型 t_strlist

  1. CREATE OR REPLACE Type t_strlist as Table of Varchar2(4000);

由于Oracle没有分割字符串的 split 函数,下面这个将逗号分隔的字符串转换为数组的函数稍稍有些杂乱,我尽量写得可读性好一点,相信并不难看懂。

  1. CREATE OR REPLACE Function f_cstr_to_list
  2. -- 将逗号分隔的字符串分解为列表
  3. (
  4. cstr In Varchar2
  5. )
  6. Return t_strlist
  7. is
  8. v_start number := 1; -- 迭代搜索开始位置
  9. v_i number := 1; -- 迭代次数
  10. v_position number := 0; -- 每次迭代找到的逗号字符的位置
  11. v_str varchar2(4000); -- 源字符串
  12. Result t_strlist;
  13. Begin
  14. Result := t_strlist();
  15. v_str := cstr || ',';
  16.  
  17. loop
  18. v_position := instr(v_str, ',', 1, v_i);
  19. if(v_position > 0) then
  20. Result.EXTEND;
  21. Result(v_i) := substr(v_str, v_start, v_position-v_start);
  22.  
  23. v_start := v_position + 1;
  24. v_i := v_i + 1;
  25. else
  26. exit;
  27. end if;
  28. end loop;
  29.  
  30. return Result;
  31. End;

这种方法除了需要自己写一个自定义函数有些麻烦(当然只需麻烦一次),而且需要冒函数写得不对而引发BUG的风险之外,可以说是非常完美。当然,实战中往往还需要按姓名等字段进行模糊查询,这时的效率如何呢?我们来试试下面这个更为实用一点的存储过程。

  1. create or replace procedure SP_QUERY_STU_BY_NAME_STATES(
  2. cur_OUT out SYS_REFCURSOR,
  3. p_name varchar2,
  4. p_states varchar2
  5. ) is
  6. begin
  7. open cur_OUT for
  8. select /*+RULE*/ * from t_stu t
  9. where t.name like '%' || p_name || '%'
  10. and t.state in (select column_value from TABLE(f_cstr_to_list(p_states)));
  11. end SP_QUERY_STU_BY_NAME_STATES;

看一下执行计划:


Oracle会先使用STATE上的索引将检索范围缩小然后再模糊匹配,效率自然会比较高。实测从3,000,000条数据里获取77条耗时0.11秒。

[Oracle](不会的是三炮)把状态列表作为存储过程参数这件小事的更多相关文章

  1. oracle 之 内存—鞭辟近里(三)

    oracle 之 内存—鞭辟近里(三) 今天是2013-07-08,今天晚上突然接到一个电话,我的外甥问我的qq是多少,我感觉很吃惊,他长大了.在他现在这个年龄就开始接触网络,我难免有少许担心,希望他 ...

  2. Oracle 数据库分页查询的三种方法

    一.Oracle 数据库分页查询的三种方法 1.简介 不能对 rownum 使用 >(大于或等于 1 的数值).>=(大于 1 的数值).=(不等于 1 的数值),否则无结果.所以直接用 ...

  3. 【书评:Oracle查询优化改写】第三章

    [书评:Oracle查询优化改写]第三章 BLOG文档结构图       导读 各位技术爱好者,看完本文后,你可以掌握如下的技能,也可以学到一些其它你所不知道的知识,~O(∩_∩)O~: ① 隐含参数 ...

  4. python函数,lambda表达式,三目运算,列表解析,递归

    一.自定义函数 定义函数时,函数体不执行:只有在调用函数时,函数体才执行.函数的结构: 1. def 2. 函数名 3. 函数体 def func_name(): 函数体 4. 返回值 如果没有声明返 ...

  5. [转]session缓存机制和三种对象状态

    摘自 http://blog.csdn.net/csh624366188/article/details/7612142 Hibernate 的Session就是其中的一个,它提供了基本的增,删,改, ...

  6. 第三章——使用系统函数、存储过程和DBCC SQLPERF命令来监控SQLServer(3)

    原文:第三章--使用系统函数.存储过程和DBCC SQLPERF命令来监控SQLServer(3) 本文为这个系列最后一篇.将是如何使用DBCC命令来监控SQLServer日志空间的使用情况. 前言: ...

  7. Hibernate中的三种数据状态

    Hibernate中的三种数据状态(临时.持久.游离) 1.临时态(瞬时态) 不存在于session中,也不存在于数据库中的数据,被称为临时态. 比如:刚刚使用new关键字创建出的对象. 2.持久态 ...

  8. 浅谈Hibernate中的三种数据状态

    Hibernate中的三种数据状态:临时.持久.游离 1.临时态(瞬时态) 不存在于session中,也不存在于数据库中的数据,被称为临时态. 数据库中没有数据与之对应,超过作用域会被JVM垃圾回收器 ...

  9. 命令stat anaconda-ks.cfg会显示出文件的三种时间状态(已加粗):Access、Modify、Change。这三种时间的区别将在下面的touch命令中详细详解:

    7.stat命令 stat命令用于查看文件的具体存储信息和时间等信息,格式为"stat 文件名称". stat命令可以用于查看文件的存储信息和时间等信息,命令stat anacon ...

随机推荐

  1. 模板类 error LNK2019: 无法解析的外部符号

    如果将类模板的声明和实现写在两个独立的文件中,在构建时会出现"error LNK2019: 无法解析的外部符号 "的错误. 解决方法有: 第一种方法,就是把类模板中成员函数的声明和 ...

  2. gcc -Wall -pedantic -ansi(转载)

    转载自R-G-Y-CQ的新浪博客 -Wall显示所有的警告信息 -Wall选项可以打开所有类型的语法警告,以便于确定程序源代码是否是正确的,并且尽可能实现可移植性. 对Linux开发人员来讲,GCC给 ...

  3. SVN

    你先在本地把包删了,然后提交上级目录就可以了.比如你要删除com.aaa.A这个类.你就把A删了,然后提交aaa就行,如果你要把aaa包删了,你就删了以后提交com这个包.SVN就删掉了 svn提交更 ...

  4. PDA手持机 移动开单进销存系统 现场出打印凭据和扫码 新的亮点

    传统车销模式弊端:1.手写开单,效率低,动作慢2.现场手写开单明细不能打印,产品明细不规范3.电脑办公人员及车销人员对车上的库存情况掌握不清楚,销售人员对每种产品销售价格不清楚4.老板对员工工作的管控 ...

  5. PHP 显示文章发布日期 一小时前 一天前 一月前 一年前

    <?PHP /*** 传入日期格式或时间戳格式时间,返回与当前时间的差距,如1分钟前,2小时前,5月前,3年前等* @param string or int $date 分两种日期格式" ...

  6. 工作记录 java

    1:tomcat有两个这个文件,是缓存文件和临时文件.可能会缓存部分的jsp文件,所以如果有时候文件重新替换不起效果的话,可以先把这两个文件夹下面的文件清空

  7. 基于dubbo构建分布式项目与服务模块

      关于分布式服务架构的背景和需求可查阅http://dubbo.io/.不同于传统的单工程项目,本文主要学习如何通过maven和dubbo将构建分布项目以及服务模块,下面直接开始. 创建项目以及模块 ...

  8. 从零点壹开始学JAVA(DAY 1 笔记)<补充记录>

    一个同事的先生(下面称呼其为耿大神)是做开发的,比较资深的那种,在他们公司的核心技术部门. 幸运的是,耿大神很乐意帮助我这株小白菜,在业余时间指导我,这里深表感谢. 耿大神隔段时间会给我提一些问题,让 ...

  9. 《程序员的自我修养》读书笔记 - dllimport

    Math.c  { __declspec (dllexport)  double Add (xx, xx) {...}} MathApp.c  { __declspec(dllimport) doub ...

  10. 在CentOS 7中安装与配置JDK8

    参考命令:http://www.jb51.net/os/RedHat/73016.html来进行安装 安装说明 系统环境:centos7 安装方式:rpm安装 软件:jdk-8u25-linux-x6 ...