【问题标题】:Optimal ORACLE function parsing最优ORACLE函数解析
【发布时间】:2014-02-14 13:16:33
【问题描述】:

全部,

虽然我对 SQL 并不陌生,但已经有很长一段时间了,而且我对 Oracle 还是很陌生。另请注意,我知道我这样做的方式可能是最慢的解决方案,但正如我所说,我是新手,它适用于我需要测试的 1000 个左右的项目。问题是它确实有效,所以我想扩展它以测试 1,000,000 个,我知道这是一头驴而不是赛马。

有一个小任务要“标准化”公司名称列表,所以我创建了一个包含已知缩写的表格

Col1        Col2    Col3    Col4    ….  Col14

Company Co  Cmpy
Limited     Ltd Lim LMT
Etc

然后我编写了一个函数来选择这个表到一个游标中,并为每个发送给它的名称循环遍历它,并将任何“NON”标准缩写替换为 Col 1 中的缩写,使用以下代码:

我一直在考虑的是重写代码(显然)并将其放入一个包中,因为我相信与现在相比,这应该会带来相当大的速度和清晰度。

我遇到的问题并不是真正改进函数的循环部分,而是我需要从调用此函数的 Select 中做些什么,以确保它只加载一次 STD_GBR_CO_SUFFIX 并重新使用光标。

我一直在阅读 O'Reilly 的一些内容,我认为我想做的事情是可能的,我只是不知道如何做。

我想我需要把它放在一个包中,并使用一个 REF 光标,BULK LOAD 看起来是一个竞争者,但我读得越多,我就越困惑。 如果那里有人可以指出我正确的方向,我可以从那里继续。我不希望有人只写解决方案我想学习如何更好地做到这一点而不是为我完成。

提前感谢大家的帮助。

所以如果我做一个

Select Standardise_Company_Suffix(company_name) AS STD_Company_Name
From VERY_LARGE_TABLE

CREATE OR REPLACE Function Standardise_Company_Suffix(Co_Name IN VARCHAR2)
RETURN varchar2 IS
  stg_Co_Name varchar2(400 byte);
  fmt_S varchar2(20 byte);
  fmt_E varchar2(20 byte);
  parse_Str varchar2(2400 byte);
  parse_char varchar2(4 byte);

CURSOR C1 IS
-- Populate Cursor with Table of Suffixes
select * from STD_GBR_CO_SUFFIX;

BEGIN

parse_char := "s";

Fmt_S := '(^|\'||parse_char||')';

Fmt_E := '(\'||parse_char||'|$)';

stg_Co_Name := upper(co_name);

parse_str := ' ';

   FOR c1pass IN C1 LOOP

   parse_str :='';

   If c1pass.column14 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column14)||Fmt_E;   End If;

   If c1pass.column13 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column13)||Fmt_E;   End If;

   If c1pass.column12 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column12)||Fmt_E;   End If;

   If c1pass.column11 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column11)||Fmt_E;   End If;

   If c1pass.column10 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column10)||Fmt_E;   End If;

   If c1pass.column9 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column9)||Fmt_E;     End If;

   If c1pass.column8 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column8)||Fmt_E;     End If;

   If c1pass.column7 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column7)||Fmt_E;     End If;

   If c1pass.column6 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column6)||Fmt_E;     End If;

   If c1pass.column5 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column5)||Fmt_E;     End If;

   If c1pass.column4 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column4)||Fmt_E;     End If;

   If c1pass.column3 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column3)||Fmt_E;     End If;

   If c1pass.column2 is not null then parse_str := parse_str||'|'||Fmt_S||Upper(c1pass.column2)||Fmt_E;     End If;

   Parse_str := substr(parse_str,2);

   If regexp_instr(stg_Co_Name,parse_str) <> 0 Then

            stg_Co_Name := regexp_REPLACE(stg_Co_Name,parse_str,
                            ' $'||UPPER(c1pass.column1||'$ '));

   Else

            stg_Co_Name := stg_Co_Name;

    End if;    

 END LOOP;

return stg_Co_Name; 

End;

/

【问题讨论】:

    标签: oracle function plsql ref-cursor


    【解决方案1】:

    这并不是您真正要求的那种方向,但如果可能的话,我会忘记该功能 - 这至少会导致上下文切换,但可能会产生相当多的其他开销 - 并尝试做一些事情在本机 SQL 中。我还会更改您的查找表,因为具有多列不是很灵活。我会在每一行中有一个标准缩写对:

    STD_VALUE            ABBREVIATION
    -------------------- ------------
    Bar                  Ba           
    Company              Co           
    Company              Cmpy         
    Foo                  Fo           
    Limited              Ltd          
    Limited              Lim          
    Limited              LMT          
    

    这似乎工作,至少对于一些基本的测试用例,但你需要在 11gR2 上,所以你有 the listagg functionrecursive subquery factoring(又名递归 CTE 或递归 with)。我非常赞赏您不希望有人只写解决方案的观点,但我很高兴弄清楚它,浪费它似乎很可惜。我试图解释下面发生了什么;希望你仍然可以从中吸取教训。而且它可能需要一些修改,所以你需要理解它......

    with p as (
      select ' $'|| upper( std_value) || '$ ' as replace_string,
        listagg('(^|\s)' || abbreviation || '(\s|$)', '|')
          within group (order by length(abbreviation) desc) as pattern,
        rank() over (order by std_value) as rn
      from std_gbr_co_suffix
      group by std_value
    ),
    r (company_name, replacements, rn, std_company_name) as (
      select company_name, 0, 0, company_name from very_large_table
      union all
      select r.company_name, r.replacements + 1, p.rn, 
        regexp_replace(r.std_company_name, p.pattern, p.replace_string)
      from r
      join p on p.rn > r.rn and regexp_like(r.std_company_name, p.pattern)
    ),
    t as (
      select company_name, std_company_name,
        rank() over (partition by company_name order by replacements desc) as rn
      from r
    )
    select company_name, std_company_name
    from t
    where rn = 1;
    

    SQL Fiddle demo 带有一些测试用例;有兴趣了解它在正确性和性能方面如何与您的 1000 多个测试项目进行比较。

    稍微分解一下,第一个 CTE p

    p as (
      select ' $'|| upper( std_value) || '$ ' as replace_string,
        listagg('(^|\s)' || abbreviation || '(\s|$)', '|')
          within group (order by length(abbreviation) desc) as pattern,
        rank() over (order by std_value) as rn
      from std_gbr_co_suffix
      group by std_value
    )
    

    ...为每个不同的std_value(原始表中的column1)生成模式和替换字符串:

    REPLACE_STRING                   RN PATTERN                                          
    ------------------------ ---------- --------------------------------------------------
     $BAR$                            1 (^|\s)Ba(\s|$)                                     
     $COMPANY$                        2 (^|\s)Cmpy(\s|$)|(^|\s)Co(\s|$)                    
     $FOO$                            3 (^|\s)Fo(\s|$)                                     
     $LIMITED$                        4 (^|\s)LMT(\s|$)|(^|\s)Lim(\s|$)|(^|\s)Ltd(\s|$)    
    

    第二个 CTE r 是递归的:

    r (company_name, replacements, rn, std_company_name) as (
      select company_name, 0, 0, company_name from very_large_table
      union all
      select r.company_name, r.replacements + 1, p.rn, 
        regexp_replace(r.std_company_name, p.pattern, p.replace_string)
      from r
      join p on p.rn > r.rn and regexp_like(r.std_company_name, p.pattern)
    )
    

    它从作为锚成员的表中的原始值开始,然后递归地应用来自p 的每个模式。 (连接中的rn 是为了阻止它以两种方式应用模式,以防原始名称匹配多个;您将做比必要的工作更多的工作并最终得到重复的结果)。对于我给出的虚拟数据:

    COMPANY_NAME                   REPLACEMENTS         RN STD_COMPANY_NAME                                 
    ------------------------------ ------------ ---------- --------------------------------------------------
    Oracle Co                                 0          0 Oracle Co                                          
    Oracle Ltd                                0          0 Oracle Ltd                                         
    Oracle Ltd.                               0          0 Oracle Ltd.                                        
    Oracle Co Ltd.                            0          0 Oracle Co Ltd.                                     
    Oracle Co Ltd Cmpy LMT                    0          0 Oracle Co Ltd Cmpy LMT                             
    Oracle Co                                 1          2 Oracle $COMPANY$                                   
    Oracle Ltd                                1          4 Oracle $LIMITED$                                   
    Oracle Co Ltd Cmpy LMT                    1          2 Oracle $COMPANY$ Ltd $COMPANY$ LMT                 
    Oracle Co Ltd Cmpy LMT                    1          4 Oracle Co $LIMITED$ Cmpy $LIMITED$                 
    Oracle Co Ltd.                            1          2 Oracle $COMPANY$ Ltd.                              
    Oracle Co Ltd Cmpy LMT                    2          4 Oracle $COMPANY$ $LIMITED$ $COMPANY$ $LIMITED$     
    

    您可以看到一个原始名称可以有多个替换,以及原始值本身;具体在这里:

    Oracle Co Ltd Cmpy LMT                    0          0 Oracle Co Ltd Cmpy LMT                             
    Oracle Co Ltd Cmpy LMT                    1          2 Oracle $COMPANY$ Ltd $COMPANY$ LMT                 
    Oracle Co Ltd Cmpy LMT                    1          4 Oracle Co $LIMITED$ Cmpy $LIMITED$                 
    Oracle Co Ltd Cmpy LMT                    2          4 Oracle $COMPANY$ $LIMITED$ $COMPANY$ $LIMITED$     
    

    replacements 值跟踪有多少正则表达式匹配,所以如果有多个匹配,那么我们可以挑选出最多的 - 对于同时具有 CompanyLimited 模式的原始值,每个都有一行单个替换,一个同时应用(同样,只有一个,因为rn 检查)。

    最终的 CTE t(是的,我很难给出这些有意义的名称)只是确定哪一行的替换最多;每个原件都有一行排名为1

    t as (
      select company_name, std_company_name,
        rank() over (partition by company_name order by replacements desc) as rn
      from r
    )
    

    ...最后我们排除所有没有排名第一的东西,剩下的就是:

    COMPANY_NAME                   STD_COMPANY_NAME                                 
    ------------------------------ --------------------------------------------------
    Oracle Co                      Oracle $COMPANY$                                   
    Oracle Co Ltd Cmpy LMT         Oracle $COMPANY$ $LIMITED$ $COMPANY$ $LIMITED$     
    Oracle Co Ltd.                 Oracle $COMPANY$ Ltd.                              
    Oracle Ltd                     Oracle $LIMITED$                                   
    Oracle Ltd.                    Oracle Ltd.                                        
    

    你也许可以用 model clause 做点什么,但我不是很熟悉...


    如果您确实想坚持使用某个功能,如果只是为了了解如何做到这一点,那么我认为您不需要引用光标,但批量收集不会受到伤害。你可以在你的包中声明一个表类型,你填充一次(每个会话),然后在你的函数中应用 regexp_replace 调用时引用它。

    再次可能比您真正想要的更完整,但是没有太多要删除的内容...除非您现在停下来,然后继续阅读 PL/SQL 集合以及使包成为阶段性的原因,在 @ 987654325@.

    您的包规范可以声明一个记录类型来保存正则表达式模式和替换字符串,以及这些记录的表。而且,至关重要的是,该表类型的包级变量。这将使包有状态,并且状态将与您的会话相关联 - 运行它的不同人将拥有该表变量的自己的副本。

    create or replace package p42 as
      type regex_rec_type is record(pattern varchar2(4000),
        replace_string varchar2(50));
      type regex_tab_type is table of regex_rec_type;
    
      regexes regex_tab_type;
    
      function standardise_company_suffix(company_name in varchar2)
      return varchar2;
    end p42;
    /
    

    包体分为两部分;你的函数和初始化块:

    create or replace package body p42 as
      function standardise_company_suffix(company_name in varchar2)
      return varchar2 is
        std_company_name varchar2(4000);
      begin
        std_company_name := company_name;
        for i in regexes.first..regexes.last loop
          std_company_name := regexp_replace(std_company_name,
            regexes(i).pattern, regexes(i).replace_string);
        end loop;
        return std_company_name;
      end standardise_company_suffix;
    
    begin
      select listagg('(^|\s)' || abbreviation || '(\s|$)', '|')
          within group (order by length(abbreviation) desc),
        ' $'|| upper( std_value) || '$ '
      bulk collect into regexes
      from std_gbr_co_suffix
      group by std_value;
    end p42;
    /
    

    块优先;它运行与我之前使用的相同的 listagg 查询,在同一个值对表上我必须替换你的,所以它得到相同的模式和替换字符串。这被放入包规范中声明的regexes 变量中。当第一次引用包时,每个会话都会发生一次。

    该函数从原始公司名称开始,循环遍历正则表达式记录的集合,依次应用每一条记录。然后它返回最终结果。非常简单,但我会让您查看 PL/SQL 参考以确切了解它在做什么。我没有打扰regexp_instr,因为我认为仅应用替换会更昂贵,但可能值得尝试,因为我不确定。

    使用相同的虚拟数据:

    select company_name,
      p42.standardise_company_suffix(company_name) as std_company_name
    from very_large_table;
    
    COMPANY_NAME                   STD_COMPANY_NAME                                 
    ------------------------------ --------------------------------------------------
    Oracle Co                      Oracle $COMPANY$                                   
    Oracle Ltd                     Oracle $LIMITED$                                   
    Oracle Ltd.                    Oracle Ltd.                                        
    Oracle Co Ltd.                 Oracle $COMPANY$ Ltd.                              
    Oracle Co Ltd Cmpy LMT         Oracle $COMPANY$ $LIMITED$ $COMPANY$ $LIMITED$     
    

    尽管这方面的代码可以说要简单得多,但我希望这在大型数据集上会慢得多,但至少这应该可以最大限度地减少开销,就像您的目标一样。您仍然会有上下文切换,但您现在不会在 PL/SQL 中切换回 SQL,这将有所帮助。我再次对与您现在拥有的性能比较以及递归 CTE 版本感兴趣。

    【讨论】:

    • Alex,首先感谢您查看此内容,其次感谢您提供了如此多的描述。我将把它拿走并解决它,因此可能需要一天左右的时间才能发布更大的感谢或一大堆问题,无论哪种方式都非常感谢您的努力。
    • 亚历克斯,我怎么理解。
    • Alex,我收到了您提供的查询和解释,我现在需要做的就是弄清楚如何将它与超过 3Mill 记录一起使用。目前在 10K 条记录上运行大约需要 2 分钟,产生大约 27.5K 条记录,分区将其排序出来。我有大约 120 个缩写字符串要测试,所以这不是一项小任务。这是一个更简洁的解决方案,但如果可以的话,我现在想和你讨论速度。顺便说一句,我从你的帖子中学到了很多东西,谢谢。!
    • @Quimbyf - 2 分钟听起来很慢,除非您针对所有 300 万行运行并过滤到 10k。尽管正则表达式无论如何都可能很慢。对于相同的数据,您的原始方法和包版本需要多长时间?开始认为这个包最终可能会更快,并且使用更少的内存。
    • 我还没有尝试过 Package 方法,而且我的原始版本无法使用。我今天会试试这个包。我做了一个选择 rownum
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-02-08
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-23
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多