본문 바로가기
기타

[MySQL] 오라클(Oracle) Merge into 구문 따라하기 - 중복키 (Duplicate key) 처리

by A6K 2021. 8. 2.

오라클을 사용하면 'Merge into' 구문이라는 편리한 DML 구문을 사용할 수 있다. PK 혹은 Unique Index가 생성되어 있는 테이블에 데이터를 입력할 때, PK나 Unique Key에 해당하는 값이 이미 테이블에 존재하면 "Error Code: 1062. Duplicate entry '??' for key PRIMARY" 같은 에러가 발생한다.

이 경우 Insert 구문 대신 update 구문을 사용해서 기존에 있는 데이터를 업데이트해야한다. 이 때, 테이블에 데이터가 존재하는지 조회하고 없으면 insert, 있으면 update로 동작하도록 해야한다.

하지만 동시성 이슈로 select 해서 존재유무를 판단하고 insert 혹은 update 하는 사이에 다른 세션에서 새로운 값을 insert 하는 경우가 있다. 이른바 phantom read 현상이 발생할 수 있는 것이다.

빨간색으로 표시한 insert 구문은 중복키가 있다는 에러를 만나게 될 것이다. 

이를 해소하기 위해서는 테이블에 락을 잡아야한다. 

테이블 혹은 업데이트하려는 Row에 락을 잡아놓고 select와 insert 사이에 Critical Section을 잡아놓으면 Phantom Read 현상이 발생하지 않게 된다. 하지만 Lock을 잡는 행위는 성능저하나 데드락 등의 문제를 일으킬 가능성이 있다.

DBMS 서버에서 이런 문제를 해결할 수 있는 장치를 제공하면 제일 좋다. 오라클(Oracle)에서는 Merge Into라는 구문을 제공해서 이런 문제를 해소할 수 있게 해준다. 오라클의 Merge Into 구문은 다음과 같다.

MERGE INTO [table_name alias]
USING [table | view | subquery]
ON [joint condition]
WHEN MATCHED THEN
    UPDATE SET column1 = value1, column2 = value2 ...
WHEN NOT MATCHED THEN
    INSERT (column1, column2, ... ) VALUES (values1, values2, ...);

merge into 구문은 오라클 9i부터 추가된 기능이며, 오라클 10g부터는 UPDATE 대신 DELETE도 사용할 수 있게 되었지만 이 포스트에서는 다루지 않겠다.


MySQL에서의 merge into 구문

안타깝게도 MySQL에서는 merge into 구문을 제공하지 않는다. 대신 비슷한 기능을 하는 몇 가지 문법이 있다.

  1. INSERT IGNORE
  2. REPLCATE INTO
  3. INSERT ... ON DUPLCATE UPDATE

각각을 테스트하기 위해서 테스트용 테이블을 만들고 데이터를 채워보자

CREATE TABLE `t1` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `entry_name` varchar(16) DEFAULT NULL,
 `size` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`),
 UNIQUE KEY `entry_name_UNIQUE` (`entry_name`)
);

INSERT INTO `T1` VALUES (NULL, 'ENTRY1', 20);

 

id 컬럼에 PK가 걸려있고, entry_name에 UniqueKey 인덱스가 걸려있다. 이 테이블에 Row 하나를 입력해 놨다.

1
2
3
4
5
6
7
8
mysql> select * from t1;
+----+------------+------+
| id | entry_name | size |
+----+------------+------+
|  1 | ENTRY1     |   20 |
+----+------------+------+
 
1 row in set (0.00 sec) 
cs

이제 이 테이블에 중복된 키를 INSERT 하면서 테스트를 해보자.

INSERT IGNORE 구문

INSERT IGNORE 구문은 중복키 에러를 무시한다. INSERT 동작을 수행하되 중복된 데이터가 없으면 그냥 INSERT 동작을 하고, 중복된 데이터가 있으면 아무것도 안한다. 

insert into `t1` values (null, 'ENTRY1', 21);

만약 중복된 데이터를 입력하면 "0 row affected, 1 warnings"라는 결과 메시지가 출력된다. 실제로 영향을 받은 ROW는 없으며 대신 Warning이 하나 발생했다는 의미다.

물론 UNIQUE Constraint를 위반하지 않는 INSERT 구문을 실행해보면 정상적으로 수행된다.

mysql> insert into `t1` values (null, 'ENTRY2', 21);
Query OK, 1 row affected (0.01 sec)

 

테이블에 쿼리를 날려보면 당연히

mysql> select * from t1;
+----+------------+------+
| id | entry_name | size |
+----+------------+------+
|  1 | ENTRY1     |   20 |
|  4 | ENTRY2     |   21 |
+----+------------+------+
2 rows in set (0.00 sec)

정상적으로 새로운 Row가 추가된 것을 볼 수 있다. 유의해야 할 점은 AUTO_INCREMENT 값은 ERROR가 발생하거나 IGNORE 되었을 때에도 증가한다는 점이다. 위 테이블에서 id 값이 4로 되어 있다.

REPLACE INTO

REPLACE INTO 구문은 오라클의 MERGE INTO 구문과 비슷하다. MySQL의 REPLACE INTO 구문은 UNIQUE KEY에 해당하는 데이터가 없으면 일반 INSERT처럼 동작하고, 있으면 DELETE & INSERT를 수행한다.

REPLACE INTO 구문을 테스트해보면,

mysql> select * from t1;
+----+------------+------+
| id | entry_name | size |
+----+------------+------+
|  1 | ENTRY1     |   20 |
|  4 | ENTRY2     |   21 |
+----+------------+------+
2 rows in set (0.00 sec) 

mysql> replace into t1 values (NULL, 'ENTRY3', 22);
Query OK, 1 row affected (0.01 sec)

mysql> select * from t1;
+----+------------+------+
| id | entry_name | size |
+----+------------+------+
|  1 | ENTRY1     |   20 |
|  4 | ENTRY2     |   21 |
|  5 | ENTRY3     |   22 |
+----+------------+------+
3 rows in set (0.00 sec)

 

존재하지 않는 새로운 ROW에 대해서는 일반 INSERT처럼 값을 추가한다.

mysql> replace into t1 values (NULL, 'ENTRY1', 23);
Query OK, 2 rows affected (0.01 sec)

mysql> select * from t1;
+----+------------+------+
| id | entry_name | size |
+----+------------+------+
|  4 | ENTRY2     |   21 |
|  5 | ENTRY3     |   22 |
|  6 | ENTRY1     |   23 |
+----+------------+------+
3 rows in set (0.00 sec)

이미 존재하는 ROW에 대해서 DELETE & INSERT처럼 동작한다. REPLACE INTO 구문의 결과로 "2 rows affected"라는 메시지를 보면, id 컬럼 기준으로 1번 ROW가 지워지고, 6번 ROW가 INSERT된 것을 확인할 수 있다. 기존의 ROW가 지워지고 새로운 ROW가 INSERT 되었으므로 AUTO_INCREMENT 컬럼 값이 증가한 채로 INSERT 되었다.

INSERT ... ON DUPLICATE KEY UPDATE

이 구문은 INSERT를 수행하되 DUPLICATE 값이 존재하면 UPDATE 뒤에 명시한 동작을 취하라는 구문이다. 이어서 테스트를 해보자.

mysql> insert into t1  values (NULL, 'ENTRY4', 24) ON DUPLICATE KEY UPDATE size = 24;
Query OK, 1 row affected (0.01 sec)

mysql> select * from t1;
+----+------------+------+
| id | entry_name | size |
+----+------------+------+
|  4 | ENTRY2     |   21 |
|  5 | ENTRY3     |   22 |
|  6 | ENTRY1     |   23 |
|  7 | ENTRY4     |   24 |
+----+------------+------+
4 rows in set (0.00 sec)

이 구문도 DUPLICATE KEY에러가 발생하지 않으면 일반 INSERT 처럼 동작한다.

중복키를 INSERT 하는 테스트를 해보면

mysql> insert into t1  values (NULL, 'ENTRY3', 25) ON DUPLICATE KEY UPDATE size = 25;
Query OK, 2 rows affected (0.01 sec)

mysql> select * from t1;
+----+------------+------+
| id | entry_name | size |
+----+------------+------+
|  4 | ENTRY2     |   21 |
|  5 | ENTRY3     |   25 |
|  6 | ENTRY1     |   23 |
|  7 | ENTRY4     |   24 |
+----+------------+------+
4 rows in set (0.00 sec)

INSERT 대신 UPDATE 처럼 동작한 것을 확인할 수 있다. ENTRY3에 대해서 INSERT를 수행하면 DUPLICATE KEY에러가 발생한다. 하지만 ON DUPLICATE KEY UPDATE 구문을 이용해서 에러가 발생하는 대신 특정 컬럼을 UPDATE 할 수 있다.

주목해야 할 점은 AUTO_INCREMENT로 지정한 id 값은 변하지 않았다는 것이다. 단순히 특정 컬럼을 UPDATE한 것처럼 동작하기 때문에 id 컬럼의 값은 보존된다.

REPLACE INTO 구문과 비교해보면 ON DUPLICATE KEY UPDATE 구문을 이용한 경우 컬럼하나만 UPDATE하기 때문에 좀 더 빠르게 수행된다.

본 포스트에서 수행한 예제 코드들은 MySQL 14.14 버전에서 수행되었다.

$ mysql --version
mysql  Ver 14.14 Distrib 5.7.22, for osx10.13 (x86_64) using  EditLine wrapper

댓글