在 SQL 语句中,如果不合理的使用函数就会严重影响性能,其实这里想说的是 PL/SQL 中的自定义函数,反而对于一些内置函数而言,影响性能的可能性较小。

为什么函数影响性能

在 SELECT 语句中调用函数时,那么查询返回的结果集中的每一行都会调用该函数。如果该函数需要执行 1 秒,返回的结果集是 10 行,那么此时 SQL 语句就需要 10 秒,如果该函数执行时间需要 3 秒,返回的结果集是 10000 条记录,那么这个时间就是 30000 秒 ≈ 500 分钟。

生产环境中自定义函数有时候会出现复杂的业务逻辑,导致自定义函数性能开销较高,如果出现不合理调用,那么很容易就会出现性能问题。

  • 创建表 TEST
SQL> create table test (id number);

Table created


declare
  row_index number;
begin
  row_index := 1;
  while row_index <= 8 loop  
    insert into test select row_index from dual;
    row_index := row_index + 1;
  end loop;
  commit;
end;
/
  • 创建函数 F_SLOW,使用 DBMS_LOCK 休眠 2 秒,模拟这个函数较慢
SQL> conn / as sysdba

SQL> grant execute on dbms_lock to scott;


create or replace function f_slow(i_value in number)
return number as
begin
  dbms_lock.sleep(2);
  return i_value + 10;
end;
/
  • 在 SELECT 中调用函数
SQL> set timing on;


SQL> select * from test;

        ID
----------
         1
         2
         3
         4
         5
         6
         7
         8

8 rows selected

Executed in 0.095 seconds


SQL> select f_slow(id) from test;

F_SLOW(ID)
----------
        11
        12
        13
        14
        15
        16
        17
        18

8 rows selected

Executed in 16.243 seconds

在 SELECT 中使用函数,由于有 8 条记录,而每次调用函数需要 Sleep 2 秒,所以总共耗费了 16 秒。

  • 在 WHERE 中调用函数
SQL> select * from test where f_slow(id) > 15;

        ID
----------
         6
         7
         8

Executed in 16.168 seconds

在 WHERE 中使用函数,同样耗费了 16 秒。

什么情况下函数影响性能

其实自定义函数影响性能,主要在与函数调用的次数或函数本身的业务逻辑是否复杂,如果 SELECT 查询中调用次数很少,影响还是非常小的。

  • 仅调用一次函数
SQL> select f_slow(id) from test where id = 2;

F_SLOW(ID)
----------
        12

Executed in 2.03 seconds
  • 函数业务逻辑简单,性能开销很低
--改写函数逻辑
create or replace function f_slow(i_value in number)
return number as
begin
  return i_value + 10;
end;
/


--创建存储过程,测试循环次数对性能的影响
create or replace procedure p_test_slow(i_iter in number)
as
  v_res varchar2(100);
begin
  for i in 1 .. i_iter loop
    select f_slow(i) into v_res from dual;
  end loop;
end;
/


--函数循环 10 次耗费的时间
SQL> exec p_test_slow(10);

PL/SQL procedure successfully completed

Executed in 0.005 seconds


--函数循环 1000 次耗费的时间
SQL> exec p_test_slow(1000);

PL/SQL procedure successfully completed

Executed in 0.02 seconds


--函数循环 100000 次耗费的时间
SQL> exec p_test_slow(100000);

PL/SQL procedure successfully completed

Executed in 1.886 seconds

从上述结果可知,当函数业务逻辑简单,性能开销很低时,循环次数对性能的影响较小。10 次循环跟 100000 循环调用差别是 1.8 秒多。

如何优化解决问题

对 SQL 中调用的自定义函数,可以通过等价改写成多表关联语句。避免产生大量的递归调用,另外就是设法减少函数被调用的次数。SQL 中应尽量避免使用自定义函数,或者尽量避免自定义函数中实现复杂业务逻辑。

标量子查询缓存

标量子查询缓存会通过缓存结果减少 SQL 对函数的调用次数,Oracle 会在内存中构建一个哈希表来缓存标量子查询的结果。当然前提是有重复值的情况下,如果没有重复值,其实这种办法是没有任何效果的

  • 将函数 F_SLOW 修改为休眠 2 秒
create or replace function f_slow(i_value in number)
return number as
begin
  dbms_lock.sleep(2);
  return i_value + 10;
end;
/
  • 直接在 SELECT 中调用函数,并查看其执行时间
SQL> select f_slow(id) from test;

F_SLOW(ID)
----------
        11
        12
        13
        14
        15
        16
        17
        18

8 rows selected

Executed in 16.254 seconds
  • 使用标量子查询,并查看其执行时间
SQL> select (select f_slow(id) from dual) from test;

(SELECTF_SLOW(ID)FROMDUAL)
--------------------------
                        11
                        12
                        13
                        14
                        15
                        16
                        17
                        18

8 rows selected

Executed in 16.158 seconds

上述执行结果中,使用标量子查询和未使用标量子查询的执行时间几乎一样,这是因为表 TEST 中没有重复值导致。

  • 删除表 TEST 数据,构造重复数据
SQL> truncate table test;

Table truncated


SQL> insert into test 
select 1 from dual union all
select 2 from dual union all
select 2 from dual union all
select 3 from dual union all
select 3 from dual union all
select 3 from dual;

SQL> COMMIT;
  • 直接在 SELECT 中调用函数,并查看其执行时间
SQL> select f_slow(id) from test;

F_SLOW(ID)
----------
        11
        12
        12
        13
        13
        13

6 rows selected

Executed in 12.178 seconds
  • 使用标量子查询,并查看其执行时间
SQL> select (select f_slow(id) from dual) from test;

(SELECTF_SLOW(ID)FROMDUAL)
--------------------------
                        11
                        12
                        12
                        13
                        13
                        13

6 rows selected

Executed in 6.187 seconds

上述执行结果中,使用标量子查询的执行时间为 6s,而未使用标量子查询的执行时间为 12s,这是因为表 TEST 中存在重复值导致。

当使用标量子查询的时候,Oracle 会将子查询结果缓存在哈希表中,如果后续的记录出现同样的值,优化器通过存在哈希表中的值,判断重复值不用重复调用函数,直接使用上次计算结果即可。从而减少函数调用次数以达到优化性能的效果。

DETERMINISTIC 关键字

Oracle 通过关键字 DETERMINISTIC声明一个函数是确定性的,确定性函数可以用于创建基于函数的索引。这个仅仅能在 Oracle 10g 以后的版本中使用,它会影响函数如何缓存在 SQL 中。

  • 新建带有 DETERMINISTIC 关键字的函数
create or replace function f_slow_derm(i_value in number)
return number
deterministic
as 
begin
  dbms_lock.sleep(2);
  return i_value + 10;
end;
/
  • 直接在 SELECT 中调用不带 DETERMINISTIC 关键字的函数,并查看其执行时间
SQL> select f_slow(id) from test;

F_SLOW(ID)
----------
        11
        12
        12
        13
        13
        13

6 rows selected

Executed in 12.101 seconds
  • 直接在 SELECT 中调用带 DETERMINISTIC 关键字的函数,并查看其执行时间
SQL> select f_slow_derm(id) from test;

F_SLOW_DERM(ID)
---------------
             11
             12
             12
             13
             13
             13

6 rows selected

Executed in 6.074 seconds

从上述结果可知,将函数标记为确定性函数确实可以提高性能,但 DETERMINISTIC 缓存受限于每次从服务器 fetch 多少数据,缓存仅在当前 fetch 的生命周期内有效,而标量子查询是当前查询内有效。另外,DETERMINISTIC 还受到数组大小的影响,数组大小(arraysize)的差异会产生截然不同的性能。

SQL> show arraysize;
arraysize 15


SQL> select f_slow_derm(id) from test;

F_SLOW_DERM(ID)
---------------
             11
             12
             12
             13
             13
             13

6 rows selected

Executed in 6.107 seconds


SQL> set arraysize 1;


SQL> select f_slow_derm(id) from test;

F_SLOW_DERM(ID)
---------------
             11
             12
             12
             13
             13
             13

6 rows selected

Executed in 12.067 seconds

arraysize 定义了一次返回到客户端的行数,当扫描了 arraysize 后,停止扫描,返回数据,然后继续扫描。 这个过程就是 SET AUTOTRACE ON 统计信息中的 SQL*Net roundtrips to/from client。 因为 arraysize 默认是 15 行,那么就有一个问题,因为我们一个 block 中的记录数一般都会超过 15 行,所以如果按照 15 行扫描一次,那么每次扫描要么仅扫描一个数据块,要么一个数据块重复扫描多次。

Function Result Cache

Oracle 11g 中引入了两个新的缓存机制。

  • 跨会话 PL/SQL 函数结果缓存

跨会话 PL/SQL 函数结果缓存(Cross-Session PL/SQL Function Result Cache):用于缓存函数调用的结果。

  • 查询结果缓存

查询结果缓存(Query Result Cache):用于缓存查询产生的整个结果集。

Oracle 11g 提供的 PL/SQL 函数的缓存机制(对于不同的会话之间可以共用),下面我们使用第一种机制进行查询结果的缓存,如下所示,指定关键词 RESULT_CACHE 来启用 Cross-Session PL/SQL Function Result Cache。

创建函数,并指定关键词 RESULT_CACHE

create or replace function f_slow(i_value in number)
return number
result_cache
as
begin
  dbms_lock.sleep(2);
  return i_value + 10;
end;
/

重建表 TEST 的数据

SQL> truncate table test;

Table truncated


declare
  row_index number;
begin
  row_index := 1;
  while row_index <= 8 loop  
    insert into test select row_index from dual;
    row_index := row_index + 1;
  end loop;
  commit;
end;
/

新建一个 session,记作 session1,在 session1 中调用函数 f_slow(),测试 SQL 性能

SQL> select * from v$mystat where rownum = 1;

       SID STATISTIC#      VALUE
---------- ---------- ----------
       195          0          0


SQL> set timing on;


SQL> select f_slow(id) from test;

F_SLOW(ID)
----------
        11
        12
        13
        14
        15
        16
        17
        18

8 rows selected

Executed in 10.147 seconds        

从上述结果可知,在 session1 中,其执行时间约为 10s。

新建一个 session,记作 session2,在 session2 中调用函数 f_slow(),测试 SQL 性能

SQL> select * from v$mystat where rownum = 1;

       SID STATISTIC#      VALUE
---------- ---------- ----------
        67          0          0


SQL> set timing on;


SQL> select f_slow(id) from test;

F_SLOW(ID)
----------
        11
        12
        13
        14
        15
        16
        17
        18

8 rows selected

Executed in 0.068 seconds

从上述结果可知,在 session2 中,其执行时间约为 0.06s。

从 session1 和 session2 的执行结果可知,使用上述方式的缓存信息可以被其他的会话使用,它依赖的对象是自动管理的。也就是说,数据库会帮我们自动缓存,从而当其他会话调用时,使用相关缓存结果来减少函数调用次数,从而达到提高性能效果。

参考资料

https://www.cnblogs.com/kerrycode/p/9099507.html

http://blog.chinaunix.net/uid-22948773-id-3354466.html?utm_source=jiancool

原创文章,转载请注明出处:http://www.opcoder.cn/article/47/