Oracle SQL 获取括号中的最后一个字符串(也可能包含括号)

Posted

技术标签:

【中文标题】Oracle SQL 获取括号中的最后一个字符串(也可能包含括号)【英文标题】:Oracle SQL Get Last String in Parentheses (may include parentheses inside too) 【发布时间】:2017-01-05 22:19:02 【问题描述】:

我正在使用这个查询:

SELECT strain.id, TRIM(SUBSTR(strain.name, 1, INSTR(strain.name, '[')-1)) AS name
FROM species_strain strain

上面的查询给了我如下内容:

id    name
-----------------------------------------------
100   CfwHE3 (HH3d) Jt1 (CD-1)
101   4GSdg-3t 22sfG/J (mdx (fq) KO)
102   Yf7mMjfel 7(tm1) (SCID)
103   B29fj;jfos x11 (tmos (line x11))
104   B29;CD (Atm (line G5))
105   Ifkso30 jel-3
106   13GupSip (te3x) Blhas/J           --------> I don't want to get (te3x)

我需要一个正则表达式,它可以为我提供最后一组括号的内容(可能或可能不在里面包含一组或多组括号) - 这需要在字符串的末尾。如果它在字符串的中间,那么我不想要它。

我想要得到的是以下内容:

(CD-1)
(mdx (fq) KO)
(SCID)
(tmos (line x11))
(Atm (line G5))

因此,如果我复制并粘贴我的整个查询,我会得到这个,但这并没有考虑到里面的括号:

SELECT DISTINCT REGEXP_SUBSTR(strain.name, '\(.*?\)', 1, REGEXP_COUNT(strain.name, '\(.*?\)')) AS name
FROM (
  SELECT strain.id, TRIM(SUBSTR(strain.name, 1, INSTR(strain.name, '[')-1)) AS name
  FROM species_strain strain
) strain
WHERE INSTR(strain.name, '(', 1, 1) > 0

查询以某种方式有效,但如果我在主括号内得到另一组括号,它会中断并且我会丢失一些数据。它返回类似:

(CD-1)
(mdx (fq)          ---------> missing KO)
(SCID)
(tmos (line x11)   ---------> missing )
(Atm (line G5)     ---------> missing )

附加要求

我确实忘记提到我需要的括号集应该在末尾。如果它后面还有其他字符,那么我不想要它。我在示例中添加了另一行。

【问题讨论】:

Oracle 正则表达式不如 PCRE 或 .NET 强大,因此,您只能使用预定义的嵌套级别。比如说,如果你只有 1 个嵌套括号,你可以使用'\([^()]*(\([^()]*\)[^()]*)*\)'。否则,您需要一些解析功能。 @WiktorStribiżew 所以如果我在 Oracle 中有Test (get (me) (too)),我将无法获得(get (me) (too)) 不,比如Test (get (me) ((me), too))。您只是无法将任意递归子模式与 Oracle 正则表达式匹配。我提供的模式适用于 1 个嵌套级别,它可以增强为支持 2 个级别,并且比现在更不可读。 @WiktorStribiżew 我认为这很好。谢谢! 经过一番努力,我找到了一个涵盖所有可能情况的解决方案(我相信),不需要编写函数,根本不使用正则表达式,不需要任何解析功能,并且不会运行数百行。一般来说,最好不要对“不可能”的事情发表意见,除非有人确定;这样的声明可能会让其他人甚至不敢尝试。 【参考方案1】:

以下解决方案使用纯 SQL(无过程/函数);它适用于任何级别的嵌套括号和“同级”括号;并且只要输入为null,它就会返回null,或者它不包含任何右括号,或者它包含一个右括号但最右边的括号是不平衡的(没有左括号,在左边这个最右边的括号,所以这对是平衡的)。

在最底部,我将显示仅当最右边的括号是输入字符串中的最后一个字符时返回“结果”所需的微小调整,否则返回 null。 这是 OP 修改后的要求

我创建了更多的输入字符串用于测试。特别注意id = 156,在这种情况下,智能解析器不会“计算”字符串文字中的括号,或者以其他方式不是“正常”括号。我的解决方案并没有走那么远 - 它对所有括号都一视同仁。

策略是从最右边括号的位置开始(如果至少有一个),然后从那里逐步向左移动,只经过左括号(如果有的话)和测试括号是否平衡。这很容易通过比较删除所有) 后的“测试字符串”的长度与删除所有( 后的长度来完成。

奖励:我能够编写没有正则表达式的解决方案,只使用“标准”(非正则表达式)字符串函数。这应该有助于保持快速。

查询

with
     species_str ( id, name) as (
       select 100, 'CfwHE3 (HH3d) Jt1 (CD-1)'         from dual union all
       select 101, '4GSdg-3t 22sfG/J (mdx (fq) KO)'   from dual union all
       select 102, 'Yf7mMjfel 7(tm1) (SCID)'          from dual union all
       select 103, 'B29fj;jfos x11 (tmos (line x11))' from dual union all
       select 104, 'B29;CD (Atm (line G5))'           from dual union all
       select 105, 'Ifkso30 jel-3'                    from dual union all
       select 106, '13GupSip (te3x) Blhas/J'          from dual union all
       select 151, ''                                 from dual union all
       select 152, 'try (this (and (this))) ok?'      from dual union all
       select 153, 'try (this (and (this)) ok?)'      from dual union all
       select 154, 'try (this (and) this (ok))?'      from dual union all
       select 155, 'try (this (and (this)'            from dual union all
       select 156, 'right grouping (includging ")")'  from dual union all
       select 157, 'try this out ) ( too'             from dual
     ),
     prep ( id, name, pos ) as (
       select id, name, instr(name, ')', -1)
       from   species_str
     ),
     rec ( id, name, str, len, prev_pos, new_pos, flag ) as (
       select  id, name, substr(name, 1, instr(name, ')', -1)),
               pos, pos - 1, pos, null
         from  prep
       union all
       select  id, name, str, len, new_pos,
               instr(str, '(',  -(len - new_pos + 2)),
               case when length(replace(substr(str, new_pos), '(', '')) =
                         length(replace(substr(str, new_pos), ')', ''))
                    then 1 end
         from  rec
         where prev_pos > 0 and flag is null
     )
select   id, name, case when flag = 1 
              then substr(name, prev_pos, len - prev_pos + 1) end as target
from     rec
where    flag = 1 or prev_pos <= 0 or name is null
order by id;

输出

        ID NAME                             TARGET                         
---------- -------------------------------- --------------------------------
       100 CfwHE3 (HH3d) Jt1 (CD-1)         (CD-1)                          
       101 4GSdg-3t 22sfG/J (mdx (fq) KO)   (mdx (fq) KO)                   
       102 Yf7mMjfel 7(tm1) (SCID)          (SCID)                          
       103 B29fj;jfos x11 (tmos (line x11)) (tmos (line x11))               
       104 B29;CD (Atm (line G5))           (Atm (line G5))                 
       105 Ifkso30 jel-3                                                    
       106 13GupSip (te3x) Blhas/J          (te3x)                          
       151                                                                  
       152 try (this (and (this))) ok?      (this (and (this)))             
       153 try (this (and (this)) ok?)      (this (and (this)) ok?)         
       154 try (this (and) this (ok))?      (this (and) this (ok))          
       155 try (this (and (this)            (this)                          
       156 right grouping (includging ")")                                  
       157 try this out ) ( too                                             

 14 rows selected 

为满足 OP 的(已编辑)要求所需的更改

在最外层的select(代码底部),我们有case when flag = 1 then... 来定义target 列,添加如下条件:

... , case when flag = 1 and len = length(name) then ...

输出进行此修改:

        ID NAME                             TARGET                         
---------- -------------------------------- --------------------------------
       100 CfwHE3 (HH3d) Jt1 (CD-1)         (CD-1)                          
       101 4GSdg-3t 22sfG/J (mdx (fq) KO)   (mdx (fq) KO)                   
       102 Yf7mMjfel 7(tm1) (SCID)          (SCID)                          
       103 B29fj;jfos x11 (tmos (line x11)) (tmos (line x11))               
       104 B29;CD (Atm (line G5))           (Atm (line G5))                 
       105 Ifkso30 jel-3                                                    
       106 13GupSip (te3x) Blhas/J                                          
       151                                                                  
       152 try (this (and (this))) ok?                                      
       153 try (this (and (this)) ok?)      (this (and (this)) ok?)         
       154 try (this (and) this (ok))?                                      
       155 try (this (and (this)            (this)                          
       156 right grouping (includging ")")                                  
       157 try this out ) ( too                                             

 14 rows selected 

【讨论】:

哇,谢谢。但是有一个问题:正确的分组是什么?为什么不匹配includging ")"?在旁注中,实际上没有必要担心括号不平衡,因为我们为此创建了诊断程序。有时可能由于数据输入错误而发生这种情况,但我们将始终更正这一点,因为成对的括号是该字符串的必须项。 还有这个:select id, name, substr(name, 1, instr(name, ')', -1)), pos, pos - 1, pos, null。为什么需要选择pos 两次?这是故意的吗? 关于id=156 - 如果你从右到左阅读,你会看到最后一个右括号不平衡 - 在这种情况下应该显示什么?我在帖子中解释过:如果最后一个右括号是字符串中的第二个,但整个输入中只有一个左括号,那么这是一个“不平衡括号”错误(异常)。在这种情况下,我选择将“目标”显示为 NULL;在这种情况下,我本可以编写查询以返回异常,但这通常不会在普通 SQL 中完成。如果我正在编写一个函数,这肯定是一个已处理的异常。 关于pos被选中两次:在递归查询中,我将len(字符串的长度直到最后一个右括号)向前移动,我也从右向左移动找到第一个给我平衡对的左括号。在第一次迭代中,len 和这个pos(ition) 重合;后来,len 保持不变,但new_pos 继续向左移动。是的,在第一次迭代中(递归查询的所谓“锚”分支),pos 的重复选择是有意的,也是必要的;它用于“初始化”两个不同的“变量”。 很抱歉再次提出这个问题,但是当我将STOCK Gt(ROSA26)Sor&lt;sp&gt;tm2(CAG-CHRM3*,-mCitrine)Ute&lt;sp/&gt;/J 添加到您的测试用例时,解决方案失败了。当它应该返回NULL时,它返回了(CAG-CHRM3*,-mCitrine)【参考方案2】:

这适用于任何深度的嵌套括号:

Oracle 设置

CREATE TABLE species_strain ( id, name ) AS
SELECT 100,   'CfwHE3 (HH3d) Jt1 (CD-1)' FROM DUAL UNION ALL
SELECT 101,   '4GSdg-3t 22sfG/J (mdx (fq) KO)' FROM DUAL UNION ALL
SELECT 102,   'Yf7mMjfel 7(tm1) (SCID)' FROM DUAL UNION ALL
SELECT 103,   'B29fj;jfos x11 (tmos (line x11))' FROM DUAL UNION ALL
SELECT 104,   'B29;CD (Atm (line G5))' FROM DUAL UNION ALL
SELECT 105,   'Ifkso30 jel-3' FROM DUAL UNION ALL
SELECT 106,   'data (1 (2 (333 (444) 3 (4 (5) 4) 3) 2) 1)' FROM DUAL;

查询 1

WITH tmp ( id, name, pos, depth ) AS (
  SELECT id,
         name,
         LENGTH( name ),
         1
  FROM   species_strain
  WHERE  SUBSTR( name, -1 ) = ')'
UNION ALL
  SELECT id,
         name,
         pos - 1,
         depth + CASE SUBSTR( name, pos - 1, 1 )
                 WHEN '(' THEN -1
                 WHEN ')' THEN +1
                          ELSE 0 END
  FROM   tmp
  WHERE  (  depth > 1
         OR SUBSTR( name, pos -1, 1 ) <> '(' )
  AND    pos > 0
)
SELECT id,
       MAX( name ) AS name,
       MIN( SUBSTR( name, pos - 1 ) ) KEEP ( DENSE_RANK FIRST ORDER BY pos )
         AS bracket
FROM   tmp
GROUP BY id;

查询 2

SELECT id,
       name,
       substr( name, start_pos ) AS bracket
FROM   (
  SELECT id,
         name,
         LAG( CASE WHEN bracket = '(' AND depth = 1 THEN pos END )
           IGNORE NULLS OVER ( PARTITION BY id ORDER BY ROWNUM )
           AS start_pos,
         pos AS end_pos,
         bracket,
         depth
  FROM   (
    SELECT id,
           name,
           COLUMN_VALUE AS pos,
           SUBSTR( name, column_value, 1 ) AS bracket,
           SUM( CASE SUBSTR( name, column_value, 1 ) WHEN '(' THEN 1 ELSE -1 END )
             OVER ( PARTITION BY id ORDER BY ROWNUM ) AS depth
    FROM   species_strain s,
           TABLE(
             CAST(
               MULTISET(
                 SELECT REGEXP_INSTR( s.name, '[()]', 1, LEVEL )
                 FROM   DUAL
                 CONNECT BY LEVEL <= REGEXP_COUNT( s.name, '[()]' )
               ) AS SYS.ODCINUMBERLIST
             )
           ) t
    WHERE  SUBSTR( s.name, -1 ) = ')'
  )
)
WHERE bracket = ')'
AND   end_pos = LENGTH( name );

结果

        ID NAME                                       BRACKET                                  
---------- ------------------------------------------ ------------------------------------------
       100 CfwHE3 (HH3d) Jt1 (CD-1)                   (CD-1)                                     
       101 4GSdg-3t 22sfG/J (mdx (fq) KO)             (mdx (fq) KO)                              
       102 Yf7mMjfel 7(tm1) (SCID)                    (SCID)                                     
       103 B29fj;jfos x11 (tmos (line x11))           (tmos (line x11))                          
       104 B29;CD (Atm (line G5))                     (Atm (line G5))                            
       106 data (1 (2 (333 (444) 3 (4 (5) 4) 3) 2) 1) (1 (2 (333 (444) 3 (4 (5) 4) 3) 2) 1)      

【讨论】:

我认为当函数不能被使用并且你需要一个纯SQL解决方案时,CONNECT BY是唯一的选择。另一方面,查询的成本为 651,而使用函数时为 3。出于可读性/可维护性/性能目的,您会选择该功能。 @KrzysztofKaszkowiak 更新为使用递归子查询因式分解子句。 OP 用括号显示输出;这些解决方案不包括它们。如果实际上需要括号,则可以轻松更改。当输入以 两个 右括号结尾时,查询 1 删除其中 both 使输出不平衡。在这些输入上,查询 2 可以正常工作。然后:查询 1 进入无限循环,查询 2 在附加测试字符串 'right grouping (includging ")")' 上产生意外结果 - Q2 返回内部括号组,忽略结束 )。正在研究通用解决方案... @mathguy 已修复,感谢您的调试 - OP 似乎没有考虑不平衡括号,因此,由于两个查询都给出了所需的答案,我不会在这种极端情况上花费大量时间,除非由 OP 要求。 @KrzysztofKaszkowiak - 关于 CONNECT BY - 自 Oracle 11.2 起可使用递归分解子查询(递归 CTE,您想为它们使用的任何名称)。绝对可以使用 CONNECT BY 完成的所有事情也可以使用递归查询完成(尽管代码有时可能会更长,因为 Oracle 预定义了某些工具,如 SYS_CONNECT_BY_PATH 和 CONNECT_BY_ISLEAF)。因此,每当您在任何地方看到 CONNECT_BY 解决方案时,请记住还有一个递归查询可以做同样的事情(但仅在 Oracle 11.2 或更高版本中可用)。【参考方案3】:

如果可以选择创建函数,那么以下函数可以完成这项工作:

create or replace
function fn_pars(p_text in varchar2) return varchar2 deterministic as 
  n_count pls_integer := 0;
begin
  if p_text is null or instr(p_text, ')', -1) = 0
    or p_text not like '%)' then
    return null;
  end if;
  for i in reverse 1..length(p_text) loop
    case substr(p_text, i, 1) 
      when ')' then n_count := n_count + 1;
      when '(' then n_count := n_count - 1;
      else null;
    end case;
    if n_count = 0 then 
      return substr(p_text, i);
    end if;
  end loop;
  return p_text;
end fn_pars;

那你就可以测试了:

select text,
       fn_pars(text)
  from (
          select 'B29fj;jfos x11 (tmos (line x11)) abc' text from dual union all
          select 'B29fj;j(fos) x11 (tmos (line x11))' text from dual union all
          select 'B29fj;j(fos) x11 (t(mo)s (line x11))' text from dual union all
          select '' text from dual union all
          select 'no parentheses' text from dual
       )

结果:

Text                                    fn_pars(text)
-----------------------------------------------------
B29fj;jfos x11 (tmos (line x11)) abc   (null)
B29fj;j(fos) x11 (tmos (line x11))     (tmos (line x11))
B29fj;j(fos) x11 (t(mo)s (line x11))   (t(mo)s (line x11))
(null)                                 (null)
no parentheses                         (null)

其中 (null) 表示没有值。 :)

该函数支持任何级别的嵌套。您还可以在同一级别嵌套多个括号。

【讨论】:

你的最后一句话真的让我想要这个解决方案。到目前为止,它会引发一个错误,即我没有足够的权限。如果她可以为我创建它,我会和我的前辈谈谈。 我正在查看您的结果,发现您返回了(tmos (line x11)),即使它后面有字符串。几分钟前我更新了这个问题,说我只需要列末尾的问题。因此,如果右括号后有字符,那么我不想要它。 你的意思是如果字符串包含除括号之外的任何其他内容,那么该字符串不匹配或者应该返回子字符串? 我的意思是,对于Test (me) too,我想要得到(me)。但如果是Test me (too),那么我想要(too)。这与我之前的问题基本相同,但这次我需要括号内的文本,即字符串末尾的文本。如果是中间,那我就不要了。 在您的结果中 - 前两个不应该匹配,因为它们在右括号后有 abcasdf。希望清除它。【参考方案4】:

您应该注意,Oracle 正则表达式不如 PCRE 或 .NET 正则表达式强大。因此,您只能使用正则表达式来匹配指定的嵌套括号级别。

以下正则表达式将匹配具有 1 个嵌套括号级别的字符串中的最后一个括号:

\([^()]*(\([^()]*\)[^()]*)*\)$

见regex demo

这个正则表达式不会匹配像Test (get (me) ((me), too)) 这样的字符串。我提供的模式适用于 1 个嵌套级别,它可以增强为支持 2 个级别,并且比现在更不可读。

详情

\( - 一个( [^()]* - 除() 之外的零个或多个字符 (\([^()]*\)[^()]*)* - 零次或多次出现: \(( [^()]* - 除了() 之外的零个或多个字符 \) - 结束) [^()]* - 除了() 之外的零个或多个字符 \) - 结束) $ - 字符串结尾。

使用喜欢

regexp_substr(col_name, '\([^()]*(\([^()]*\)[^()]*)*\)$', 1, 1)

【讨论】:

这似乎有效。抱歉,当我说last string 时,我的问题标题可能不是很清楚。我的意思是它必须在字符串的末尾。右括号后不应有其他文本。更新问题。【参考方案5】:

如果你只有一个嵌套,那么以下应该可以工作:

regexp_substr(col, '[(]([^()]*|[(][^()]*[)])+[)]$', 1, 1)

这满足您问题中的所有示例,但不允许任意嵌套括号。

【讨论】:

所以我尝试了你的正则表达式,到目前为止查询仍在运行:pastebin.com/59jy23cb 我的正则表达式更快,因为它遵循展开循环原则。这个包含一个交替,执行速度会慢一点(或很多,取决于数据)。 @WiktorStribiżew 似乎是这样。在我决定结束它之前,我的查询运行了 3 多分钟。你的肯定更快,但我确实忘记了更新问题中包含的 1 个重要要求。

以上是关于Oracle SQL 获取括号中的最后一个字符串(也可能包含括号)的主要内容,如果未能解决你的问题,请参考以下文章

Oracle SQL中的“缺少右括号”

oracle sql 最近5个季度的最后一天

oracle中如何获取最后执行的SQL语句并绑定变量值

如何在sql db2中的空格后获取字符串的最后一个字符

oracle 用SQl语句截取最后一个“\”后面的内容

2020-12-05 hive sql 截取最后一个特殊字符后面的内容