如何查找仅包含数字数据的非数字列?

How to find non-numeric columns containing only numeric data?

我想在我的 Oracle 数据库模式中找到所有只包含数字数据但具有非数字类型的列。 (所以基本上可能选择了错误的数据类型的列候选。)

我查询所有 varchar2 列:

SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE
FROM user_tab_cols
WHERE DATA_TYPE = 'VARCHAR2';

此外,我有一个查询来检查 table myTable 和列 myColumn:

中的任何非数字数据
SELECT 1
FROM myTable
WHERE NOT REGEXP_LIKE(myColumn, '^[[:digit:]]+$');

我喜欢以第一个查询仅 returns 行的方式组合两个查询,第二个查询 not exists

这里的主要问题是第一个查询在数据字典的元层上,其中 TABLE_NAME 和 COLUMN_NAME 作为数据出现,我需要该数据作为 identifiers(并且不是数据)在第二个查询中。

在伪SQL中我有类似的想法:

SELECT TABLE_NAME, COLUMN_NAME, DATA_TYPE
FROM user_tab_cols
WHERE DATA_TYPE = 'VARCHAR2'
AND NOT EXISTS
(SELECT 1 from asIdentifier(TABLE_NAME) 
WHERE NOT REGEXP_LIKE(asIdentifier(COLUMN_NAME), '^[[:digit:]]+$'));

像这样创建一个函数:

create or replace function isNumeric(val in VARCHAR2) return INTEGER AS
res NUMBER;
begin
   res := TO_NUMBER(val);
   RETURN 1;
EXCEPTION
   WHEN OTHERS THEN
      RETURN 0;
END;

那么你可以这样使用它:

DECLARE
  r integer;
BEGIN
   For aCol in (SELECT TABLE_NAME, COLUMN_NAME FROM user_tab_cols WHERE DATA_TYPE = 'VARCHAR2') LOOP
      -- What about CHAR and CLOB data types?
      execute immediate 'select count(*) from '||aCol.TABLE_NAME||' WHERE isNumeric('||aCol.COLUMN_NAME||') = 0' into r;
      if r = 0 then
         DBMS_OUTPUT.put_line(aCol.TABLE_NAME ||' '||aCol.COLUMN_NAME ||' contains numeric values only');
      end if;
   end loop;
end;

请注意,此 PL/SQL 块的性能会很差。希望这只是一份一次性工作。

有两种可能的方法:动态SQL (DSQL) 和XML。

第一个已经在另一个回复中演示过,速度更快。

XML 纯属娱乐

create or replace function to_number_udf(p in varchar2) return number
  deterministic is
  pragma udf;
begin
  return p * 0;
  exception when invalid_number or value_error then return 1;
end to_number_udf;
/

create table t_chk(str1, str2) as
select '1', '2' from dual union all
select '0001.1000', 'helloworld' from dual;

SQL> column owner format a20
SQL> column table_name format a20
SQL> column column_name format a20
SQL> with tabs_to_check as
  2  (
  3  select 'collection("oradb:/'||owner||'/'||table_name||'")/ROW/'||column_name||'/text()' x,
  4         atc.*
  5    from all_tab_columns atc
  6   where table_name = 'T_CHK'
  7     and data_type = 'VARCHAR2'
  8     and owner = user
  9  )
 10  select --+ no_query_transformation
 11         owner, table_name, column_name
 12    from tabs_to_check ttc, xmltable(x columns "." varchar2(4000)) x
 13  group by owner, table_name, column_name
 14  having max(to_number_udf(".")) = 0;

OWNER                TABLE_NAME           COLUMN_NAME
-------------------- -------------------- --------------------
TEST                 T_CHK                STR1

PS。在 Oracle 12.2 上,您可以使用 to_number(... default ... on conversion error) 而不是 UDF。

检查一个字符串是否全是数字还是至少包含一个非数字字符的更快方法是使用 translate 函数。 las,由于 Oracle 处理空字符串的非 SQL 标准方式,我们必须使用的函数形式有点复杂:

translate(input_string, 'z0123456789', 'z')

z 可以是任何非数字字符;我们需要它以便第三个参数不为空)。这通过 t运行slating z 到它自己和 0 等来实现。因此,如果输入字符串是 null 或全数字,并且仅在这种情况下,函数返回的值是 null.

此外:为了使过程更快,您可以使用 EXISTS 条件测试每一列。如果一列不是数字,那么在大多数情况下 EXISTS 条件将很快变为真,因此您将不得不检查此类列中的极少数值。

当我试图完成这项工作时,我 运行 遇到了许多附带问题。大概您想查看所有模式(SYSSYSTEM 除外)。因此,您需要从具有 SYSDBA 权限的帐户 运行 过程(匿名块)。然后 - 我 运行 遇到非标准 table 和列名称(名称以下划线等开头)的问题;这让人想起用双引号定义的标识符——一种糟糕的做法。

为了说明,我将使用 HR 模式 - 该方法在其上起作用。您可能需要进一步调整;我无法通过更改行来使其工作

and owner = 'HR'

and owner != 'SYS'

所以 - 有了这么长的介绍 - 这就是我所做的。

首先,在一个 "normal" 用户帐户中(我自己的,名为 INTRO - 我 运行 一个非常小的数据库,只有一个 "normal" 用户,加上Oracle "standard" 用户,如 SCOTT、HR 等)- 因此,在模式 INTRO 中,我创建了一个 table 来接收所有人的名称、table 名称和列名称数据类型为 VARCHAR2 且仅包含 "numeric" 值或 null 的列(数字按照您的方式定义。)此处注意:如果您随后想要真正检查所有数值,您确实需要一个正则表达式,或类似于 Wernfried 展示的内容;否则,我仍然会在匿名过程中使用 EXISTS 条件而不是 COUNT。

然后我创建了一个匿名块来查找所需的列。注意:您不会有模式 INTRO - 所以在我的代码中的任何地方都更改它(在创建 table 和匿名块中)。如果过程成功完成,您应该能够查询 table。我也在最后展示。

当以 SYS(或具有 SYSDBA 权限的其他用户)身份登录时:

create table intro.cols_with_numbers (
  owner_name  varchar2(128),
  table_name  varchar2(128),
  column_name varchar2(128)
);

declare x number;
begin
  execute immediate 'truncate table intro.cols_with_numbers';
  for t in ( select owner, table_name, column_name
             from   dba_tab_columns
             where  data_type like 'VARCHAR2%'
               and  owner = 'HR'
           ) 
  loop
    execute immediate 'select case when exists (
                                select *
                                from ' || t.owner || '.' || t.table_name ||
                              ' where  translate(' || t.column_name || ',
                                         ''z0123456789'', ''z'') is not null
                              ) then 1 end
                       from   dual'    
    into x;
    if x is null then
      insert into intro.cols_with_numbers (owner_name, table_name, column_name)
         values(t.owner, t.table_name, t.column_name);
    end if;
  end loop;
end;
/

运行这个程序然后查询table:

select * from intro.cols_with_numbers;

no rows selected

(这意味着在错误的数据类型 VARCHAR2 中,HR 模式中的 tables 中没有数字列 - 或者至少,没有只有非负整数值的此类列。)你可以进一步测试,通过有意创建一个带有这样一个列的 table 并通过程序测试它是 "caught"。

ADDED - 这是当我将所有者从 'HR' 更改为 'SCOTT' 时发生的情况:

PL/SQL procedure successfully completed.


OWNER_NAME           TABLE_NAME           COLUMN_NAME        
-------------------- -------------------- --------------------
SCOTT                BONUS                JOB                 
SCOTT                BONUS                ENAME   

所以它似乎工作正常(虽然在其他模式上我有时 运行 出错......我会看看我是否能弄清楚那是什么)。

在这种情况下,table 是空的(没有行!)- 这是您可能会发现的 "false positive" 的一个示例。 (更一般地说,如果 VARCHAR2 列中的所有内容都是 null - 在 table 的所有行中,您将得到误报。)

另请注意,列可能只有数值,但最好的数据类型仍然是 VARCHAR2。当值只是标识符而不是 "numbers"(我们可以相互比较或与固定值 and/or 进行算术运算)时就是这种情况。示例 - SSN(社会安全号码)或其他国家的同等号码; SSN 是每个人与政府开展业务的 "official" 标识符。 SSN 是数字(实际上,也许是为了强调它不应该是一个 "number" 这一事实,尽管它的名字,它通常用几个破折号写成...)