【问题标题】:Performance and Readability of REGEXP_SUBSTR vs INSTR and SUBSTRREGEXP_SUBSTR 与 INSTR 和 SUBSTR 的性能和可读性
【发布时间】:2017-04-30 14:20:02
【问题描述】:

根据我的另一个问题Using REGEXP_SUBSTR with Strings Qualifier,我正在尝试确定哪种方法更适合使用。

结果数据集应仅按正确顺序显示分隔符 PLE#ALL 之前的字符串。 包中已有的当前查询是这样的(DDL 和 DML 在帖子底部):

SELECT  DATA1
      , DECODE(SIGN(0 - instr(DATA1, 'PLE')), -1, SUBSTR(DATA1, 1, instr(DATA1, 'PLE') - 1)) GET_DATA_TILL_FIRST_PLE
      , DECODE(SIGN(0 - instr(DATA1, '#')), -1, SUBSTR(DATA1, 1, instr(DATA1, '#') - 1)) GET_DATA_TILL_FIRST_NUM_SIGN
      , DECODE(SIGN(0 - instr(DATA1, 'ALL')), -1, SUBSTR(DATA1, 1, instr(DATA1, 'ALL') - 1)) GET_DATA_TILL_FIRST_ALL
      , NVL(DECODE(SIGN(0 - instr(DATA1, 'PLE')), -1, SUBSTR(DATA1, 1, instr(DATA1, 'PLE') - 1), 0,
        DECODE(SIGN(0 - instr(DATA1, '#')), -1, SUBSTR(DATA1, 1, instr(DATA1, '#') - 1), 0,
        DECODE(SIGN(0 - instr(DATA1, 'ALL')), -1, SUBSTR(DATA1, 1, instr(DATA1, 'ALL') - 1), DATA1), DATA1), DATA1), DATA1) PUT_THEM_ALL_TOGETHER    
FROM    table_x;    

以下数据集的结果:

DATA1                   | GET_DATA_TILL_FIRST_PLE | GET_DATA_TILL_FIRST_#_SIGN  | GET_DATA_TILL_FIRST_ALL    |  PUT_THEM_ALL_TOGETHER
----------------------- | ----------------------- | --------------------------- | -------------------------- |  ----------------------
STRING_EXAMPLE          | STRING_EXAM             |                             |                            |  STRING_EXAM
TREE_OF_APPLES          | TREE_OF_AP              |                             |                            |  TREE_OF_AP
FIRST_EXAMPLE           | FIRST_EXAM              |                             |                            |  FIRST_EXAM
IMPLEMENTATION          | IM                      |                             |                            |  IM
PARIS                   |                         |                             |                            |  PARIS
PLEONASM                |                         |                             |                            |  PLEONASM
XXXX 1                  |                         |                             |                            |  XXXX 1 
XXXX YYYYYY 2 FFFFFFFFF |                         |                             |                            |  XXXX YYYYYY 2 FFFFFFFFF
XXXX YYYYYY 5FFFFFFFFF  |                         |                             |                            |  XXXX YYYYYY 5FFFFFFFFF
OPOPOPOPO #09090 APPLE  | OPOPOPOPO #09090 AP     | OPOPOPOPO                   | OPOPOPOPO #                |  OPOPOPOPO #09090 AP
OPOPOPOPO BALL#         |                         | OPOPOPOPO BALL              | OPOPOPOPO B                |  OPOPOPOPO BALL
BALL IS #LIFE           |                         | BALL IS                     | B                          |  BALL IS     

PS。我只需要PUT_THEM_ALL_TOGETHER 列,但我也包含了其他列并添加上下文。

我发现查询有点混乱且难以阅读,所以我尝试使用REGEXP_SUBSTR 并在@vkp 的建议下,我想出了以下查询 这会产生与上述相同的数据集。

SELECT  DATA1
  , REGEXP_SUBSTR(DATA1, '(.+?)PLE',1,1,null,1) GET_DATA_TILL_FIRST_PLE
  , REGEXP_SUBSTR(DATA1, '(.+?)#',1,1,null,1) GET_DATA_TILL_FIRST_#_SIGN
  , REGEXP_SUBSTR(DATA1, '(.+?)ALL',1,1,null,1) GET_DATA_TILL_FIRST_ALL
  , COALESCE(REGEXP_SUBSTR(DATA1, '(.+?)PLE',1,1,null,1),
             REGEXP_SUBSTR(DATA1, '(.+?)#',1,1,null,1),
             REGEXP_SUBSTR(DATA1, '(.+?)ALL',1,1,null,1),
             DATA1) PUT_THEM_ALL_TOGETHER
FROM    table_x;     

但是,从@MathGuy 的回答看来,INSTRSUBSTR 的效率更高。 我在某种程度上对此进行了测试,这就是我得到的:

使用INSTRSUBSTR

SET TIMING ON;    
BEGIN
    UPDATE  table_x
    SET     DATA2 = NVL(DECODE(SIGN(0 - instr(DATA1, 'PLE')), -1, SUBSTR(DATA1, 1, instr(DATA1, 'PLE') - 1), 0,
                    DECODE(SIGN(0 - instr(DATA1, '#')), -1, SUBSTR(DATA1, 1, instr(DATA1, '#') - 1), 0,
                    DECODE(SIGN(0 - instr(DATA1, 'ALL')), -1, SUBSTR(DATA1, 1, instr(DATA1, 'ALL') - 1), DATA1), DATA1), DATA1), DATA1);    
    ROLLBACK;        
END;
/            

PL/SQL 过程成功完成。
经过:00:00:00.234

使用REGEXP_SUBSTR:

SET TIMING ON;  
BEGIN    
    UPDATE  table_x
    SET     DATA2 = COALESCE(REGEXP_SUBSTR(DATA1, '(.+?)PLE',1,1,null,1)
                            ,REGEXP_SUBSTR(DATA1, '(.+?)#',1,1,null,1)
                            ,REGEXP_SUBSTR(DATA1, '(.+?)ALL',1,1,null,1)
                            ,DATA1);
    ROLLBACK;        
END;
/    

PL/SQL 过程成功完成。
经过:00:00:00.236

虽然这是一个非常有限的测试数据,但它表明INSTRSUBSTR 的组合比REGEXP_SUBSTR 快一点。 为了可读性,使用REGEXP_SUBSTR 代替INSTRSUBSTR 是否可以忽略不计?

DML 和 DDL:

create table table_x 
(
    data1 varchar2(100)    
   ,data2 varchar2(100)
);

INSERT INTO table_x (DATA1) VALUES ('STRING_EXAMPLE');
INSERT INTO table_x (DATA1) VALUES ('TREE_OF_APPLES');
INSERT INTO table_x (DATA1) VALUES ('FIRST_EXAMPLE');  
INSERT INTO table_x (DATA1) VALUES ('IMPLEMENTATION');   
INSERT INTO table_x (DATA1) VALUES ('PARIS');            
INSERT INTO table_x (DATA1) VALUES ('PLEONASM');        

INSERT INTO table_x (DATA1) VALUES ('XXXX 1');   
INSERT INTO table_x (DATA1) VALUES ('XXXX YYYYYY 2 FFFFFFFFF'); 
INSERT INTO table_x (DATA1) VALUES ('XXXX YYYYYY 5FFFFFFFFF'); 

INSERT INTO table_x (DATA1) VALUES ('OPOPOPOPO #09090 APPLE'); 
INSERT INTO table_x (DATA1) VALUES ('OPOPOPOPO BALL#'); 
INSERT INTO table_x (DATA1) VALUES ('BALL IS #LIFE');   

谢谢。

【问题讨论】:

  • Elapsed: 00:00:00.236 - 这是 236 毫秒与 234 毫秒。您似乎在一个非常有限的数据集(少于几千条记录)上对其进行了测试。这样的测试是不可靠的——对于 DBMS 来说,几千条记录不算什么,这种差异也可能来自 DBMS 和您的客户端之间的网络延迟。请准备一个包含几百万行而不是几百或几千行的数据集,然后再次重复测试并进行测量。我敢打赌,正则表达式会输..
  • 嗨@krokodilko,是的,这在上面的帖子中得到了承认。
  • @krokodilko - 这很有意义,但“现在做什么”也取决于 OP 的现实问题。如果在他的实际问题中,他的表中只有 5,000 行,那么他可能不在乎在具有 1000 万行的表上哪个解决方案“最好”。如果他有一个包含 5,000 行的表,他会更关心可读性和可维护性而不是性能。
  • @krokodilko - 话虽如此,谢谢你让我好奇地进行了一些测试。有关结果,请参阅我刚刚发布的单独答案。

标签: sql regex oracle


【解决方案1】:

我已经发布了一个答案,展示了如何以正确的方式使用 INSTRSUBSTR 来解决这个问题。

在这个“答案”中,我解决了另一个问题——哪种解决方案更有效。我将在下面解释测试,但底线是:REGEXP 解决方案比 INSTR/SUBSTR 解决方案花费 40 倍

设置:我创建了一个包含 150 万个随机字符串的表(全部正好是 8 个字符,全部为大写字母)。然后我修改了 10% 的字符串以添加子字符串 'PLE',另外 10% 以添加 '#',另外 10% 以添加 'ALL'。我通过在 mod(rownum, 9) 位置拆分原始字符串(即 0 到 8 之间的数字)并在该位置连接 'PLE''#''ALL' 来做到这一点。诚然,这不是获取我们需要的测试数据的最有效或最优雅的方式,但这无关紧要 - 关键在于创建测试数据并在我们的测试中使用它。

所以:我们现在有一个只有一列的表,data1,在 150 万行中有一些随机字符串。 10% 每个都有子字符串 PLE#ALL

测试包括创建新字符串data2,就像在原始帖子中一样。我没有将结果重新插入表中;不管data2 是如何计算的,将其插入回到表中的时间应该是相同的。

相反,我将主查询放在一个外部查询中,该查询计算得到的data2 值的长度之和。这样我保证优化器不会走捷径:必须生成所有data2 值,必须测量它们的长度,然后求和。

以下是创建基表所需的语句,我称之为table_z,然后是我运行的查询。

create table table_z as
select dbms_random.string('U', 8) as data1 from dual
connect by level <= 1500000;

update table_z 
set data1 = case
when rownum between      1 and 150000 then substr(data1, 1, mod(rownum, 9)) 
                               || 'PLE' || substr(data1, mod(rownum, 9) + 1)
when rownum between 150001 and 300000 then substr(data1, 1, mod(rownum, 9)) 
                               || '#'   || substr(data1, mod(rownum, 9) + 1)
when rownum between 300001 and 450000 then substr(data1, 1, mod(rownum, 9)) 
                               || 'ALL' || substr(data1, mod(rownum, 9) + 1)
          end
where rownum <= 450000;

commit;

INSTR/SUBSTR解决方案

select sum(length(data2))
from (
select data1, 
       case 
         when instr(data1, 'PLE', 2) > 0 then substr(data1, 1, instr(data1, 'PLE', 2) - 1)
         when instr(data1, '#'  , 2) > 0 then substr(data1, 1, instr(data1, '#'  , 2) - 1)
         when instr(data1, 'ALL', 2) > 0 then substr(data1, 1, instr(data1, 'ALL', 2) - 1)
         else data1 end
       as data2
from   table_z
);

SUM(LENGTH(DATA2))
------------------
          10713352

1 row selected.

Elapsed: 00:00:00.73

REGEXP解决方案

select sum(length(data2))
from (
select data1, 
       COALESCE(REGEXP_SUBSTR(DATA1, '(.+?)PLE',1,1,null,1)
                            ,REGEXP_SUBSTR(DATA1, '(.+?)#',1,1,null,1)
                            ,REGEXP_SUBSTR(DATA1, '(.+?)ALL',1,1,null,1)
                            ,DATA1)
       as data2
from   table_z
);

SUM(LENGTH(DATA2))
------------------
          10713352

1 row selected.

Elapsed: 00:00:30.75

在有人提出这些建议之前:我多次重复这两个问题;第一个解决方案始终在 0.75 到 0.80 秒内运行,第二个查询在 30 到 35 秒内运行。慢了 40 多倍。 (所以编译器/优化器花费时间来编译查询不是问题;这实际上是执行时间。)此外,这与从基表中读取 150 万个值无关——在两个测试,并且比处理花费的时间要少得多。无论如何,我首先运行INSTR/SUBSTR 查询,所以如果有任何缓存,REGEXP 查询将是受益者。

编辑:我刚刚发现了建议的 REGEXP 解决方案的一个低效之处。如果我们将搜索模式锚定到字符串的开头(例如'^(.+?)PLE',注意^ 锚点),则 REGEXP 查询的运行时间从 30 秒下降到 10 秒。显然,Oracle 的实现不够聪明,无法识别这种等价性,并尝试从第二个字符、第三个字符等开始搜索。但执行时间几乎长了 15 倍; 15

【讨论】:

  • 我认为您今天处于详细模式,例如@mathguy --verbose
  • @TimBiegeleisen - :-) 我经常这样。众所周知,我说得太多而不是说得不够。
  • “显然,Oracle 的实现不够聪明,无法识别这种等价性”,就像我喜欢好好挖掘 Oracle 一样,他们的辩护 (.+?)PLE 不等于 ^(.+?)PLE^ 是该行的开头,因此定义了另一个模式的更严格的子集。 RegEx 很强大,但也需要自己的编译来执行,并且总是比基本的likeinstr 文字字符匹配函数慢。如果您可以从第一个字符开始匹配,它也总是会更快,因为它可以更快地排除不匹配。如果该字符串上有索引,则更是如此。
  • @Davos - 领先的(.+?) 使您的观察结果都错误。如果从第三个字符开始存在匹配,则该匹配的前导 (.+?) 部分可以扩展为包括字符串的前两个字符,这意味着从第一个字符开始也存在匹配。如果第一个字符也没有匹配,则第三个字符也不匹配。以及您对索引的评论...如果 reg exp 的第一部分是(.+?),索引如何提供帮助?正如他们所说,“让睡着的狗躺着”。
  • 我明白你的意思,在这种情况下它们是等价的。如果该模式允许换行符(我意识到它不允许),那么^ 将排除换行符之后的任何不等价的匹配项;如果可以确定该模式的其余部分没有考虑换行符,那么也许这是他们可以包括的优化。关于索引,我提出了一个一般性的观点,即如果您可以从一开始就进行字符串匹配而不使用前导通配符,那么可以使用索引,大多数正则表达式模式匹配将排除索引搜索。是的,我希望我有。
【解决方案2】:

在实践中使用INSTR 的方法和使用REGEXP_SUBSTR 的方法都在进行非常相似的字符串操作。

让我们比较使用基本字符串函数和使用正则表达式的查询中的第二个选择项。这是您的第一个查询中的术语,它仅使用 INSTR

DECODE(SIGN(0 - instr(DATA1, 'PLE')), -1, SUBSTR(DATA1, 1, instr(DATA1, 'PLE') - 1))

假设DATA1 列匹配,这将需要两次调用INSTR。在不知道实现细节的情况下,我假设每次调用INSTR 都需要在DATA1 中从左到右传递字符串,直到找到'PLE' 或到达字符串末尾。在这两种情况下,INSTR 只需要对 DATA1 中的字符串进行一次传递。

这是相同的选择项,但使用了正则表达式:

REGEXP_SUBSTR(DATA1, '(.+?)PLE',1,1,null,1)

我不太熟悉您在调用REGEXP_SUBSTR 时使用的所有参数,但据我所知,这只是对DATA1 列中的所有内容进行香草风味的正则表达式捕获,直到第一次出现'PLE' 发生。同样,这也只需要一次通过 DATA1 字符串,并且您的正则表达式中没有任何内容,例如前瞻/后视或其他任何需要超过一次通过的东西。

您的所有选择看起来都与此非常相似,因此我假设使用基本 Oracle 字符串函数的性能与正则表达式相比非常相似,并且您自己的性能测试似乎证实了这一点,至少对于您的特定数据集。

实际上,正在执行非常相似的字符串操作,只是在高层次上它们的措辞略有不同。就个人而言,我可能会选择正则表达式解决方案,因为它更具可读性并且可能更易于维护。

【讨论】:

  • 不错的推测。唉,实际测试表明结论是 100% 错误的。在具有 150 万个 DATA1 值的表中,其中 10% 的每个值包含子字符串 'PLE''#''ALL'REGEXP 解决方案比 INSTR/SUBSTR 解决方案花费的时间长 40 倍。 (我发布了一个单独的答案来描述测试,任何愿意的人都可以复制它。)您提供的推测的问题是我们不知道 Oracle 如何实现不同功能的细节。我不知道为什么INSTR/SUBSTR 这么快。就是这样。
  • 至于可读性,我不相信。我在另一个答案中展示了我将如何使用INSTR/SUBSTR 编写解决方案。在我看来,它似乎更清晰 - 我可以立即告诉它应该做什么,以及它是如何做到的。但这可能是主观的 - 如果有人一直使用正则表达式,他们可能会发现REGEXP 解决方案更易于阅读。
  • @mathguy 您的 cmets 是推测性的,因为您没有研究运行时间如何随数据集的大小、操作类型等变化。执行 any 可能会产生隐含的开销 Oracle 中的正则表达式,它可以解释你所看到的。我相信我对这两种情况下使用的算法的总体分析是合理的。
  • 问题不在于算法(我不知道 Oracle 使用什么算法,是吗?),而在于执行时间。如果您的分析大致正确,那么结果不应该取决于数据的大小——当然也不是 40 倍!我的观点也正是您得出的结论:在 Oracle 中执行 any 正则表达式有很大的开销(这是我之前反复阅读过的内容,也是我告诉 OP 坚持使用 INSTR/SUBSTR 的原因在他的原始帖子中,与此相关)。
  • @mathguy 但是对于具有可读性和可维护性的查询有一些话要说。正则表达式比基本字符串函数更容易阅读和维护。
【解决方案3】:

使用INSTRSUBSTR 的解决方案可以重写,因此它与基于regexp 的解决方案一样容易(或更容易)阅读和维护。关于性能,您可以测试两个版本并决定。注意:此解决方案将比您尝试使用INSTRSUBSTR 更快,因为在CASE 表达式中,CASE 的连续分支仅在第一个TRUE 分支之前被评估 - 其余分支不被评估. (也因为您的解决方案有大量不必要的函数调用 - nvldecodesign,这些都不需要。)

select data1, 
       case when instr(data1, 'PLE') > 0 then substr(data1, 1, instr(data1, 'PLE') - 1)
            when instr(data1, '#')   > 0 then substr(data1, 1, instr(data1, '#'  ) - 1)
            when instr(data1, 'ALL') > 0 then substr(data1, 1, instr(data1, 'ALL') - 1)
            else data1 end
       as data2
from   table_x;

根据您的 UPDATE 声明进行调整。

编辑:我错过了一个事实,如果初始结果是NULL,那么表达式应该返回原始字符串。这意味着我们不应该在字符串的开头搜索'PLE''#''ALL'。但是,如果有可能在输入字符串的开头找到“搜索模式”,然后在字符串的后面再次找到 - 例如,'ALL IN ALL'。所以我们需要从data1 的第二个字符而不是第一个字符开始搜索INSTR。为此,我们使用INSTR 的第三个(可选)参数。

更新的解决方案:

select data1, 
       case 
          when instr(data1, 'PLE', 2) > 0 then substr(data1, 1, instr(data1, 'PLE', 2) - 1)
          when instr(data1, '#'  , 2) > 0 then substr(data1, 1, instr(data1, '#'  , 2) - 1)
          when instr(data1, 'ALL', 2) > 0 then substr(data1, 1, instr(data1, 'ALL', 2) - 1)
          else data1 end
       as data2
from   table_x;

【讨论】:

  • 我没有在 data2 列中得到PLEONASM 的结果,但这看起来不错。此外,APP 应该是 ALL,但两者的结果相同。
  • @MigsIsip - 我编辑将“APP”更改为“ALL”。关于PLEONASM,我一直错过了,当你在字符串的开头找到PLE(或#ALL)时,你不想返回NULL(虽然我不确定为什么不呢;那会更有意义)。无论如何,这很容易适应 - 我在答案末尾添加了 Edit
【解决方案4】:

您是否测量过您遇到了某种性能问题?你在看什么大小的数据集?您希望实现什么样的加速?例如,如果您希望在每个查询中提高 10% 的速度,那么 234 毫秒和 236 毫秒之间的差异不会产生任何影响,而您是在浪费您作为程序员的时间。

您的 SQL 查询是否花费了大量时间来运行您的代码?如果您的应用程序是 80% 的网络传输和 20% 的服务器端,并且您设法将查询速度提高了 10%,那么您的应用程序只加速了 2%。最好花时间减少通过网络获取数据所需的时间。

另一种思考方式:想象一下,您发现在现实生活中,您每个月花的钱比赚的还多。你开始想“好吧,也许当我去星巴克时,我会买一小杯咖啡而不是一大杯,我会节省 50 美分!”但是如果你有 3,000 美元的抵押贷款会吃掉 60%你的实得工资?

如果您不知道您的应用程序的慢点,您可能会在错误的事情上浪费时间。

【讨论】:

    猜你喜欢
    • 2021-06-17
    • 2015-08-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多