1

我需要将一些值合并到一个表中,当具有指定键的行已经存在时更新一个字段,或者如果它不存在则插入一个新行。

这是我的桌子:

profiles(name, surname, active);

在哪里:

name    VARCHAR2(30)
surname VARCHAR2(30)
active  NUMBER(1)

name and surname -> composite primary key

我正在使用这个查询:

MERGE INTO profiles USING (
    SELECT
        'Mark' myName,
        'Zibi' mySurname,
        '1'    myActive
   FROM DUAL
) ON (
   name = myName
   AND surname = mySurname
)
WHEN MATCHED THEN
    UPDATE SET
        active = myActive
WHEN NOT MATCHED THEN
    INSERT (
        name,
        surname,
        active
    ) VALUES (
        myName,
        mySurname,
        myActive
    );

它可以工作,但即使active已设置为,它也会更新记录1

我想做的是这样的:

WHEN MATCHED THEN
    IF(active != myActive)
        UPDATE SET
            active = myActive
    ELSE
        RAISE CUSTOM EXCEPTION
WHEN NOT MATCHED THEN
    INSERT [...]

那可能吗?AFAIK 我不能把if这样的MERGE声明写成这样,那怎么办呢?

4

4 回答 4

2

Using PL/SQL to Run a Conditional Merge Operation

Edit: The original post asks how to process an existing set of data into an established table (named: PROFILES) through an approach that SQL or PL/SQL can solve it.

Edit Again: The last comment from OP was pretty subtle. If you don't have direct SQL access, then you will need a CURSOR, a driving query or some other construct to process each of the records your feeding in anyways. Many JDBC based middle-ware components also accept cursors as inputs. You could feed in all your data in one procedure call... take a look at REF CURSOR data types in PL/SQL. If that is the case, this solution can still help.

Using a composite join key, update data in a target table based on multiple criteria:

  1. INSERT source data if it does not exist already.
  2. Toggle or UPDATE a status value if the person identifier (name + surname) exists.
  3. If person already exists in the target table and has an 'active' status already, skip it.

Sample Data

I named my tables slightly different and modified the column name "name" which is a reserved sql/plsql keyword... to prevent any possible future conflicts.

The sample data insert statements (DML):

*For clarity: The names in the test schema are not an exact match to the OP. STACK_PROFILES = PROFILES and STACK_PROFILE_MERGE_SOURCE represents "some source"... this could have been an xml feed, a csv text file, etc.etc.

from: load_profile_data.sql...

CREATE TABLE "STACK_PROFILES" ( "PROFILE_NAME" VARCHAR2(40), "SURNAME" VARCHAR2(40), "ACTIVE" NUMBER(1,0), CONSTRAINT "STACK_PROFILES_PK" PRIMARY KEY ("PROFILE_NAME", "SURNAME") ENABLE )

INSERT INTO STACK_PROFILES (profile_name, surname, active) VALUES ('LOIS' , 'LAINE', 0); INSERT INTO STACK_PROFILES (profile_name, surname, active) VALUES ('MARTIN', 'SHORT', 1); INSERT INTO STACK_PROFILES (profile_name, surname, active) VALUES ('ROBIN' , 'WILLIAMS', 0); INSERT INTO STACK_PROFILES (profile_name, surname, active) VALUES ('GRACE' , 'HOPPER', 0); INSERT INTO STACK_PROFILES (profile_name, surname, active) VALUES ('LOIS' , 'LAINE-KENT', 0);

commit; ...

CREATE TABLE "STACK_PROFILE_MERGE_SOURCE" ( "PROFILE_NAME" VARCHAR2(40), "SURNAME" VARCHAR2(40), CONSTRAINT "STACK_PROFILE_MERGE_SOURCE_PK" PRIMARY KEY ("PROFILE_NAME", "SURNAME") ENABLE ) /

INSERT INTO STACK_PROFILE_MERGE_SOURCE (profile_name, surname) VALUES ('BRUCE' , 'WAYNE'); INSERT INTO STACK_PROFILE_MERGE_SOURCE (profile_name, surname) VALUES ('SPONGE' , 'ROBERT'); INSERT INTO STACK_PROFILE_MERGE_SOURCE (profile_name, surname) VALUES ('CLARK' , 'KENT'); INSERT INTO STACK_PROFILE_MERGE_SOURCE (profile_name, surname) VALUES ('LOIS' , 'LAINE'); INSERT INTO STACK_PROFILE_MERGE_SOURCE (profile_name, surname) VALUES ('MARTIN' , 'SHORT'); INSERT INTO STACK_PROFILE_MERGE_SOURCE (profile_name, surname) VALUES ('DAMON' , 'WAYANS'); INSERT INTO STACK_PROFILE_MERGE_SOURCE (profile_name, surname) VALUES ('ROBIN' , 'WILLIAMS'); INSERT INTO STACK_PROFILE_MERGE_SOURCE (profile_name, surname) VALUES ('BRUCE' , 'WILLIS'); INSERT INTO STACK_PROFILE_MERGE_SOURCE (profile_name, surname) VALUES ('DENNIS' , 'HOPPER'); INSERT INTO STACK_PROFILE_MERGE_SOURCE (profile_name, surname) VALUES ('WHOOPI' , 'GOLDBERG'); INSERT INTO STACK_PROFILE_MERGE_SOURCE (profile_name, surname) VALUES ('GRACE' , 'HOPPER'); INSERT INTO STACK_PROFILE_MERGE_SOURCE (profile_name, surname) VALUES ('JERI' , 'RYAN');

Test Cases

It's helpful to understand the requirements presented. Writing up a few test cases gets us closer.

For test cases 1 and 2...

Conditional Merge Test Cases 1,2

For test cases 3 and 4...

Conditional Merge Test Cases 3,4

The PL/SQL Source Code

There is a simpler way to apply additional conditional logic through a SQL-merge like function. The PL/SQL Anonymous block following uses outer join syntax to identify records to be inserted vs. updated. The third category (active and already present in the target table) is also observed as the cursor processing loop skips records of that definition.

The processing loop and cursor

We use the FOR UPDATE and WHERE CURRENT OF syntax in the dml operations because the state of data referenced within this query changes during the lifespan of its use.

 declare
    c_default_status_active   constant number:= 1;
    c_status_inactive         constant number:= 0;

    cursor profile_cur is
       select sp.profile_name as target_name, 
              sp.surname as target_surname, sp.active as original_status,
              spm.profile_name as source_name, spm.surname as source_surname

         from stack_profiles sp, stack_profile_merge_source spm
        where spm.profile_name = sp.profile_name(+)
          and spm.surname = sp.surname(+)
        order by spm.profile_name asc nulls last, 
          spm.surname asc
          for update of sp.profile_name, sp.surname, sp.active;

        v_rec_profile  profile_cur%ROWTYPE;

     begin

      open profile_cur;
     fetch profile_cur into v_rec_profile;

     while profile_cur%found loop
       -- insert condition (no match in outer join...)
       if v_rec_profile.original_status is null
       then
       insert into stack_profiles (profile_name, surname, active)
       values (v_rec_profile.source_name, v_rec_profile.source_surname, 
           c_default_status_active);

       elsif
       -- flip status from inactive to active for existing but 
       -- inactive records.
       v_rec_profile.original_status = c_status_inactive then
       update stack_profiles
          set active = c_default_status_active
        where current of profile_cur;
          end if;

      fetch profile_cur into v_rec_profile;
      end loop;
      close profile_cur;

commit;

end;

Discussion

I have noted many different approaches to this type of problem. The specific approach used here is to demonstrate the concept involved. Results may vary depending on the database configuration, its usage and set up.

于 2014-06-26T11:35:55.373 回答
1

在这种情况下,最好通过存储过程或仅通过从客户端执行匿名 SQL 块而不是单个MERGESQL 语句来使用 PL/SQL。

匿名 PL/SQL 块可能如下所示:

declare
  -- Parameters of query, initialization values  
  pName    profiles.name%type    := 'Mark';
  pSurname profiles.surname%type := 'Zibi';
  pActive  profiles.active%type  := 0;

  -- variable used for test against table
  vIsActiveInDb profiles.active%type;
begin

  select 
    max(profs.active) into vIsActiveInDb
  from 
    profiles profs
  where 
    profs.name = pName and profs.surname = pSurname
  ;

  if(vIsActiveInDb is null) then
    -- profile not found, create new one 
    insert into profiles(name, surname, active)
    values(pName, pSurname, pActive);

  elsif(vIsActiveInDb != pActive) then
    -- profile found, activity flag differs 
    update profiles set active = pActive 
    where name = pName and surname = pSurname;

  else
    -- profile found with same activity flag
    raise_application_error(
      -20001, -- custom error code from -20000 to -20999
      'Profile "'||pName||' '||pSurname||'" already exists with same activity flag'
    );  
  end if;

end;

SQLFiddle

上面的代码有两个建议:
1. (name, surname)pair 是主键,所以总是选择单行或者什么都不选;
2.active字段不能为空(例如使用not null约束创建)。
如果此建议失败,代码会稍微复杂一些。此变体可在this SQLFiddle.

我从未使用过MyBatis,但根据您对此类查询的评论 XML 描述的回答可能如下所示:

<update id="UpdateProfileActivity" parameterType="map" statementType="CALLABLE">   
  declare
    -- Parameters of query, initialization values
    pName    profiles.name%type    := #{piName,    mode=IN, jdbcType=VARCHAR};
    pSurname profiles.surname%type := #{piSurname, mode=IN, jdbcType=VARCHAR};
    pActive  profiles.active%type  := #{piActivity,mode=IN, jdbcType=NUMERIC};

    -- variable used for test against table
    vIsActiveInDb profiles.active%type;   begin

    select
      max(profs.active) into vIsActiveInDb
    from
      profiles profs
    where
      profs.name = pName and profs.surname = pSurname
    ;

    if(vIsActiveInDb is null) then
      -- profile not found, create new one
      insert into profiles(name, surname, active)
      values(pName, pSurname, pActive);

    elsif(vIsActiveInDb != pActive) then
      -- profile found, activity flag differs
      update profiles set active = pActive
      where name = pName and surname = pSurname;

    else
      -- profile found with same activity flag
      raise_application_error(
        -20001, -- custom error code from -20000 to -20999
        'Profile "'||pName||' '||pSurname||'" already exists with same activity flag'
      );
    end if;

  end; 
</update>
于 2014-06-26T11:42:25.817 回答
0

好的,我想这不是一个好习惯,但由于您的 ACTIVE 列的类型为 NUMBER(1),您只需尝试将其值更新为更大的值,即可轻松生成 ORA-01438 异常。例如,如果 active 的新旧值相等,这样的事情将引发异常:

MERGE INTO profiles USING (
    SELECT
        'Mark' myName,
        'Zibi' mySurname,
        1    myActive
   FROM DUAL
) ON (
   name = myName
   AND surname = mySurname
)
WHEN MATCHED THEN
    UPDATE SET
        active =  CASE WHEN active = myActive THEN 11 ELSE myActive END
WHEN NOT MATCHED THEN
    INSERT (
        name,
        surname,
        active
    ) VALUES (
        myName,
        mySurname,
        myActive
    );
于 2014-06-25T14:25:55.130 回答
0

您可以通过where在 source -using (---- subquery ---) 上添加条件来执行此操作,以在匹配命令时进行过滤,或者在不匹配时添加 where 条件。

在下面的示例中,我将合并从 id 520 到 530 的记录,同时我不会插入 id =525 的记录

--------
merge into merchant_tmp2 dest
using (select * from merchant where id between 520 and 530) src
on(dest.id=src.id)
when matched then 
update set address=address ||' - updated'
when not matched then 
insert (ID,....)
values (src.ID,....)
where src.id <> 525;

参考:https ://docs.oracle.com/cd/B28359_01/server.111/b28286/statements_9016.htm#SQLRF01606

于 2018-11-07T10:32:33.393 回答