为什么在LOOP中捕获错误会导致性能问题?

时间:2017-09-25 16:23:07

标签: postgresql

我有一个性能问题的功能:

totalCharge := 0;
FOR myRecord IN ... LOOP
    ......
    IF severalConditionsAreMet THEN
        BEGIN

            SELECT t1.charge INTO STRICT recordCharge
            FROM t1
            WHERE t1.id = myRecord.id AND otherComplexConditionsHere;

            totalCharge := totalCharge + recordCharge;

            ...........
        EXCEPTION
             WHEN OTHERS THEN 
                 NULL;
        END;
    END IF;

END LOOP;

该函数被调用232次(不计算访问FOR的代码的次数)。 来自FOR LOOP的IF最终被访问4466次,并且需要561秒来完成所有4466次迭代。

对于我所拥有的特定数据集,始终访问IF,上面的SELECT从不返回数据,代码每次都到达EXCEPTION分支。 我已将代码更改为:

totalCharge := 0;
FOR myRecord IN ... LOOP
    ......
    IF severalConditionsAreMet THEN

        SELECT t1.charge INTO recordCharge
        FROM t1
        WHERE t1.id = myRecord.id AND otherComplexConditionsHere;

        IF (recordCharge IS NULL) THEN
            CONTINUE;
        END IF;

        totalCharge := totalCharge + recordCharge;

        ...........

    END IF;

END LOOP;

请注意,对于表t1,t1.charge列上定义了NOT NULL条件。 这次,来自IF的代码需要1-2秒才能完成所有4466次迭代。

基本上,我所做的只是替换

BEGIN
…
EXCEPTION
….
END;

使用

IF conditionIsNotMet THEN
    CONTINUE;         
END IF;

有人可以向我解释为什么这有效吗? 幕后发生了什么? 我怀疑当你在LOOP中捕获异常并且代码最终生成异常时,Postgres不能使用缓存计划来优化该代码,因此它最终会在每次迭代时规划代码,这会导致性能问题。 我的假设是否正确?

稍后编辑:

我改变了Vao Tsun提供的例子来反映我想要说明的案例。

CREATE OR REPLACE FUNCTION initialVersion()
RETURNS VOID AS $$
declare
  testDate DATE;
begin
  for i in 1..999999 loop
    begin
    select now() into strict testDate where 1=0;
    exception when others 
    then null;
    end;
  end loop;
end;
$$ Language plpgsql;

CREATE OR REPLACE FUNCTION secondVersion()
RETURNS VOID AS $$
declare
    testDate DATE;
begin
  for i in 1..999999 loop
    select now() into testDate where 1=0;
    if testDate is null then 
      continue;
    end if;
  end loop;
end;
$$ Language plpgsql;

select initialVersion(); -- 19.7 seconds

select secondVersion(); -- 5.2

正如您所看到的,差异大约为15秒。 在我最初提供的示例中,差异更大,因为SELECT FROM t1针对复杂数据运行,并且需要更多时间来执行第二个示例中提供的简单SELECT。

2 个答案:

答案 0 :(得分:1)

我在PostgreSQL - 一般邮件组中提出了同样的问题here,并得到了一些回答,阐明了这个问题,并且#34;神秘"对我来说:

David G. Johnston:

  

" Tip:包含EXCEPTION子句的块非常重要   进入和退出比没有一个的块更昂贵。因此,   不需要使用EXCEPTION。"

     

我有点怀疑"计划缓存"与此有关;一世   怀疑它基本上存在高内存和运行时开销   处理需要将异常转换为的可能性   分支而不是让它致命。

汤姆莱恩:

  

是的,它是关于设置和结束的开销   子事务。这是一个相当昂贵的机制,但我们没有   任何能够从任意错误中恢复的更便宜的东西。

以及David G. Johnston的补充:

  

[...]设置pl / pgsql执行层以捕获"任意SQL层   异常"相当昂贵。即使用户指定具体   错误pl / pgsql中的错误处理机制是泛型的代码   (任意)错误。

这些答案帮助我了解了事情的运作方式。 我在这里发布这个答案,因为我希望这个答案可以帮助别人。

答案 1 :(得分:0)

给出详细信息 - 无法重现:

t=# do
$$
declare
begin
  for i in 1..999999 loop
    perform now();
/*    exception when others then null; */
    if null then null; end if;
  end loop;
end;
$$
;
DO
Time: 1920.568 ms
t=# do
$$
declare
begin
  for i in 1..999999 loop
    begin
    perform now();
    exception when others then null;
    end;
  end loop;
end;
$$
;
DO
Time: 2417.425 ms

正如你所看到的那样,千万次迭代的差异是显而易见的,但微不足道。请在您的机器上进行相同的测试 - 如果您得到相同的结果,则需要提供更多详细信息......