【问题标题】:Is the use of SELECT COUNT(*) before SELECT INTO slower than using Exceptions?在 SELECT INTO 之前使用 SELECT COUNT(*) 是否比使用 Exceptions 慢?
【发布时间】:2013-08-09 13:25:18
【问题描述】:

我的last question 让我思考。

1)

SELECT COUNT(*) INTO count FROM foo WHERE bar = 123;
IF count > 0 THEN
    SELECT a INTO var FROM foo WHERE bar = 123;
    -- do stuff
ELSE
    -- do other stuff
END IF;

2)

BEGIN
    SELECT a INTO var FROM foo where bar = 123;
    -- do stuff
EXCEPTION
    WHEN no_data_found THEN
        --do other stuff
END ;

我假设 2 号更快,因为它需要少一次访问数据库。

有没有我不考虑的情况下 1 会更好?

编辑:我打算让这个问题再挂几天,在回答之前收集更多关于答案的投票。

【问题讨论】:

  • 首先,我认为如果您检查一下,您会发现使用异常会更慢。其次,也是最重要的,异常中断程序的流程使它们更难阅读、理解和理解。如果你想检查一个条件是否返回一行,那么只需计算行数并处理结果。

标签: oracle plsql oracle10g


【解决方案1】:

如果您使用问题中的精确查询,那么第一个变体当然会更慢,因为它必须计算表中满足条件的所有记录。

必须写成

SELECT COUNT(*) INTO row_count FROM foo WHERE bar = 123 and rownum = 1;

select 1 into row_count from dual where exists (select 1 from foo where bar = 123);

因为检查记录是否存在就足以满足您的目的。

当然,这两种变体都不能保证其他人不会在两个语句之间更改 foo 中的某些内容,但如果此检查是更复杂场景的一部分,则不是问题。想想当有人在将foo.a 的值选择为var 后更改了foo.a 的值的情况,同时执行了一些引用选定var 值的操作。因此,在复杂的场景中,最好在应用程序逻辑级别处理此类并发问题。
执行原子操作最好使用单个 SQL 语句。

上述任何变体都需要在 SQL 和 PL/SQL 之间进行 2 次上下文切换以及 2 次查询,因此在表中找到行的情况下,执行速度比下面描述的任何变体都要慢。

还有另一种变体可以无一例外地检查行的存在:

select max(a), count(1) into var, row_count 
from foo 
where bar = 123 and rownum < 3;

如果 row_count = 1,则只有一行满足条件。

有时只检查存在就足够了,因为foo 的唯一约束保证foo 中没有重复的bar 值。例如。 bar 是主键。
在这种情况下,可以简化查询:

select max(a) into var from foo where bar = 123;
if(var is not null) then 
  ...
end if;

或使用光标处理值:

for cValueA in ( 
  select a from foo where bar = 123
) loop
  ...  
end loop;

下一个变体来自link,由@user272735 在他的回答中提供:

select 
  (select a from foo where bar = 123)
  into var 
from dual;

根据我的经验,在大多数情况下,任何没有异常块的变体都比有异常的变体更快,但是如果此类块的执行次数较低,那么最好使用异常块处理 no_data_foundtoo_many_rows 异常来改进代码可读性。

选择使用异常或不使用异常的正确点是问一个问题“这种情况对于应用程序来说是正常的吗?”。如果未找到行并且这是可以处理的预期情况(例如添加新行或从另一个地方获取数据等),最好避免异常。如果发生意外并且无法修复情况,则捕获异常以自定义错误消息,将其写入事件日志并重新抛出,或者根本不捕获它。

要比较性能,只需在您的系统上创建一个简单的测试用例,并多次调用这两个变体并进行比较。
再说了,在 90% 的应用程序中,这个问题更多的是理论而不是实际,因为还有很多其他的性能问题来源必须首先考虑。

更新

我在 SQLFiddle 站点上复制了来自 this page 的示例,并进行了一些更正 (link)。
结果证明,从dual 中选择的变体表现最好:当大多数查询成功时开销很小,而当丢失行数增加时性能下降最低。
令人惊讶的是,如果所有查询都失败,则 count() 和两个查询的变体显示最佳结果。

| FNAME | LOOP_COUNT | ALL_FAILED | ALL_SUCCEED | variant name |
----------------------------------------------------------------
|    f1 |       2000 |       2.09 |        0.28 |  exception   |
|    f2 |       2000 |       0.31 |        0.38 |  cursor      |
|    f3 |       2000 |       0.26 |        0.27 |  max()       |
|    f4 |       2000 |       0.23 |        0.28 |  dual        |
|    f5 |       2000 |       0.22 |        0.58 |  count()     |

-- FNAME        - tested function name 
-- LOOP_COUNT   - number of loops in one test run
-- ALL_FAILED   - time in seconds if all tested rows missed from table
-- ALL_SUCCEED  - time in seconds if all tested rows found in table
-- variant name - short name of tested variant

下面是测试环境和测试脚本的设置代码。

create table t_test(a, b)
as
select level,level from dual connect by level<=1e5
/
insert into t_test(a, b) select null, level from dual connect by level < 100
/

create unique index x_text on t_test(a)
/

create table timings(
  fname varchar2(10), 
  loop_count number, 
  exec_time number
)
/

create table params(pstart number, pend number)
/
-- loop bounds
insert into params(pstart, pend) values(1, 2000)
/

-- f1 - 异常处理

create or replace function f1(p in number) return number
as
  res number;
begin
  select b into res
  from t_test t
  where t.a=p and rownum = 1;
  return res;
exception when no_data_found then
  return null;
end;
/

-- f2 - 光标循环

create or replace function f2(p in number) return number
as
  res number;
begin
  for rec in (select b from t_test t where t.a=p and rownum = 1) loop
    res:=rec.b;
  end loop;
  return res;
end;
/

-- f3 - max()

create or replace function f3(p in number) return number
as
  res number;
begin
  select max(b) into res
  from t_test t
  where t.a=p and rownum = 1;
  return res;
end;
/

-- f4 - 在 select from dual 中选择为字段

create or replace function f4(p in number) return number
as
  res number;
begin
  select
    (select b from t_test t where t.a=p and rownum = 1)
    into res
  from dual;
  return res;
end;
/

-- f5 - 检查 count() 然后获取值

create or replace function f5(p in number) return number
as
  res number;
  cnt number;
begin
  select count(*) into cnt
  from t_test t where t.a=p and rownum = 1;

  if(cnt = 1) then
    select b into res from t_test t where t.a=p;
  end if;

  return res;
end;
/

测试脚本:

declare
  v       integer;
  v_start integer;
  v_end   integer;

  vStartTime number;

begin
  select pstart, pend into v_start, v_end from params;

  vStartTime := dbms_utility.get_cpu_time;

  for i in v_start .. v_end loop
    v:=f1(i);
  end loop;

  insert into timings(fname, loop_count, exec_time) 
    values ('f1', v_end-v_start+1, (dbms_utility.get_cpu_time - vStartTime)/100) ;
end;
/

declare
  v       integer;
  v_start integer;
  v_end   integer;

  vStartTime number;

begin
  select pstart, pend into v_start, v_end from params;

  vStartTime := dbms_utility.get_cpu_time;

  for i in v_start .. v_end loop
    v:=f2(i);
  end loop;

  insert into timings(fname, loop_count, exec_time) 
    values ('f2', v_end-v_start+1, (dbms_utility.get_cpu_time - vStartTime)/100) ;
end;
/

declare
  v       integer;
  v_start integer;
  v_end   integer;

  vStartTime number;

begin
  select pstart, pend into v_start, v_end from params;

  vStartTime := dbms_utility.get_cpu_time;

  for i in v_start .. v_end loop
    v:=f3(i);
  end loop;

  insert into timings(fname, loop_count, exec_time) 
    values ('f3', v_end-v_start+1, (dbms_utility.get_cpu_time - vStartTime)/100) ;
end;
/

declare
  v       integer;
  v_start integer;
  v_end   integer;

  vStartTime number;

begin
  select pstart, pend into v_start, v_end from params;

  vStartTime := dbms_utility.get_cpu_time;

  for i in v_start .. v_end loop
    v:=f4(i);
  end loop;

  insert into timings(fname, loop_count, exec_time) 
    values ('f4', v_end-v_start+1, (dbms_utility.get_cpu_time - vStartTime)/100) ;
end;
/

declare
  v       integer;
  v_start integer;
  v_end   integer;

  vStartTime number;

begin
  select pstart, pend into v_start, v_end from params;
  --v_end := v_start + trunc((v_end-v_start)*2/3);

  vStartTime := dbms_utility.get_cpu_time;

  for i in v_start .. v_end loop
    v:=f5(i);
  end loop;

  insert into timings(fname, loop_count, exec_time) 
    values ('f5', v_end-v_start+1, (dbms_utility.get_cpu_time - vStartTime)/100) ;
end;
/

select * from timings order by fname
/

【讨论】:

  • 我认为您的性能测试不会产生有意义的结果,因为当第一次读取表 t_test 时,它会被放入缓存/缓冲区,因此在所有其他执行中都不会再次读取磁盘。因此,您主要衡量的是内存吞吐量,而不是实际应用程序中的性能。
  • @Wernfried 你是对的,这个测试不是关于查询性能的。查询的复杂性不是该研究的主题,测试仅说明处理查询结果的方法。所有案例都通过唯一索引字段查询表并使用相同的模式where t.a=p and rownum = 1。所以所有的性能优势和劣化都属于在 PL/SQL 中处理查询结果的一种方式。
【解决方案2】:

首先看到:Oracle PL/SQL - Are NO_DATA_FOUND Exceptions bad for stored procedure performance? 这与您的问题基本相同。之后见About the performance of exception handling

在这两种情况下,您还应该准备好处理 too_many_rows 异常,除非您的数据库架构强制执行 bar 的唯一性。

这是 PL/SQL,因此您一直在进行数据库之旅 - 相反,您应该害怕/意识到 PL/SQL - SQL context switches。另见what Tom says

但不要害怕从 PLSQL 调用 SQL - 这是 PLSQL 最擅长的。

首先,您不应该担心程序的性能,而应该担心程序的正确性。在这方面,我投票支持方案 #2。

【讨论】:

    【解决方案3】:

    我不确定更快,但我会说 (2) 显然更好,因为您没有考虑有人在 (1) 中的陈述之间发出 DELETE FROM foo where bar='123' 的情况。

    【讨论】:

    • 我想投两票。这是关于一致性,而不是性能。
    【解决方案4】:

    这种情况我通常是这样做的:

    DECALRE
       CURSOR cur IS
       SELECT a FROM foo where bar = 123;
    BEGIN
       OPEN cur;
       FETCH cur INTO var;
       IF cur%FOUND THEN
          -- do stuff, maybe a LOOP if required
       ELSE
          --do other stuff
       END;
    END;
    

    这有一些好处:

    您只从数据库中读取 一个 记录,其余的则跳过。如果您只需要知道行数是否 > 1,应该是最快的方法。

    您不会使用“异常”处理程序来处理“正常”情况,有些人认为这是“更漂亮”的编码。

    【讨论】:

    • 看起来像“游标循环”变体的另一种语法:游标,没有例外。但是每个游标都会增加一点开销,因为它需要打开和关闭。另一件事:如果您在查询中指定“where rownum = 1”,则服务器永远不会超出找到的第一个记录,因此即使max()count() 具有这种条件的变体,第一个好处也是如此。
    • 你确定吗?每个 DML 或 SELECT 在数据库中创建一个 隐式 游标,内部过程是:打开游标 -> 解析语句 -> 绑定变量(如果需要) -> 执行 -> 获取行 -> 关闭游标。与 显式 游标的区别主要在于您必须输入的代码量(据我所知)。
    • 抱歉有错误。当然,游标在这两种情况下都存在。我真正想指出的是 PL/SQL 代码中存在游标变量,需要由 PL/SQL 解释器单独处理。在我们的主题上有一个nice discussion at AskTom
    猜你喜欢
    • 2011-09-25
    • 2014-01-19
    • 2015-01-15
    • 1970-01-01
    • 2014-11-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多