PL/SQL: необходимо сравнить данные для каждого поля в таблице в plsql.

Мне нужно создать процедуру, которая будет принимать коллекцию в качестве входных данных и сравнивать данные с данными промежуточной таблицы построчно для каждого поля (около 50 столбцов).

Бизнес-логика:

  1. всякий раз, когда значение столбца промежуточной таблицы не совпадает с соответствующим значением переменной коллекции, мне нужно обновить 'FAIL' в столбце STATUS промежуточной таблицы и указать причину в столбце REASON для этой строки.

  2. В случае совпадения необходимо обновить 'SUCCESS' в столбце STATUS.

Полезная нагрузка будет составлять около 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. Необходимо сравнить все переменные данные с помощью оператора «и». Не уверен, правильно это или нет, потому что если будет 50 столбцов, то условие станет очень тяжелым.

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

  3. Необходимо обновить столбец REASON, когда какие-либо данные столбца не совпадают (первое несоответствующее имя столбца) для этой строки, в этом подходе я не могу достичь.

Пожалуйста, предложите любой другой хороший способ выполнить это требование.

Изменить:

На моем конце есть только одна таблица, т.е. целевая таблица. Ввод будет поступать из любого другого источника в качестве объекта коллекции.


person SKG    schedule 22.02.2020    source источник
comment
Насколько велики поля? Я бы объединил все значения полей вместе с именем столбца в одну большую строку для каждой строки, а затем сравнил бы это.   -  person OldProgrammer    schedule 22.02.2020
comment
¿Почему ввод является коллекцией?¿исходная таблица находится в той же базе данных?   -  person alvalongo    schedule 22.02.2020
comment
@alvalongo Нет, исходной таблицы нет. Ввод будет поступать со стороны ICS. Они будут вызывать эту процедуру, передавая несколько строк в виде коллекции.   -  person SKG    schedule 23.02.2020


Ответы (1)


ИСПРАВЛЕННЫЙ ответ
Вы можете загрузить записи во временную таблицу, но если вам не нужна дополнительная обработка, в этом нет необходимости. Насколько я знаю, невозможно определить столбец-нарушитель (только первый), не просматривая столбец за столбцом. Тем не менее, ваша другая проблема, связанная с объявлением переменной, не требуется. Вы можете объявить одну переменную, определенную как %rowtype, которая дает вы получаете доступ к каждому столбцу по имени.

Циклический просмотр массива данных для поиска случайных ошибок просто плох (имхо) с SQL, доступным для устранения хороших ошибок одним махом. И это доступно здесь. Несмотря на то, что ваш ввод представляет собой массив, мы можем использовать его как таблицу с помощью оператора TABLE, который допускает массив (коллекцию), как если бы это была таблица базы данных. Таким образом, оператор МИНУС может использоваться до сих пор. Следующая процедура установит соответствующий статус и определит первый столбец с пропущенным соответствием для каждой записи во входном массиве. Он возвращается к исходному определению в спецификации пакета, но заменяет процедуру 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 и Forall.

Удачи.

ИСХОДНЫЙ ответ
Нет необходимости сравнивать каждый столбец или строить строку путем объединения. Как вы указали, сравнение 50 столбцов становится довольно тяжелым. Так что пусть СУБД сделает большую часть работы. Использование оператора 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 — это настоящая головная боль. Но оператор МИНУС по-прежнему работает в 19c;

person Belayer    schedule 22.02.2020
comment
Извините, я не упомянул, что входные данные будут поступать из API ICS в виде коллекции. Итак, на моем конце есть только одна таблица, т. Е. В вашем примере таблица «STAGE». Таким образом, в основном здесь необходимо сравнение между записями входной коллекции с записями таблицы STAGE. А также следует определить, какое значение столбца не соответствует. если в случае несовпадения значений нескольких столбцов необходимо обновить первое несоответствующее имя столбца в REASON. - person SKG; 23.02.2020
comment
Будет ли хорошо, если я сохраню записи коллекции во временной таблице, а затем сравню с минусом? Но все же мы не можем определить, какое значение столбца не соответствует. - person SKG; 23.02.2020
comment
Я пересмотрел ответ. Он использует ваш входной массив, но делает это как таблицу с оператором MINUS. - person Belayer; 25.02.2020