【问题标题】:Oracle Function to return similarity between stringsOracle函数返回字符串之间的相似性
【发布时间】:2015-10-23 17:32:03
【问题描述】:

我有一个有趣的问题,想知道 oracle 是否有内置函数来执行此操作,或者我需要在 plsql 中找到一种快速的方法。

取2个字符串:

  s1 = 'abc def hijk'
  s2 = 'abc def iosk'

该函数需要返回abc def,因为到那时为止字符串完全相同。

另一个例子:

  s1 = 'abc def hijk www'
  s2 = 'abc def iosk www'

函数需要返回abc def

我能想到的唯一方法是遍历 string1 并将每个字符与 substr() 再次比较字符串 2 的 substr。

只是想知道甲骨文是否有内置的东西。性能非常重要。

【问题讨论】:

    标签: string oracle performance plsql


    【解决方案1】:

    您应该检查包UTL_MATCH 是否有类似的功能,但要准确获取您的请求,您必须编写自己的函数。

    对公共子字符串长度的二分查找对于长字符串提供了良好的性能。

     create or replace function ident_pfx(str1 varchar2, str2 varchar2) return varchar2
     as
      len_beg PLS_INTEGER;
      len_end PLS_INTEGER;
      len_mid PLS_INTEGER;
      len_result PLS_INTEGER; 
     begin
        if str1 is null or str2 is null then return null; end if;
      -- 
        len_result := 0;
        len_beg := 0;
        len_end := least(length(str1),length(str2));
    
         LOOP  
         BEGIN
           -- use binary search for the common substring length
           len_mid := ceil((len_beg + len_end) / 2);
    
           IF (substr(str1,1,len_mid) = substr(str2,1,len_mid))
           THEN
              len_beg := len_mid; len_result := len_mid;
           ELSE
              len_end := len_mid;
           END IF;
          END;
    
          IF (len_end - len_beg) <= 1 THEN
            -- check last character
            IF (substr(str1,1,len_end) = substr(str2,1,len_end))
            THEN
             len_result := len_end;
            END IF;       
            EXIT ; 
          END IF;       
         END LOOP;
      return substr(str1,1,len_result);
     end;
     /
    
    
     select ident_pfx('abc def hijk www','abc def iosk www') ident_pfx from dual;
    
     abc def 
    

    【讨论】:

      【解决方案2】:

      如果“性能非常重要”,则应避免子字符串上的“循环”。

      这里是使用 XOR 的替代方法(由 @EvilTeach 提出)。

       with string_transform as  (
           select 'abc def hijk www' str1, 'abc def iosk www' str2 from dual
       ),
       str as (
       select 
        str1, str2,
        -- add suffix to handle nulls and identical strings
        -- calculate XOR
        utl_raw.bit_xor(utl_raw.cast_to_raw(str1||'X'),utl_raw.cast_to_raw(str2||'Y')) str1_xor_str2
       from string_transform 
       ), str2 as (
       select 
         str1, str2,
         str1_xor_str2,
         -- replace all non-identical characters (not 00) with 2D = '-'
         utl_raw.translate(str1_xor_str2,
                           utl_raw.translate(str1_xor_str2,'00','01'),
                           utl_raw.copies('2D',length(str1_xor_str2))) xor1
       from str
       ), str3 as (
       select 
         str1, str2,
         -- replace all identical characters (00) with 2B (= '+') and cast back to string
         utl_raw.cast_to_varchar2(utl_raw.translate(xor1,'00','2B')) diff
         -- diff = ++++++++---+++++ (+ means identical position; - difference)
       from str2
       )
       select str1, str2, 
        -- remove the appended suffix character
        substr(diff,1,length(diff)-1) diff,
        -- calculate the length of the identical prefix
        instr(diff,'-')-1 same_prf_length
       from str3 
       ;
      

      基本上这两个字符串都首先转换为 RAW 格式。 XOR 将相同的字节(字符)设置为 00。使用 translate 时,相同的字节将转换为“+”,所有其他字节都转换为“-”。 相同的前缀长度是字符串中第一个“-”的位置减一。 从技术上讲,一个(不同的)后缀字符被添加到两个字符串中以处理 NULL 和相同的字符串。

      注意,如果字符串长于2000,则必须添加一些额外的处理 由于 UTL_RAW.CAST_TO_VARCHAR2 的限制。

      【讨论】:

        【解决方案3】:

        重新阅读您的问题后,这就是您真正想要的:

        with cte1 as  (
            select 1 id, 'abc def hijk www' str from dual
            union all
            select 2 id, 'abc def iosk www' str from dual
        ), num_gen as (
            -- a number generator up to the minimum length of the strings
            SELECT level num
            FROM dual t
            CONNECT BY level <= (select min(length(str)) from cte1)
        ), cte2 as (
            -- build substrings of increasing length
            select id, num_gen.num, substr(cte1.str, 1, num_gen.num) sub
            from cte1
            cross join num_gen
        ), cte3 as (
            -- self join to check if the substrings are equal
            select x1.num, x1.sub sub1, x2.sub sub2
            from cte2 x1
            join cte2 x2 on (x1.num = x2.num and x1.id != x2.id)
        ), cte4 as (
            -- select maximum string length
            select max(num) max_num
            from cte3
            where sub1 = sub2
        )
            -- finally, get the substring with the max length
            select cte3.sub1
            from cte3
            join cte4 on (cte4.max_num = cte3.num)
            where rownum = 1
        

        本质上,这就是您在 pl/sql 中要做的事情:构建长度增加的子字符串并在它们不再匹配的点停止。

        【讨论】:

          【解决方案4】:

          另一个可能的解决方案是使用 XOR。 如果将两个字符串异或在一起,则无论两个字符串匹配,结果都应该有一个 NUL 字节。

          XOR 不是本地运算符,但我很确定其中一个库支持它。

          【讨论】:

            【解决方案5】:

            我怀疑是否有一些内置的 SQL 函数,但只能使用正则表达式在 SQL 中完成:

            with cte1 as  (
                select 1 id, 'abc def hijk www' str from dual
                union all
                select 2 id, 'abc def iosk www' str from dual
            ), cte2 as (
                SELECT distinct id, trim(regexp_substr(str, '[^ ]+', 1, level)) str
                FROM cte1 t
                CONNECT BY instr(str, ' ', 1, level - 1) > 0
            )
            select distinct t1.str
            from cte2 t1
            join cte2 t2 on (t1.str = t2.str and t1.id != t2.id)
            

            我没有做过任何性能测试,但我的经验告诉我,这很可能比任何 pl/sql 解决方案都快,因为您完全避免了上下文切换。

            【讨论】:

            • 我同意 connect by 将是一个很好的解决方案。您发布的查询返回 3 行“abc”、“def”和“www”。我只需要一个字符串'abc def',它需要比较每个字符而不是按单词标记。我认为这是正确的概念,需要稍作调整。
            • 是的,在重新阅读您的问题后,我注意到您想要一些不同的东西:-D 然后我会发布另一个答案。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2011-07-02
            • 2019-01-29
            • 1970-01-01
            • 2019-04-18
            相关资源
            最近更新 更多