【问题标题】:Oracle - Optimizing looping over all regex matches on CLOB column when only interested in the distinct matchesOracle - 当只对不同的匹配感兴趣时,优化循环 CLOB 列上的所有正则表达式匹配
【发布时间】:2014-12-31 15:29:30
【问题描述】:

我正在创建一个循环遍历表(有数千行)的存储过程,并且对于每一行都有一个 CLOB 列,我想从中获取正则表达式的所有匹配项(sa“FNR”)。此后,我想在新表中插入每个 distinct 匹配项。单个 CLOB 列可能包含数千个匹配项,但通常它是在 CLOB 中重复的相同“FNR” - 即,不同的正则表达式匹配项要少得多,而这些是我唯一感兴趣的。 然而,我所做的过程花费了非常长的时间,我怀疑循环遍历所有匹配项是最耗时的部分。

我的程序如下所示:

CREATE OR REPLACE PROCEDURE SP_MTV_FINN_FNR AS
BEGIN
DECLARE 
  v_n NUMBER;
  v_cnt NUMBER;
  v_mtrid NUMBER;
  v_regex_fnr VARCHAR2(54) := '(((0[1-9]|[12]\d|3[01])(0[1-9]|1[012])(\d{2}))(\d{5}))';
  v_doc CLOB;
  v_fnr VARCHAR2(11);

  BEGIN
    -- Get all rows from table --
    SELECT COUNT(*) INTO v_n FROM TABLE;
    IF v_n > 0 THEN
      -- Loop over all rows --
      FOR i IN 1..v_n LOOP
        SELECT doc, mtrid
          INTO v_doc, v_mtrid
          FROM (SELECT DOC doc, ID mtrid, ROWNUM rnum
            FROM TABLE
            WHERE ROWNUM <=i)
          WHERE rnum >= i;
        IF v_doc IS NOT NULL THEN
          SELECT REGEXP_COUNT(v_doc, v_regex_fnr) INTO v_cnt FROM DUAL;
          IF v_cnt >= 1 THEN
            -- For each regex match - time consuming, right? --
            FOR j IN 1..v_cnt LOOP
              SELECT REGEXP_SUBSTR(v_doc, v_regex_fnr, 1, j, 'm') INTO v_fnr FROM DUAL;
                IF CHECK_FNR(v_fnr) = 'TRUE' THEN
                  INSERT INTO TABLE2(MTR_ID, FNR)
                    SELECT v_mtrid, v_fnr FROM DUAL;
                END IF;
            END LOOP;
          END IF;
        END IF;
        COMMIT;
      END LOOP;
    END IF;
  END LOOP;
EXCEPTION WHEN OTHERS THEN
  DBMS_OUTPUT.PUT_LINE('Error - rollback');
  DBMS_OUTPUT.PUT_LINE('The error code is ' || SQLCODE || '- ' || SQLERRM);
  ROLLBACK;
END;

/

有人知道如何优化这个过程吗?

我使用的是 Oracle 11.2.0.3.0。 (顺便说一句,我知道 ctx_entity-package,但在这个版本上它被禁用了。不过,我正在考虑启用它。)

更新

在应用 nop77svk 提供的非常有用的性能优化技术后,我可以肯定地说 regexp_substr() 超过 CLOB 是瓶颈,因为不幸的是没有性能改进。 但是,我想出了一个“hack/workaround”,我将regexp_substr() 调用的数量降到最低,并带来了巨大的性能提升。首先,我想制作一个增量“训练”的正则表达式,不包括以前的匹配,但由于 Oracle 不支持负前瞻,所以这不起作用。我最终保存了CLOB,并使用regexp_replace() 删除了所有匹配项。由于 CLOB 中出现了很多相同的情况,这使该过程免于大量 regexp_substr() 调用,并且同时处理了 distinct 要求。

下面是我的结果,基于 nop77svk 的贡献。是的,我又在MERGE 语句中使用DUAL,但是这里有什么办法吗?

CREATE OR REPLACE PROCEDURE SP_MTV_FINN_FNR2 AS
BEGIN
DECLARE 
v_regex_fnr                 VARCHAR2(54) := '(((0[1-9]|[12]\d|3[01])(0[1-9]|1[012])(\d{2}))(\d{5}))';
v_fnr                       VARCHAR2(11);

v_doc CLOB;

type rec_table2             is record (
    mtr_id                      table2.mtr_id%type,
    fnr                         table2.fnr%type
);
type arr_table2             is table of rec_table2 index by simple_integer;
table2_bulk                 arr_table2;
table2_row                  rec_table2;
BEGIN
FOR rec IN (
    select doc, MTR_ID as mtrid
    from TABLE
    where DOC is not null
) LOOP
    v_doc := rec.doc;
    loop
        v_fnr := REGEXP_SUBSTR(v_doc, v_regex_fnr, 1, 1, 'm');
        exit when v_fnr is null;
        v_vedlegg := REGEXP_REPLACE(v_doc, v_fnr , '' , 1 , 0); -- Incrementally remove all occurences of match from doc --
        IF CHECK_FNR(v_fnr) = 'TRUE' THEN
            table2_row.mtr_id := rec.mtrid;
            table2_row.fnr := v_fnr;
            table2_bulk(table2_bulk.count+1) := table2_row;
        END IF;
    END LOOP;
END LOOP;
forall i in indices of table2_bulk
      MERGE INTO TABLE2 T
      USING (SELECT table2_bulk(i).mtr_id mtrid, table2_bulk(i).fnr fnr FROM DUAL) B
      ON (T.MTR_ID = B.mtrid AND T.FNR = B.fnr)
      WHEN NOT MATCHED THEN INSERT (T.MTR_ID, T.FNR)
      VALUES (B.mtrid, B.fnr);

COMMIT;
EXCEPTION
WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('Error - rollback');
    DBMS_OUTPUT.PUT_LINE('The error code is ' || SQLCODE || '- ' || SQLERRM);
    ROLLBACK;
END;
END;
/

【问题讨论】:

  • 从删除DUAL 表调用开始。 例如: v_cnt := REGEXP_COUNT(v_doc, v_regex_fnr)
  • 不要怀疑循环是慢的部分,检查一下。尽管嵌套循环通常是个坏主意。您还进行了不必要的连接切换。您实际上插入了多少行? check_fnr 在做什么?您可以将值放入一个集合中以消除重复项,然后在一个批量操作中插入所有内容,或者使用连接循环来提取匹配项等;但您需要测试阻塞点的实际位置。
  • 谢谢你们俩。插入的行数取决于正则表达式匹配的数量,但总共可能有几千个。 Check_fnr 正在对“fnr”进行校验和检查,因为正则表达式不足以识别匹配是否实际上是国家识别号。 check_fnr 过程通常需要 0.001-0.003s。

标签: regex oracle performance loops distinct


【解决方案1】:

迭代地调整您的 PL/SQL 块 ...

迭代 0: 修复语法错误 ...

CREATE OR REPLACE PROCEDURE SP_MTV_FINN_FNR AS
  v_n NUMBER;
  v_cnt NUMBER;
  v_mtrid NUMBER;
  v_regex_fnr VARCHAR2(54) := '(((0[1-9]|[12]\d|3[01])(0[1-9]|1[012])(\d{2}))(\d{5}))';
  v_doc CLOB;
  v_fnr VARCHAR2(11);

  BEGIN
    -- Get all rows from table --
    SELECT COUNT(*) INTO v_n FROM TABLE;
    IF v_n > 0 THEN
      -- Loop over all rows --
      FOR i IN 1..v_n LOOP
        SELECT doc, mtrid
          INTO v_doc, v_mtrid
          FROM (SELECT DOC doc, ID mtrid, ROWNUM rnum
            FROM TABLE
            WHERE ROWNUM <=i)
          WHERE rnum >= i;
        IF v_doc IS NOT NULL THEN
          SELECT REGEXP_COUNT(v_doc, v_regex_fnr) INTO v_cnt FROM DUAL;
          IF v_cnt >= 1 THEN
            -- For each regex match - time consuming, right? --
            FOR j IN 1..v_cnt LOOP
              SELECT REGEXP_SUBSTR(v_doc, v_regex_fnr, 1, j, 'm') INTO v_fnr FROM DUAL;
                IF CHECK_FNR(v_fnr) = 'TRUE' THEN
                  INSERT INTO TABLE2(MTR_ID, FNR)
                    SELECT v_mtrid, v_fnr FROM DUAL;
                END IF;
            END LOOP;
          END IF;
        END IF;
        COMMIT;
      END LOOP;
    END IF;
EXCEPTION WHEN OTHERS THEN
  DBMS_OUTPUT.PUT_LINE('Error - rollback');
  DBMS_OUTPUT.PUT_LINE('The error code is ' || SQLCODE || '- ' || SQLERRM);
  ROLLBACK;
END;

迭代 1: 删除不必要的 context switches 和无用的行计数 ...

DECLARE 
    v_cnt NUMBER;
    v_regex_fnr VARCHAR2(54) := '(((0[1-9]|[12]\d|3[01])(0[1-9]|1[012])(\d{2}))(\d{5}))';
    v_fnr VARCHAR2(11);
BEGIN
    FOR rec IN (
        select doc, id as mtrid
        from table
    ) LOOP
        IF rec.doc IS NOT NULL THEN
            v_cnt := REGEXP_COUNT(rec.doc, v_regex_fnr);

            IF v_cnt >= 1 THEN
                -- For each regex match - time consuming, right? --
                FOR j IN 1..v_cnt LOOP
                    v_fnr := REGEXP_SUBSTR(rec.doc, v_regex_fnr, 1, j, 'm');

                    IF CHECK_FNR(v_fnr) = 'TRUE' THEN
                        INSERT INTO TABLE2(MTR_ID, FNR) values (rec.mtrid, v_fnr);
                    END IF;
                END LOOP;
            END IF;
        END IF;
    END LOOP;
    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error - rollback');
        DBMS_OUTPUT.PUT_LINE('The error code is ' || SQLCODE || '- ' || SQLERRM);
        ROLLBACK;
END;
/

迭代 2: 减少外部循环的数量 ...

DECLARE 
    v_cnt NUMBER;
    v_regex_fnr VARCHAR2(54) := '(((0[1-9]|[12]\d|3[01])(0[1-9]|1[012])(\d{2}))(\d{5}))';
    v_fnr VARCHAR2(11);
BEGIN
    FOR rec IN (
        select doc, id as mtrid
        from table
        where doc is not null
    ) LOOP
        v_cnt := REGEXP_COUNT(rec.doc, v_regex_fnr);

        IF v_cnt >= 1 THEN
            -- For each regex match - time consuming, right? --
            FOR j IN 1..v_cnt LOOP
                v_fnr := REGEXP_SUBSTR(rec.doc, v_regex_fnr, 1, j, 'm');

                IF CHECK_FNR(v_fnr) = 'TRUE' THEN
                    INSERT INTO TABLE2(MTR_ID, FNR) values (rec.mtrid, v_fnr);
                END IF;
            END LOOP;
        END IF;
    END LOOP;
    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error - rollback');
        DBMS_OUTPUT.PUT_LINE('The error code is ' || SQLCODE || '- ' || SQLERRM);
        ROLLBACK;
END;
/

迭代 3: 缩短迭代 2 的代码 ...

DECLARE 
    v_regex_fnr VARCHAR2(54) := '(((0[1-9]|[12]\d|3[01])(0[1-9]|1[012])(\d{2}))(\d{5}))';
    v_fnr VARCHAR2(11);
BEGIN
    FOR rec IN (
        select doc, id as mtrid, REGEXP_COUNT(rec.doc, v_regex_fnr) as regexp_cnt
        from table
        where doc is not null
            and regexp_like(doc, v_regex_fnt)
    ) LOOP
        FOR j IN 1..rec.regexp_cnt LOOP
            v_fnr := REGEXP_SUBSTR(rec.doc, v_regex_fnr, 1, j, 'm');

            IF CHECK_FNR(v_fnr) = 'TRUE' THEN
                INSERT INTO TABLE2(MTR_ID, FNR) values (rec.mtrid, v_fnr);
            END IF;
        END LOOP;
    END LOOP;
    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error - rollback');
        DBMS_OUTPUT.PUT_LINE('The error code is ' || SQLCODE || '- ' || SQLERRM);
        ROLLBACK;
END;
/

迭代 4: 删除不必要的 regexp_count() 计数 ...

DECLARE 
    v_regex_fnr VARCHAR2(54) := '(((0[1-9]|[12]\d|3[01])(0[1-9]|1[012])(\d{2}))(\d{5}))';
    v_fnr VARCHAR2(11);
    j integer;
BEGIN
    FOR rec IN (
        select doc, id as mtrid
        from table
        where doc is not null
    ) LOOP
        j := 1;
        loop
            v_fnr := REGEXP_SUBSTR(rec.doc, v_regex_fnr, 1, j, 'm');
            exit when v_fnt is null;

            IF CHECK_FNR(v_fnr) = 'TRUE' THEN
                INSERT INTO TABLE2(MTR_ID, FNR) values (rec.mtrid, v_fnr);
            END IF;

            j := j + 1;
        END LOOP;
    END LOOP;
    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error - rollback');
        DBMS_OUTPUT.PUT_LINE('The error code is ' || SQLCODE || '- ' || SQLERRM);
        ROLLBACK;
END;
/

迭代 5: 将结果保存到内存并立即将其刷新到 DB(使用集合绑定),并处理 distinct 要求...

create or replace type obj_table2
as
object (
    mtr_id                      integer,
    fnr                         varchar2(4000)
);
/
create or replace type arr_table2
as
table of obj_table2;
/

DECLARE 
    v_regex_fnr                 VARCHAR2(54) := '(((0[1-9]|[12]\d|3[01])(0[1-9]|1[012])(\d{2}))(\d{5}))';
    v_fnr                       VARCHAR2(11);
    j                           integer;

    table2_bulk                 arr_table2 := arr_table2();
BEGIN
    FOR rec IN (
        select doc, id as mtrid
        from table
        where doc is not null
    ) LOOP
        j := 1;
        loop
            v_fnr := REGEXP_SUBSTR(rec.doc, v_regex_fnr, 1, j, 'm');
            exit when v_fnt is null;

            IF CHECK_FNR(v_fnr) = 'TRUE' THEN
                table2_bulk.extend();
                table2_bulk(table2_bulk.last) := new obj_table2(
                    mtr_id => rec.mtrid,
                    fnr => v_fnr
                );
            END IF;

            j := j + 1;
        END LOOP;
    END LOOP;

    insert into table2(mtr_id, fnr)
    select mtr_id, fnr
    from table(table2_bulk) X
    minus
    select mtr_id, fnr
    from table2;

    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        DBMS_OUTPUT.PUT_LINE('Error - rollback');
        DBMS_OUTPUT.PUT_LINE('The error code is ' || SQLCODE || '- ' || SQLERRM);
        ROLLBACK;
END;
/

迭代 6: 在决定 show off detestably 的同时将其全部扔掉......

insert into table2 (mtr_id, fnr)
with xyz (doc, mtrid, fnr, j) as (
    select doc, id as mtrid, cast(null as varchar2(4000)) as fnr, 0 as j
    from table A
    where doc is not null
    --
    union all
    --
    select doc, mtrid,
        regexp_substr(doc, '(((0[1-9]|[12]\d|3[01])(0[1-9]|1[012])(\d{2}))(\d{5}))', 1, j+1, 'm') as fnr,
        j+1
    from xyz X
    where j = 0
        or j > 0 and X.fnr is not null
)
select distinct mtrid, fnr
from xyz
where j > 0
    and fnr is not null
    and CHECK_FNR(fnr) = 'TRUE'
;
commit;

请注意,这些代码 sn-ps 甚至可能不起作用。由于您没有向我们提供任何测试数据设置,我们只能以假设的方式调整您的代码。

请注意,其中最慢的部分仍然是 regexp_substr() 超过 CLOB 值。您可能需要考虑使用regexp_substr()position 参数而不是occurence 参数来获取后续的正则表达式匹配。

享受吧。

【讨论】:

  • 对于使用递归 CTE;那时不只是我炫耀那些!解释一下它在做什么可能会有所帮助,或者至少有一个文档链接
  • 谢谢,伙计们。 :-) 添加了一些链接,也修复了我第一次忘记的区分要求。亚历克斯,你肯定不是唯一的 :-) ,这个递归的 with 是一个值得传福音的奇迹(虽然偶尔会出现错误)。
  • 哇,感谢您真正彻底的解释和良好的答案!我一定会尝试这些迭代(并阅读​​文档以了解迭代 6 中发生了什么;))我应该告诉 check_fnr 函数在做什么,但它与唯一性无关。它是“fnr”的校验和检查。顺便说一句,在第 5 次迭代中,进行不同检查的最佳方法是什么?也许在调用 check_fnr 函数之前检查它是否存在于 table2_bulk 中?
  • 是的,几分钟前刚刚阅读并相应地修改了我的答案。请不要将这些代码 sn-ps 作为 alpha 和 omega;而是从中找到灵感 - 解决性能问题的迭代方法可以让您的生活更轻松。如果我的回答使您的代码运行得更快,那么我很高兴能提供帮助。 :-)
  • 至于迭代 5 中的区别检查... 硬核。您可以使用模式级对象+集合类型,并通过将集合(作为数据源,在from 子句中)绑定到插入选择而不是使用批量 DML(即forall ...)来实现。跨度>
【解决方案2】:

使用静态调用删除 DUAL 引用。从而在 PL/SQL 和 SQL 引擎之间节省了不需要的context switching

我还添加了隐式游标来逐条处理。

下一级改进可能是bulk insert 进入另一个表。虽然我没有在这里做。

CREATE OR REPLACE PROCEDURE SP_MTV_FINN_FNR AS
BEGIN
DECLARE 
  v_n NUMBER;
  v_cnt NUMBER;
  v_mtrid NUMBER;
  v_regex_fnr VARCHAR2(54) := '(((0[1-9]|[12]\d|3[01])(0[1-9]|1[012])(\d{2}))(\d{5}))';
  v_doc CLOB;
  v_fnr VARCHAR2(11);

BEGIN
    -- Get all rows from table --
  /* Lets go with a Implicit cursor */
  FOR MYREC IN (SELECT DOC doc, ID mtrid
                  FROM TABLE)
  LOOP
     IF MYREC.DOC IS NOT NULL THEN
        v_cnt := REGEXP_COUNT(MYREC.DOC, v_regex_fnr);
        IF v_cnt >= 1 THEN
        -- For each regex match - time consuming, right? --
            FOR j IN 1..v_cnt LOOP
              v_fnr := REGEXP_SUBSTR(MYREC.DOC, v_regex_fnr, 1, j, 'm');
              IF CHECK_FNR(v_fnr) = 'TRUE' THEN
                INSERT INTO TABLE2(MTR_ID, FNR)
                    VALUES (MYREC.MTRID,v_fnr);
                END IF;
            END LOOP;
        END IF;
        COMMIT;
    END IF;
  END LOOP;
EXCEPTION WHEN OTHERS THEN
  DBMS_OUTPUT.PUT_LINE('Error - rollback');
  DBMS_OUTPUT.PUT_LINE('The error code is ' || SQLCODE || '- ' || SQLERRM);
  ROLLBACK;
END;

【讨论】:

  • 感谢您的贡献。然而,在这种情况下,上下文切换并不是瓶颈,但所有regexp_substr() 调用都是。如果您有兴趣,我已经用我想出的解决方法更新了这个问题!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-03-17
  • 1970-01-01
  • 1970-01-01
  • 2010-11-16
  • 1970-01-01
  • 2014-05-08
  • 2016-11-04
相关资源
最近更新 更多