当表中的列数不同时,如何比较oracle中的两个表

时间:2016-10-20 13:32:56

标签: oracle

我的数据库中有两个表,它们是EXP1和EXP2。我尝试使用下面的查询,当两个表具有相同的列数但我的表EXP1有1000列时,此查询正在工作,而EXP2有1000 + 4。

select *
from 
(
    (select * from exp1
     minus 
     select * from exp2)
    union all
    (select * from exp2
     minus
     select * from exp1)
);

1 个答案:

答案 0 :(得分:0)

INTRO:下面我将展示如何做到"手工"这些工具(例如SQL Developer)可以做得更快更好。我对此(和你的!)的兴趣是双重的:学习和使用一些可以帮助解决许多其他问题的想法;并首先了解这些工具的内幕。

行。假设你有两个表,它们有许多共同的列(可能不是以相同的顺序),而且几列可能不同 - 一个表中可能有少量列,但另一个表中可能没有。首先,您希望能够只查看公共列。

然后,假设已经完成了。现在这两个表的左边有多个共同的行,但有一些是不同的。行可以存在于一个表中但不存在于另一个表中,或者两行(每个表中有一行)可能非常相似,但它们可能仅在一个或少量列值中不同。逻辑上,这些仍然是第一个表中的一行而不是第二行,而另一行仅在第二个表中但不在第一个表中。但是,假设两个表都具有相同的PK列 - 那么两个表中的PK值可能相同,但至少有一个OTHER列在两个表中具有不同的PK值。并且,您希望在两个表之间找到这些差异。

在下文中,我将假设如果两个表中的两列具有相同的名称,它们也将具有相同的数据类型。如果在您的情况下无法保证,可以在我识别"常用列"的部分中进行更多工作来修复它。 - 而不是仅仅通过名称匹配它们,从目录视图中,它们也必须与数据类型匹配。

当您在最后一步中比较两个表中的行时,(A minus B) union all (B minus A)有效,但效率不高。每个表都被读取两次,minus是一个昂贵的运算符。几年前,我在AskTom的长篇大论中讨论了更有效的解决方案,我将在下面进行说明。即:收集两个表中的所有行(使用union all),按所有列分组,并忽略计数为2的组。这意味着在两个表中找到的行,因此它们是重复的union all!实际上,你会看到一个额外的小技巧,以确定从哪个表中"非重复"行来了。为" table_name"添加一列在最终选择中,在使用count(*) = 1对组进行分组和保留后,选择max(table_name)。您需要一个聚合函数(如max()),因为您正在进行分组,但对于这些行,每个组只有一行,因此max()实际上只是表名。

这种方法的优点在于它也可用于识别常见的列!在这种情况下,我们将比较USER_TAB_COLS视图中的行 - 我们选择出现在任一表中的列名,并仅保留重复的列名(因此列名出现在两个表中)。在解决方案的该部分中,我还检索column_id,用于对列进行排序。如果你不熟悉keep (dense_rank first...),请不要担心 - 它并不是那么复杂,但它也不是那么重要。

首先让我们设置一个测试用例。我将EMP表从SCOTT模式复制到我自己的模式,我复制它(现在我有两个副本,名为EMP1EMP2),我修改了他们咯。我从每个列中删除了一个不同的列,我从每个列中删除了几个(不同的)行,并在一个表中修改了一个薪水。我不会显示结果(略有不同)的表格,但是如果你跟随,只需要select *,并在继续阅读之前比较它们。

创建表格:

create table EMP1 as select * from scott.emp;
Table EMP1 created.

select * from EMP1;

EMPNO ENAME      JOB        MGR HIREDATE              SAL   COMM  DEPTNO
----- ---------- --------- ---- ------------------- ----- ------ -------
 7369 SMITH      CLERK     7902 1980-12-17 00:00:00   800             20
 7499 ALLEN      SALESMAN  7698 1981-02-20 00:00:00  1600    300      30
 7521 WARD       SALESMAN  7698 1981-02-22 00:00:00  1250    500      30
 7566 JONES      MANAGER   7839 1981-04-02 00:00:00  2975             20
 7654 MARTIN     SALESMAN  7698 1981-09-28 00:00:00  1250   1400      30
 7698 BLAKE      MANAGER   7839 1981-05-01 00:00:00  2850             30
 7782 CLARK      MANAGER   7839 1981-06-09 00:00:00  2450             10
 7788 SCOTT      ANALYST   7566 1987-04-19 00:00:00  3000             20
 7839 KING       PRESIDENT      1981-11-17 00:00:00  5000             10
 7844 TURNER     SALESMAN  7698 1981-09-08 00:00:00  1500      0      30
 7876 ADAMS      CLERK     7788 1987-05-23 00:00:00  1100             20
 7900 JAMES      CLERK     7698 1981-12-03 00:00:00   950             30
 7902 FORD       ANALYST   7566 1981-12-03 00:00:00  3000             20
 7934 MILLER     CLERK     7782 1982-01-23 00:00:00  1300             10

略微修改它们:

create table EMP2 as select * from EMP1;
Table EMP2 created.

alter table emp1 drop column hiredate;
Table EMP1 altered.

alter table emp2 drop column comm;
Table EMP2 altered.

delete from EMP1 where ename like 'A%';
2 rows deleted;

delete from EMP2 where sal >= 3000;
3 rows deleted

update EMP2 set sal = 2950 where empno = 7698;
1 row updated

commit;

此时您最好select * from EMP1;select * from EMP2;并进行比较。

现在让我们找出这两个表共有哪些列。

select column_name, 
       min(column_id) keep(dense_rank first order by table_name) as col_id
from   user_tab_cols
where  table_name in ('EMP1', 'EMP2')
group by column_name
having count(*) = 2
order by col_id;

COLUMN_NAME COL_ID
----------- ------
EMPNO            1
ENAME            2
JOB              3
MGR              4
SAL              5
DEPTNO           7

 6 rows selected 

完美,所以现在我们可以比较两个表,但只有在我们" project"它们只是沿着公共列。

select max(table_name) as table_name, EMPNO, ENAME, JOB, MGR, SAL, DEPTNO
from (
       select 'EMP1' as table_name, EMPNO, ENAME, JOB, MGR, SAL, DEPTNO from EMP1
       union all
       select 'EMP2' as table_name, EMPNO, ENAME, JOB, MGR, SAL, DEPTNO from EMP2
     )
group by EMPNO, ENAME, JOB, MGR, SAL, DEPTNO
having count(*) = 1
order by EMPNO, ENAME, JOB, MGR, SAL, DEPTNO, table_name;

TABLE_NAME EMPNO ENAME      JOB          MGR    SAL   DEPTNO
---------- ----- ---------- --------- ------ ------ --------
EMP2        7499 ALLEN      SALESMAN    7698   1600       30
EMP1        7698 BLAKE      MANAGER     7839   2850       30
EMP2        7698 BLAKE      MANAGER     7839   2950       30
EMP1        7788 SCOTT      ANALYST     7566   3000       20
EMP1        7839 KING       PRESIDENT          5000       10
EMP2        7876 ADAMS      CLERK       7788   1100       20
EMP1        7902 FORD       ANALYST     7566   3000       20

 7 rows selected 

输出几乎是我们所需要的。注意第一列,它告诉我们"未配对"行来自;并注意BLAKE,他在两个表中有不同的薪水(第一列帮助我们看看他在哪个表中有多少薪水)。

到目前为止看起来很完美,但是当你有1000列时该怎么办?您可以使用"常用列"的结果将它们放在C或Java等中。上面的查询 - 或者您可以使用动态SQL在Oracle中完成所有操作。

据我所知,Oracle中SQL语句的文本长度没有限制;文档说" SQL语句的长度限制取决于许多因素,包括数据库配置,磁盘空间和内存" (可能在您的Oracle版本上,他们没有提到)。无论如何,它将超过4000个字符,因此我们需要使用CLOB。特别是,我们无法使用listagg() - 我们需要一种解决方法。我在下面使用xmlagg()。然后,文档说如果你连接文本并且至少有一个操作数是CLOB,那么结果将是CLOB;如果这对您不起作用,您可能需要将较小的文本片段包装在to_clob()中。 "动态SQL"下面的查询将生成我上面使用的查询的全文;您只需复制它并将其粘贴回前端并执行即可。您可能必须删除包装双引号等,具体取决于您的前端和设置。

首先,我们如何创建一个(可能非常长的)字符串,即常见列名列表,在最终查询中重复五次 - 再次查看"最终查询"我们曾经比较过上面的两个表。

with
     common_cols ( column_name, col_id ) as (
       select column_name, 
              min(column_id) keep(dense_rank first order by table_name) as col_id
       from   user_tab_cols
       where  table_name in ('EMP1', 'EMP2')
       group by column_name
       having count(*) = 2
     ),
     col_string ( str ) as (
       select rtrim(xmlcast(xmlagg(xmlelement(e, column_name, ', ') order by col_id)
                                                    as clob), ', ') from common_cols
     ) 
select * from col_string;

STR
-----------------------------------
EMPNO, ENAME, JOB, MGR, SAL, DEPTNO

最后是完整的动态SQL查询(结果正好是我之前在其公共列上比较EMP1EMP2的查询):

with
     common_cols ( column_name, col_id ) as (
       select column_name, 
              min(column_id) keep(dense_rank first order by table_name) as col_id
       from   user_tab_cols
       where  table_name in ('EMP1', 'EMP2')
       group by column_name
       having count(*) = 2
     ),
     col_string ( str ) as (
       select rtrim(xmlcast(xmlagg(xmlelement(e, column_name, ', ') order by col_id) 
                                                    as clob), ', ') from common_cols
     )
select 'select max(table_name) as table_name, ' || str                 || chr(10) ||
       'from ('                                                        || chr(10) ||
       '       select ''EMP1'' as table_name, ' || str || ' from EMP1' || chr(10) ||
       '       union all'                                              || chr(10) ||
       '       select ''EMP2'' as table_name, ' || str || ' from EMP2' || chr(10) ||
       '     )'                                                        || chr(10) ||
       'group by ' || str                                              || chr(10) ||
       'having count(*) = 1'                                           || chr(10) ||
       'order by ' || str || ', table_name;'  as comp_sql_str
from   col_string;