【问题标题】:PL/SQL : Need to compare data for every field in a table in plsqlPL/SQL : 需要比较 plsql 中表中每个字段的数据
【发布时间】:2020-06-06 17:45:44
【问题描述】:

我需要创建一个过程,它将集合作为输入,并将每个字段(大约 50 列)的数据与暂存表数据逐行进行比较。

业务逻辑:

  1. 每当临时表列值与相应的集合变量值不匹配时,我需要将 'FAIL' 更新到临时表 STATUS 列中,并将原因更新到该行的 REASON 列中。

  2. 如果匹配,则需要更新 STATUS 列中的 'SUCCESS'

每次调用的有效负载约为 500 行。

我创建了以下示例脚本:

PKG 规范:

CREATE OR REPLACE
PACKAGE process_data
IS
TYPE pass_data_rec
IS
  record
  (
    p_eid employee.eid%type,
    p_ename employee.ename%type,
    p_salary employee.salary%type,
    p_dept employee.dept%type 
  );

type p_data_tab IS TABLE OF pass_data_rec INDEX BY binary_integer;
PROCEDURE comp_data(inpt_data IN p_data_tab);
END;

PKG 正文:

  CREATE OR REPLACE
    PACKAGE body process_data
    IS
    PROCEDURE comp_data (inpt_data IN p_data_tab)
    IS
      status VARCHAR2(10);
      reason VARCHAR2(1000);
      cnt1   NUMBER;
      v_eid employee_copy.eid%type;
      v_ename employee_copy.ename%type;
    BEGIN
      FOR i IN 1..inpt_data.count
      LOOP
        SELECT ec1.eid,ec1.ename,COUNT(*) over () INTO v_eid,v_ename,cnt1
        FROM employee_copy ec1
        WHERE ec1.eid = inpt_data(i).p_eid;
        IF cnt1 > 0 THEN
          IF (v_eid=inpt_data(i).p_eid AND v_ename = inpt_data(i).p_ename) THEN
            UPDATE employee_copy SET status = 'SUCCESS' WHERE eid = inpt_data(i).p_eid;
          ELSE
            UPDATE employee_copy SET status = 'FAIL' WHERE eid = inpt_data(i).p_eid;
          END IF;
        ELSE
          NULL;
        END IF;
      END LOOP;
      COMMIT;
      status :='success';
    EXCEPTION
    WHEN OTHERS THEN
      status:= 'fail';
      --reason:=sqlerrm;
    END;
    END;

但在这种方法中,我有以下提到的问题。

  1. 需要为每个列值声明所有局部变量。
  2. 需要使用'and'运算符比较所有变量数据。不确定它是否正确,因为如果有 50 列,那么 if 条件会变得很重。

    IF (v_eid=inpt_data(i).p_eid AND v_ename = inpt_data(i).p_ename) THEN

  3. 当该行的任何列数据不匹配(第一个不匹配的列名)时,需要更新 REASON 列,在这种方法中我无法实现。

请提出任何其他实现此要求的好方法。

编辑:

我这边只有一张桌子,即目标桌子。输入将来自任何其他来源作为集合对象。

【问题讨论】:

  • 字段有多大?我会将所有字段值与列名连接成每行一个大字符串,然后进行比较。
  • ¿为什么输入是一个集合?¿源表在同一个数据库上?
  • @alvalongo 没有。没有源表。输入将来自 ICS 端。他们将通过以集合的形式传递多行来调用这个过程。

标签: oracle plsql collections associative-array


【解决方案1】:

修改后的答案
您可以将记录加载到 t temp 表中,但除非您需要额外的处理,否则没有必要。 AFAIK 没有办法识别有问题的列(仅限第一个),而无需逐列遍历。但是,您不必担心必须声明一个变量。您可以声明一个定义为%rowtype 的变量,它使您可以按名称访问每一列。

循环遍历一组数据以查找偶尔出现的错误是很糟糕的(恕我直言),使用 SQL 可以一举消除好的错误。它可以在这里找到。即使您的输入是一个数组,我们也可以使用 TABLE 运算符将其用作表,它允许数组(集合)就像是数据库表一样。因此,可以使用 MINUS 运算符。以下例程将设置适当的状态并识别输入数组中每个条目的第一个未匹配列。它恢复到您在包规范中的原始定义,但替换了 comp_data 过程。

create or replace package body process_data
is
    procedure comp_data (inpt_data in p_data_tab)
    is
      -- define local array to hold status and reason for ecah.
      type status_reason_r is record
           ( eid    employee_copy.eid%type 
           , status employee_copy.status%type
           , reason employee_copy.reason%type
           );          
      type status_reason_t is
           table of status_reason_r
           index by pls_integer;
      status_reason status_reason_t := status_reason_t();

      -- define error array to contain the eid for each that have a mismatched column  
      type error_eids_t is table of employee_copy.eid%type ;
      error_eids error_eids_t; 
      current_matched_indx pls_integer;

      /*
        Helper function to identify 1st mismatched column in error row.
        Here is where we slug our way through each column to find the first column
        value mismatch. Note: There is actually validate the column sequence, but 
        for purpose here we'll proceed in the input data type definition.
      */
      function identify_mismatch_column(matched_indx_in pls_integer)
        return varchar2
      is
          employee_copy_row employee_copy%rowtype;
          mismatched_column employee_copy.reason%type;
      begin
          select * 
            into employee_copy_row
            from employee_copy
           where employee_copy.eid = inpt_data(matched_indx_in).p_eid;

          -- now begins the task of finding the mismatched column.
          if employee_copy_row.ename !=  inpt_data(matched_indx_in).p_ename
          then 
             mismatched_column := 'employee_copy.ename';
          elsif employee_copy_row.salary !=  inpt_data(matched_indx_in).p_salary 
          then  
             mismatched_column := 'employee_copy.salary';
          elsif employee_copy_row.dept !=  inpt_data(matched_indx_in).p_dept 
          then 
             mismatched_column := 'employee_copy.dept'; 
         -- elsif continue until ALL columns tested
          end if; 

          return  mismatched_column;

      exception
          -- NO_DATA_FOUND is the one error that cannot actually be reported in the customer_copy table.
          -- It occurs when an eid exista in the input data but does not exist in customer_copy.
          when NO_DATA_FOUND 
          then 
              dbms_output.put_line( 'Employee (eid)=' 
                                  || inpt_data(matched_indx_in).p_eid
                                  || ' does not exist in employee_copy table.'
                                  );
              return 'employee_copy.eid ID is NOT in table';
      end identify_mismatch_column;

      /*   
        Helper function to find specified eid in the initial inpt_data array
        Since the resulting array of mismatching eid derive from a select without sort
        there is no guarantee the index values actually match. Nor can we sort to build 
        the error array, as there is no way to know the order of eid in the initial array.
        The following helper identifies the index value in the input array for the specified 
        eid in error.
      */
      function match_indx(eid_in employee_copy.eid%type)
        return pls_integer
      is
          l_at        pls_integer := 1;
          l_searching boolean     := true;
      begin
          while l_at <= inpt_data.count
          loop 
             exit when eid_in = inpt_data(l_at).p_eid;
             l_at := l_at + 1; 
          end loop; 
          if l_at > inpt_data.count
          then  
             raise_application_error( -20199, 'Internal error: Find index for ' || eid_in ||' not found');
          end if; 
          return l_at;
      end match_indx;


    -- Main     
    begin
      -- initialize status table for each input enter 
      -- additionally this results is a status_reason table in a 1:1 with the input array.
      for i in 1..inpt_data.count
      loop
        status_reason(i).eid    := inpt_data(i).p_eid;
        status_reason(i).status :='SUCCESS';
      end loop;

      /*
         We can assume the majority of data in the input array is valid meaning the columns match.
         We'll eliminate all value rows by selecting each and then MINUSing those that do match on 
         each column. To accomplish this cast the input with TABLE function allowing it's use in SQL.
         Following produces an array of eids that have at least 1 column mismatch.
      */        
      select p_eid
        bulk collect into error_eids 
        from (select p_eid, p_ename, p_salary, p_dept from TABLE(inpt_data) 
              minus
              select eid, ename, salary, dept from employee_copy
             )  exs;

      /*
         The error_eids array now contains the eid for each miss matched data item.
         Mark the status as failed, then begin the long hard process of identifying 
         the first column causing the mismatch.
         The following loop used the nested functions to slug the way through. 
         This keeps the main line logic clear.
      */
      for i in 1 .. error_eids.count  -- if all inpt_data rows match then count is 0, we bypass the enttire loop
      loop
         current_matched_indx                       := match_indx(error_eids(i)); 
         status_reason(current_matched_indx).status := 'FAIL';
         status_reason(current_matched_indx).reason := identify_mismatch_column(current_matched_indx);
      end loop; 

      -- update employee_copy with appropriate status for each row in the input data.
      -- Except for any cid that is in the error eid table but doesn't exist in the customer_copy table.
      forall i in inpt_data.first .. inpt_data.last 
          update employee_copy
             set status = status_reason(i).status
               , reason = status_reason(i).reason
           where eid = inpt_data(i).p_eid;

    end comp_data;
end process_data;

如果您不熟悉其他一些技术,您可能想了解它们:

  1. 嵌套函数。该过程中定义和使用了 2 个函数。
  2. 批量处理。这就是 Bulk Collect and Forall。

祝你好运。

原始答案
不必比较每一列,也不必通过连接来构建字符串。正如您所指出的,比较 50 列变得非常繁重。因此,让 DBMS 完成大部分工作。使用 MINUS 运算符可以满足您的需要。

... MINUS 运算符,它只返回由 第一个查询,但不是第二个。

使用该任务只需要 2 次更新:1 次标记“失败”,1 次标记“成功”。所以试试:

create table e( e_id integer
              , col1 varchar2(20)
              , col2 varchar2(20)
              ); 
create table stage ( e_id integer
                   , col1 varchar2(20)
                   , col2 varchar2(20)
                   , status varchar2(20)
                   , reason varchar2(20)
                   );

-- create package spec and body
create or replace package process_data
is   
    procedure comp_data;
end process_data; 

create or replace package body process_data
is
    package body process_data   
    procedure comp_data 
    is
    begin  
        update stage 
           set status='failed'
             , reason='No matching e row'
         where e_id in ( select e_id 
                          from (select e_id, col1, col2 from stage
                                except
                                select e_id, col1, col2 from e
                               )  exs                     
                       );
        update stage 
           set status='success'
         where status is null; 
    end comp_data;
end process_data;   

-- test 
-- populate tables  
insert into e(e_id, col1, col2)  
   select (1,'ABC','def')       from dual union all
   select (2,'No','Not any')    from dual union all      
   select (3,'ok', 'best ever') from dual union all
   select (4,'xx','zzzzzz')     from dual;

insert into stage(e_id, col1, col2)
   select (1,'ABC','def')         from dual union all
   select (2,'No','Not any more') from dual union all
   select (4,'yy', 'zzzzzz')      from dual union all
   select (5,'no e','nnnnn')      from dual;

-- run procedure

begin 
    process_data.comp_date; 
end; 

-- check results
select * from stage;

别问了。是的,您必须列出您希望在 MINUS 操作中涉及的每个查询中进行比较的每一列
我知道文档链接很旧(10gR2),但实际上查找 Oracle 文档是一件非常痛苦的事情。但是 MINUS 运算符在 19c 中的功能仍然相同;

【讨论】:

  • 抱歉,我错过了提及输入将来自 ICS api 作为集合。所以在我的最后只有一个表,即在您的示例“STAGE”表中。所以基本上这里需要在输入集合记录与 STAGE 表记录之间进行比较。并且还应该确定哪个列值不匹配。如果多列值不匹配,则需要更新 REASON 中第一个不匹配的列名。
  • 如果我将收集记录存储到临时表中然后使用减号进行比较会更好吗?但是我们仍然无法确定哪个列值不匹配。
  • 我已经修改了答案。它使用您的输入数组,但使用 MINUS 语句作为表。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-09-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多