Oracle 18c - REGEXP_REPLACE 的替代品

Oracle 18c - Alternative to REGEXP_REPLACE

迁移到Oracle 18c企业版后,基于函数的索引创建失败

这是我的索引 DDL:

CREATE INDEX my_index ON my_table
(UPPER( REGEXP_REPLACE ("DEPT_NUM",'[^[:alnum:]]',NULL,1,0)))
TABLESPACE my_tbspace
PCTFREE    10
INITRANS   2
MAXTRANS   255
STORAGE    (
            INITIAL          64K
            MINEXTENTS       1
            MAXEXTENTS       UNLIMITED
            PCTINCREASE      0
            BUFFER_POOL      DEFAULT
           );

我收到以下错误:

ORA-01743: only pure functions can be indexed
01743. 00000 -  "only pure functions can be indexed"
*Cause:    The indexed function uses SYSDATE or the user environment.
*Action:   PL/SQL functions must be pure (RNDS, RNPS, WNDS, WNPS).  SQL
           expressions must not use SYSDATE, USER, USERENV(), or anything
           else dependent on the session state.  NLS-dependent functions
           are OK.

这是 18c 中的已知错误吗?如果不再支持这个基于函数的索引,还有什么写这个函数的方法?

很可能是 REGEXP_REPLACE 导致了问题,请参阅 。您可以使用用户定义的函数绕过限制(感谢 Bob Jarvis)

CREATE OR REPLACE FUNCTION KEEP_ALNUM(strIn IN VARCHAR2)
  RETURN VARCHAR2
  DETERMINISTIC
AS
BEGIN
  RETURN UPPER(REGEXP_REPLACE(strIn, '[^[:alnum:]]', NULL, 1, 0));
END KEEP_ALNUM;
/

CREATE INDEX DEPTS_1 ON DEPTS(KEEP_ALNUM(DEPT_NUM));

只需确保函数具有关键字 DETERMINISTIC,然后您甚至可以像下面这样定义无用的函数并在其上创建函数索引

CREATE OR REPLACE FUNCTION SillyValue RETURN VARCHAR2 DETERMINISTIC
AS
BEGIN
  RETURN DBMS_RANDOM.STRING('p', 20);
END;
/

有几个解决方法。

第一个是 hack。 您可能知道,当您创建 FBI 时,Oracle 会在其上创建隐藏列和索引。 此外,您甚至可以指定该列的名称而不是 FBI 表达式,Oracle 将使用索引。

set lines 70 pages 70
column column_name format a15
column data_type format a15

drop table my_table;

create table my_table(dept_num, dept_descr) as select rownum||'*', 'dummy' from dual connect by level <= 1e6; 

create index my_index
   on my_table(upper(regexp_replace(dept_num, '[^[:alnum:]]', null, 1, 0)));

select column_name, data_type from user_tab_cols where table_name = 'MY_TABLE';

explain plan for
select * from my_table where upper(regexp_replace(dept_num, '[^[:alnum:]]', null, 1, 0)) = '666';
select * from table(dbms_xplan.display(format => 'BASIC'));

explain plan for
select * from my_table where SYS_NC00003$ = '666';
select * from table(dbms_xplan.display(format => 'BASIC'));

输出

Table dropped.
Table created.
Index created.

COLUMN_NAME     DATA_TYPE      
--------------- ---------------
DEPT_NUM        VARCHAR2       
DEPT_DESCR      CHAR           
SYS_NC00003$    VARCHAR2       

3 rows selected.
Explain complete.

PLAN_TABLE_OUTPUT                                                     
----------------------------------------------------------------------
Plan hash value: 2234884270                                           

--------------------------------------------------------              
| Id  | Operation                           | Name     |              
--------------------------------------------------------              
|   0 | SELECT STATEMENT                    |          |              
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| MY_TABLE |              
|   2 |   INDEX RANGE SCAN                  | MY_INDEX |              
--------------------------------------------------------              

9 rows selected.
Explain complete.

PLAN_TABLE_OUTPUT                                                     
----------------------------------------------------------------------
Plan hash value: 2234884270                                           

--------------------------------------------------------              
| Id  | Operation                           | Name     |              
--------------------------------------------------------              
|   0 | SELECT STATEMENT                    |          |              
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| MY_TABLE |              
|   2 |   INDEX RANGE SCAN                  | MY_INDEX |              
--------------------------------------------------------              

9 rows selected.

因此,为了模仿 FBI,您可以创建一个隐藏列并在其上创建一个索引。 这可以在 Oracle 11g 中使用 dbms_stats.create_extended_stats.

完成
drop index my_index;

begin
   for i in (select dbms_stats.create_extended_stats
             (user, 'my_table', '(upper(regexp_replace("DEPT_NUM", ''[^[:alnum:]]'', null, 1, 0)))') as col_name
               from dual)
   loop
      execute immediate(utl_lms.format_message('alter table %s rename column "%s" to my_hidden_col','my_table', i.col_name));
   end loop;
end;
/

select column_name, data_type from user_tab_cols where table_name = 'MY_TABLE';

create index my_index on my_table(my_hidden_col);

explain plan for
select * from my_table where upper(regexp_replace(dept_num, '[^[:alnum:]]', null, 1, 0)) = '666';
select * from table(dbms_xplan.display(format => 'BASIC'));

explain plan for
select * from my_table where MY_HIDDEN_COL = '666';
select * from table(dbms_xplan.display(format => 'BASIC'));

输出

Index dropped.
PL/SQL procedure successfully completed.

COLUMN_NAME     DATA_TYPE      
--------------- ---------------
DEPT_NUM        VARCHAR2       
DEPT_DESCR      CHAR           
MY_HIDDEN_COL   VARCHAR2       

3 rows selected.
Index created.
Explain complete.

PLAN_TABLE_OUTPUT                                                     
----------------------------------------------------------------------
Plan hash value: 2234884270                                           

--------------------------------------------------------              
| Id  | Operation                           | Name     |              
--------------------------------------------------------              
|   0 | SELECT STATEMENT                    |          |              
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| MY_TABLE |              
|   2 |   INDEX RANGE SCAN                  | MY_INDEX |              
--------------------------------------------------------              

9 rows selected.
Explain complete.

PLAN_TABLE_OUTPUT                                                     
----------------------------------------------------------------------
Plan hash value: 2234884270                                           

--------------------------------------------------------              
| Id  | Operation                           | Name     |              
--------------------------------------------------------              
|   0 | SELECT STATEMENT                    |          |              
|   1 |  TABLE ACCESS BY INDEX ROWID BATCHED| MY_TABLE |              
|   2 |   INDEX RANGE SCAN                  | MY_INDEX |              
--------------------------------------------------------              

9 rows selected.

从 Oracle 12c 开始记录了隐藏的列,因此它变得更加简单。

alter table my_table add (my_hidden_col invisible as 
(upper(regexp_replace(dept_num, '[^[:alnum:]]', null, 1, 0))) virtual);
create index my_index on my_table(my_hidden_col);

另一种方法是在没有正则表达式的情况下实现相同的逻辑。

create index my_index on my_table(
translate(upper(dept_num, '_'||translate(dept_num, '_ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789', '_'), '_')));

但在这种情况下,您必须确保谓词中所有带有正则表达式的表达式都替换为新表达式。

我发现最简单的解决方法是使用 NLS_UPPER 而不是 UPPER:

创建索引
CREATE INDEX my_index ON my_table
( REGEXP_REPLACE (NLS_UPPER("DEPT_NUM"),'[^[:alnum:]]',NULL,1,0)))
TABLESPACE my_tbspace
PCTFREE    10
INITRANS   2
MAXTRANS   255
STORAGE    (
            INITIAL          64K
            MINEXTENTS       1
            MAXEXTENTS       UNLIMITED
            PCTINCREASE      0
            BUFFER_POOL      DEFAULT
           );

问题是 regexp_replace 不是确定性的。更改 NLS 设置时出现问题:

alter session set nls_language = english;

with rws as (
  select 'STÜFF' v
  from   dual
)
  select regexp_replace ( v, '[A-Z]+', '#' )
  from   rws;

REGEXP_REPLACE(V,'[A-Z]+','#')   
#Ü#  

alter session set nls_language = german;

with rws as (
  select 'STÜFF' v
  from   dual
)
  select regexp_replace ( v, '[A-Z]+', '#' )
  from   rws;

REGEXP_REPLACE(V,'[A-Z]+','#')   
#     

U-umlaut 位于英语字母表的末尾。但是在德语中的 U 之后。所以第一个语句 不会 替换它。第二个。

在 Oracle 数据库 12.1 和更早版本中,regexp_replace 错误地 标记为确定性。 12.2 通过使其成为非确定性来修复此问题。

仔细考虑是否有任何解决方法可以正确管理变音符号。

MOS 说明 2592779.1 对此进行了进一步讨论。