最近听了个讲座,其中介绍到了Oracle的谓词,原始版本的例子,如下所示,从数据上能看到,c1='3'的时候,c2的值是个字符串类型的数字,
SQL> create table test(c1 char(1), c2 varchar2(1));
Table created.
SQL> insert into test values('1', 'A');
1 row created.
SQL> insert into test values('2', 'A');
1 row created.
SQL> insert into test values('3', '3');
1 row created.
如下测试,在Oracle 11g执行,提示错误,认为to_number的变量存在非法的数值类型,
SQL> select to_number(c2) as value, c1 from test where c1='3' and to_number(c2)>2;
select to_number(c2) as value, c1 from test where c1='3' and to_number(c2)>2
*
ERROR at line 1:
ORA-01722: invalid number
之所以提示这个,我们能从执行计划中找到原因,可以看到,谓词条件先执行的是to_number(c2),
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 3 (100)| |
|* 1 | TABLE ACCESS FULL| TEST | 1 | 5 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter((TO_NUMBER("C2")>2 AND "C1"='3'))
因为我们知道,c1='1'、c1='2'的时候,c2是'A',to_number('A')就会提示invalid number,
SQL> select to_number('A') from dual;
select to_number('A') from dual
*
ERROR at line 1:
ORA-01722: invalid number
从这儿我们还可以知道一点,就是Oracle的where各条件的位置其实并不重要,准确来说,在CBO优化器模式下,究竟是c1='3' and to_number(c2)>2,还是to_number(c2)>2 and c1='3',都会经过Oracle的查询改写,给出真正执行的条件顺序,可以从10053找到他的查询改写。如上例子中,按照语义,就是先执行c1='3',按说他得到的c2就是数值类型的字符串,可以使用to_number函数,但实际上无论where中怎么写,都是按照to_number(c2)>2 and c1='3'来执行,因此报错。但是如果使用RBO优化器,where条件的顺序就可能影响执行计划。
说明了Oracle真正执行的语句是经过他的优化器调整的“他认为”的路径,但这个路径很可能导致语句执行错误,如上的示例中,如果先执行c1='3',再执行to_number(c2),应该就可以。
如果按照语义,改造一下,子查询中指定where c1='3',打算得到的c2都是数值的字符串类型,然后在外层where value(to_number(c2))>2,但是仍然报"ORA-01722: invalid number",
SQL> select * from (select to_number(c2) as value, c1 from test where c1='3') where value>2;
从执行计划,能看到谓词条件仍是如下这个,虽然to_number(c2)在外层,但实际执行时,Oracle通过谓词推入,将外层条件和内层条件进行了整合,
---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 3 (100)| |
|* 1 | TABLE ACCESS FULL| TEST | 1 | 5 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter((TO_NUMBER("C2")>2 AND "C1"='3'))
针对这个问题,可以改造如下,子查询中增加rownum>=1,避免谓词推入,
SQL> select * from (select to_number(c2) as value, c1 from test where c1='3' and rownum>=1) where value>2;
执行计划的谓词条件就改成了如下,并未做谓词推入,而是按照我们的“语义”选择的执行路径,这样就可以先找到c1='3'的记录,他的c2是'3',就可以正常用to_number()函数了,
-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 3 (100)| |
|* 1 | VIEW | | 1 | | 3 (0)| 00:00:01 |
|* 2 | COUNT | | | | | |
|* 3 | FILTER | | | | | |
|* 4 | TABLE ACCESS FULL | TEST | 1 | 5 | | 00:00:01 |
-------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - filter("VALUE">2)
3 - filter(ROWNUM>=1)
4 - filter("C1"='3')
另外一种方式,就是采用ordered_predicates这个HINT,CBO下强制按照where条件书写顺序来执行,
select /*+ ordered_predicates */ to_number(c2) as value, c1 from test where c1='3' and to_number(c2)>2;
TO_NUMBER(C2) C
------------- -
3 3
可以看到,谓词条件就改为,达到了我们需要让条件c1='3'先执行的目的,
1 - filter(("C1"='3' AND TO_NUMBER("C2")>2))
可能有细心的朋友发现,上述例子中c2的类型是char,如果是number或者varchar2,现象还相同么?
眼见为实,如果c2定义为number或者varchar2(1),
无论何种写法,都可以正常执行,
从谓词条件,能知道他的顺序就是我们需要的,先按照c1='3'进行过滤,他得到的c2,都是能使用to_number()函数的值,因此不会报错,
1 - filter(("C1"='3' AND TO_NUMBER("C2")>2))
说明Oracle在做查询转换的时候,针对char、varchar2、number不同的数据类型,可能设置了不同的优先级,才导致了不同的执行条件顺序。
从这个案例中,还可以提醒我们一点,就是字段的含义和类型应该保持一致,这是在数据库设计阶段需要注意的。例如c1存储的就是“数字”,如果定义为字符串类型,varchar2还好,char就出现了上述执行错误的场景,如果“数字”就使用数值类型number存储,就会绕过这个坑,同理,像“日期”用字符串类型存储存在相同的问题,可以参考《为什么日期不建议使用VARCHAR2或者NUMBER?》。
因此,有时一些表象问题背后,可能蕴藏着非规范的问题,或者说其实可以通过规范的设计和开发,避免这些“坑”,知其然更要知其所以然,更要从根本,解决碰到的问题。