3

我有一个表,其中包含日期字段(让它成为date s_date)和描述字段(varchar2(n) desc)。我需要的是编写一个脚本(或单个查询,如果可能的话),它将解析该字段,如果它包含一个有效的 oracle 日期,那么desc它将切断这个日期并更新.s_datenull

但是还有一个条件 -中的日期必须恰好出现一次desc。如果有 0 或 >1 - 不应更新任何内容。

当我使用正则表达式想出这个非常丑陋的解决方案时:

----------------------------------------------

create or replace function to_date_single( p_date_str in varchar2 )
    return date
is
    l_date date;
    pRegEx varchar(150);
    pResStr varchar(150); 
begin
    pRegEx := '((0[1-9]|[12][0-9]|3[01])[.](0[1-9]|1[012])[.](19|20)\d\d)((.|\n|\t|\s)*((0[1-9]|[12][0-9]|3[01])[.](0[1-9]|1[012])[.](19|20)\d\d))?';
    pResStr := regexp_substr(p_date_str, pRegEx);
    if not (length(pResStr) = 10)
    then return null;
    end if;
    l_date := to_date(pResStr, 'dd.mm.yyyy');
    return l_date;
exception
    when others then return null;
end to_date_single;

----------------------------------------------

update myTable t
set t.s_date = to_date_single(t.desc)
where t.s_date is null;

----------------------------------------------

但它的工作速度非常慢(每条记录超过一秒,我需要更新大约 30000 条记录)。是否有可能以某种方式优化功能?也许这是没有正则表达式的方法?还有其他想法吗?

任何建议表示赞赏:)

编辑:

好的,也许它对某人有用。以下正则表达式会根据一个月的天数执行有效日期 (DD.MM.YYYY) 的检查,包括闰年的检查:

(((0[1-9]|[12]\d|3[01])\.(0[13578]|1[02])\.((19|[2-9]\d)\d{2}))|((0[1-9]|[12]\d|30)\.(0[13456789]|1[012])\.((19|[2-9]\d)\d{2}))|((0[1-9]|1\d|2[0-8])\.02\.((19|[2-9]\d)\d{2}))|(29\.02\.((1[6-9]|[2-9]\d)(0[48]|[2468][048]|[13579][26])|((16|[2468][048]|[3579][26])00))))

我将它与@David建议的查询一起使用(请参阅接受的答案),但我已经尝试select而不是update(因此每行少1个正则表达式,因为我们不这样做regexp_substr)只是为了“基准测试”目的。

数字在这里可能不会说明太多,因为这完全取决于硬件、软件和特定的数据库设计,但我花了大约 2 分钟来选择 36K 条记录。更新会比较慢,但我认为这仍然是一个合理的时间。

4

2 回答 2

4

我会按照单个更新查询的方式对其进行重构。

在 where 子句中使用两个 regexp_instr() 调用来查找第一次出现匹配而第二次没有匹配的行,并使用 regexp_substr() 提取匹配字符以进行更新。

update my_table
set    my_date = to_date(regexp_subtr(desc,...),...)
where  regexp_instr(desc,pattern,1,1) > 0 and
       regexp_instr(desc,pattern,1,2) = 0

您可能会获得更好的性能:

update my_table
set    my_date = to_date(regexp_subtr(desc,...),...)
where  case regexp_instr(desc,pattern,1,1)
         when 0 then 'N'
         else case regexp_instr(desc,pattern,1,2)
           when 0 then 'Y'
           else 'N'
         end
       end = 'Y'

...因为它仅在第一个非零时评估第二个正则表达式。第一个查询也可能这样做,但优化器可能会选择首先评估第二个谓词,因为它是一个相等条件,假设它更具选择性。

或者重新排序 Case 表达式可能会更好——这是一种难以判断的权衡,并且可能非常依赖于数据。

于 2013-06-13T22:53:03.197 回答
1

我认为没有办法改进这项任务。实际上,为了达到您想要的效果,它应该变得更慢。您的正则表达式与月份范围之外的文本相31.02.2013匹配31.04.2013。如果你把年份放在游戏中,它会变得更糟。29.02.2012是有效的,但29.02.2013不是。这就是为什么您必须测试结果是否是有效日期的原因。由于没有完整的正则表达式,因此您真的必须通过 PLSQL 来完成。

在您的to_date_single函数中,当发现无效日期时返回 null。但这并不意味着文本中不会有其他有效日期。因此,您必须继续尝试,直到找到两个有效日期或到达文本末尾:

create or replace function fn_to_date(p_date_str in varchar2) return date is
    l_date date;
    pRegEx varchar(150);
    pResStr varchar(150);
    vn_findings number;
    vn_loop number;
begin
    vn_findings := 0;
    vn_loop := 1;
    pRegEx := '((0[1-9]|[12][0-9]|3[01])[.](0[1-9]|1[012])[.](19|20)\d\d)';
    loop
        pResStr := regexp_substr(p_date_str, pRegEx, 1, vn_loop);
        if pResStr is null then exit; end if;
        begin
           l_date := to_date(pResStr, 'dd.mm.yyyy');
           vn_findings := vn_findings + 1;

           -- your crazy requirement :)
           if vn_findings = 2 then
              return null;
           end if;
        exception when others then
          null;
         end;
         -- you have to keep trying :)
         vn_loop := vn_loop + 1;
    end  loop;
    return l_date;
end;

一些测试:

select fn_to_date('xxxx29.02.2012xxxxx')            c1 --ok
     , fn_to_date('xxxx29.02.2012xxx29.02.2013xxx') c2 --ok, 2nd is invalid
     , fn_to_date('xxxx29.02.2012xxx29.02.2016xxx') c2 --null, both are valid    
from dual

因为无论如何你都必须尝试和出错,一个想法是使用更简单的正则表达式。类似的东西\d\d[.]\d\d[.]\d\d\d\d就足够了。当然,这取决于您的数据。使用@David 的想法,您可以过滤行数以应用您的to_date_single函数(因为它很慢),但单独的正则表达式不会满足您的要求:

update my_table
set    my_date = fn_to_date( )
where  regexp_instr(desc,patern,1,1) > 0
于 2013-06-13T23:54:34.327 回答