MySQL에서 Temporary Table을 활용한 데이터 질의..그 효과는?

Overview

오늘은 Temporary Table에 관해 포스팅을 하겠습니다. Select및 Update 등을 이따금씩 Temporary Table을 활용하여 수행하는 경우가 있습니다. 동시에 많은 데이터를 일괄 변경하는 것에서는 분명 강점이 있을 것이라 판단되는데, 어떤 상황에서 적절하게 사용하는 것이 좋을까요? 관련 성능 벤치마크 결과를 공개하겠습니다.

Environment

테이블에는 약 1000만 건 데이터가 존재하며, Primary Key외에는 추가 인덱스는 생성하지 않았습니다. 서로 동등하게 빠른 데이터 접근이 가능하다는 가정 하에 PK외 인덱스에서 발생할 수 있는 성능 저하 요소를 배제하기 위해서 입니다.^^

## DDL for dbatest
CREATE TABLE dbatest (
  i int(11) NOT NULL AUTO_INCREMENT,
  c1 int(11) NOT NULL,
  c2 int(11) NOT NULL,
  c3 varchar(255) DEFAULT NULL,
  PRIMARY KEY (i),
) ENGINE=InnoDB;

## Table Infomation
+-------------+----------+-------+-------+------------+
| TABLE_NAME  | ROWS     | DATA  | IDX   | TOTAL_SIZE |
+-------------+----------+-------+-------+------------+
| dba.dbatest | 10000105 | 1283M | 0.00M | 1283.00M   |
+-------------+----------+-------+-------+------------+

성능 테스트 시 Temporary Table은 아래와 패턴(tmp_dbatest_세션번호)으로 DB 세션 단위로 정의하여 성능을 측정하였습니다. 물론 Temporary Table이 필요한 부분에서만 사용 되겠죠.^^

Memory Storage 엔진은 테이블 Lock으로 동작하지만, 자신의 Temporary Table은 자신의 세션에서만 사용하기 때문에 동시에 여러 세션에서 읽히게 되는 그런 경우는 없다고 봐도 무관합니다.

## DDL for Temporary Table
CREATE TEMPORARY TABLE tmp_dbatest_12(
  i int not null,
  primary key(i)
) engine = memory;

대상 테이블에는 앞에서 언급한 것과 같이 약 1,000만 건 데이터가 들어있으며, Primary Key는 1부터 순차적으으로 정의도어 있습니다.

트래픽은 Java Thread를 여러 개 발생하여 마치 실 서비스 상태와 유사한 환경을 조성하였으며, 5개 세션(쓰레드)부터 단계적으로 200개까지 세션 수를 늘려서 초당 트랜잭션 수(TPS)를 측정하였습니다.

디스크 지연으로 인한 영향을 최소화하기 위해서 메모리는 충분히 할당하였고, 데이터 생성 후에는 DB Restart를 배제함으로써 모든 데이터를 메모리에 있다고 가정하였습니다. (테스트 시 리소스 현황을 확인하면서 Disk I/O로 인한 Bottleneck이 없음을 어느정도 확신하습니다.)

테스트는 기본적으로 하나의 트랜잭션에서 20건의 데이터 Select 또는 Update를 얼마나 효율적으로 질의할 수 있느냐에 초점을 두었습니다.

자! 이제 성능 테스트 결과를 공개합니다.

Benchmark Result : Select

Temporary Table 사용 유무를 나누어서 테스트를 진행하였습니다.

Temporary Table

  1. Drop Temporary Table
  2. Create Temporary Table
  3. 1부터 10,000,000 범위 숫자 20개 무작위 추출
  4. 다음과 같이 Temporary Table과 Join하여 데이터 Select
SELECT a.i, a.c1, a.c2
FROM dbatest a
INNER JOIN tmp_dbatest_12 b ON a.i = b.i

None Temporary Table

  1. 1부터 10,000,000 범위 숫자 20개 무작위 추출
  2. 다음과 같이 IN 구문을 사용하여 Select
SELECT a.i, a.c1, a.c2
FROM dbatest a
WHERE a.i in (1,2,3,4,..,17,18,19,20)
Performance Test Select Result
Performance Test Select Result

동시 접속 수가 많아질수록 Temporary Table 없이 사용하는 것이 성능이 월등히 좋았습니다.

위 케이스에서는 상당히 예상 가능한 결과로, Create/Drop/Insert/Select 등 네 가지 질의가 동일 트랜잭션에서 발생하기 때문 요인으로 볼 수 있겠죠. 하지만 만약 추후 기 저장된 데이터를 재활용한다면 상당히 다른 결과를 보여줄 수 있겠죠? (예를 들어 통계 중간 단계 혹은 자주 읽히는 데이터 임시 저장이라든가..)

IN 구문 성능이 1,000건 Select에서는 어느정도 영향이 있지 않을까라는 생각이 들어서 1,000 건 동시 데이터 질의 트래픽을 발생하여 측정하였습니다.

테스트 결과는 하단과 같으며, 여전히 Temporary Table 없이 사용하는 것이 성능이 더 좋았습니다. 앞선 결과와 차이가 크지 않은 것은 Select 쿼리 자체의 부하가 늘어났기 때문으로 파악할 수 있습니다.

Performance Test 1000 Rows Select Result
Performance Test 1000 Rows Select Result

그리고 테스트 도중  “Query End” 상태로 약 20초 대기상태에 빠지는 경우가 발생하였습니다. 아마도 내부 메모리 경합으로 인한 문제가 아닐까 추측해봅니다.

Performance Test Wait

Benchmark Result : Update

앞선 테스트 방식과 동일하게 Update에서도 Temporary Table 사용 유무에 따라 구분하였고, 동시에 20 Row의 데이터 변경하는 것에 초점을 맞추었습니다.

Temporary Table

  1. Drop Temporary Table
  2. Create Temporary Table
  3. 1부터 10,000,000 범위 숫자 20개 무작위 추출
  4. 다음과 같이 Temporary Table과 Join하여 데이터 Update
UPDATE dbatest a
INNER JOIN tmp_dbatest_12 b ON a.i = b.i
SET a.c1 = a.c1 +10, a.c2 = a.c2 + 10000

None Temporary Table

  1. 1부터 10,000,000 범위 숫자 20개 무작위 추출
  2. PreparedStatement를 사용하여 다음과 같은 쿼리 20번 수행
UPDATE dbatest SET c1 = c1 +10, c2 = c2 + 10000 WHERE i = ?
Performance Test Update Result
Performance Test Update Result

Update에서는 상당한 효과를 보입니다. Temporary Table없이 단순 건 단위로 Update 수행하는 것보다 확실히 효율이 좋습니다. 데이터 처리 시 Permission Check -> Open Table -> Process -> Close Table 등과 같이 일련의 작업이 필요한데 이러한 것을 한방 쿼리로 퉁(?) 친 결과가 아닐까 생각해 봅니다. 하지만 앞서서 발생했던 처리 지연 원인이 확실하지 않은 시점에서 Update 시 무조건 좋다고 볼 수는 없겠네요.

OLTP 환경에서 안정성 검토가 이루어져야 할 것이고, 10건 이상 동시 데이터 변경 작업에서는 성능 향상 효과를 상당히 얻을 수 있겠습니다.^^

Conclusion

위 결과를 정리하자면 다음과 같습니다.

Select 시에 Temporary Table을 사용하는 것은 성능 상으로는 전혀 도움이 되지 않습니다. 빈도있는 테이블 생성/삭제, 데이터 Insert및 Join Select 등 불필요한 단계 때문이죠. 하지만 중간 결과를 저장하거나, 추후 빈도있는 재사용을 위한 목적이라면 큰 효과를 볼 수 있을 것 같습니다.

그리고 위 Update 그래프 결과를 참고할 때 10건 이상 동시 데이터 업데이트 처리 시에는 분명 효율성이 상승하는 효과는 분명히 있습니다. 서비스 로직에 따라서 동시에 수많은 데이터를 업데이트 처리하는 경우에는 큰 효과를 걷을 수 있겠습니다.

하지만, 앞서서 발생했던 처리 지연을 염두해야 하며, 가능한한 Drop 및 Create 구문을 발생하지 않고 테이블을 재사용하는 방안이 조금은 더 합리적일 것 같습니다.

Amazon RDS에서 유실된 데이터 복원하기

Overview

Amazon Relational Database Service(Amazon RDS)는 클라우드에서 관계형 데이터베이스를 쉽게 설치, 운영 및 확장할 수 있는 서비스입니다.

자원을 유연하게 배분할 수 있는 이점이 있는 클라우드이지만, 모든 서비스는 결국에는 사람 손을 거쳐야 하고, 때로는 인재로 인한 데이터 유실 사고가 발생할 수 있습니다.

사용이 편리하게 구현되어 있지만, 사용자에게 제공하는 권한 또한 상당히 제약적(인스턴스 관리자일지라도)입니다.

오늘은 Amazon RDS 상에서 데이터 유실 장애가 발생한 경우 대처할 수 있는 방안에 관하여 포스팅하도록 하겠습니다. (기준은 MySQL이나 타 DBMS도 큰 차이가 없을 것 같네요^^)

복구 순서는 다음과 같습니다.

  1. 백업 DB 인스턴스 생성
  2. 임시 복구 테이블 생성
  3. 데이터 추출 및 복원

백업 DB 인스턴스 생성

Amazon RDS에서는 앞서 언급드린 것과 같이 사용자에게 제공하는 권한이 상당히 제약적입니다. 무엇보다 DB서버에는 OS개념이 없고, 오직 DBMS에 개방된 포트를 통해서만 DB 접속이 가능합니다. 그리고 사용 시 불필요한 권한 또한 대부분 회수 되어 있죠. 결과적으로 로컬 IDC에서 데이터 유실 발생 시 Binlog Position을 활용하여 장애 시점 이전으로 데이터를 돌리는 것이 불가합니다.

하지만 RDS에서는  “Restore To Point In Time” 라는 기능을 제공합니다. 새벽에 생성된 DB 이미지와 내부적으로 Binary Log를 취합하여 실제 사용자가 원하는 시점의 DB 인스턴스를 생성하는 기능이죠.

백문이불여일견!! 한번 보시죠^^

Amazon Restore To Point In Time

Amazon RDS 콘솔 상에서 “Restore To Point In Time“버튼을 클릭합니다. 그러면 하단과 같이 DB 인스턴스 생성을 위한 옵션을 입력하는 레이어가 뜹니다.

Amazon Restore To Point In Time Option

Restore Time을 장애 시점 이전으로 설정합니다. 물론 장애 시점과 가까울수록 데이터 신뢰성을 더욱 커지겠죠. 단, 여기서 시간은  UTC 기준으로 넣으셔야 합니다.

DB Instance Identifier“에는 생성할 DB 인스턴스 이름이니, 기존 네이밍과 중복이 되지 않도록 지정합니다.

기타 옵션은 큰 의미는 없으나, 비용적인 측면을 고려하여 “DB  Instance Class“를 Small 사이즈로 설정합니다. (생성된 DB에서는 단순 Data Export만 수행할 것이기 때문에 좋은 성능은 필요 없습니다.)

옵션을 모두 입력을 하고 “Launch DB Instance“를 클릭하면 아래와 같이 새로운 DB 인스턴스가 생성이 됩니다. ^^

Amazon Launch Backup DB Instance

DB 인스턴스가 생성되는 과정은 기존 운영되고 있는 서비스 DB에는 영향을 주지 않습니다. 새벽에 스냅샷 형태로 풀 백업된 DB 이미지에 Binary Log 변경 사항을 내부적으로 취합하기 때문이죠. ^^

오~랜 시간이 지난 후 확인을 해보면 백업 DB 인스턴스 Status가 Available로.. 즉 새로운 DB 인스턴스 생성이 완료되었습니다 . 참 쉽죠??

Amazon RDS End Point

신규로 생성된 백업 DB 인스턴스를 클릭하면 관련 Description이 위와 같이 나오는데, 여기서 End Point를 보면 RDS에 접근하기 위한 주소가 나옵니다. 클라이언트에서는 End Point를 통해서 DB 접속을 진행하면 됩니다.

임시 복구 테이블 생성

자! 이제 백업 DB 인스턴스를 생성하였으니, 데이터를 복구하는 단계로 넘어가야겠죠? 사전에 복구할 테이블과 동일한 구조의 테이블을 생성을 합니다. 데이터 이관을 Export/Import로 데이터를 이관하기 위함입니다.

임시 복구 테이블 생성

mysql> create table tb_repair_tmp like tb_repair;
Query OK, 0 rows affected (0.42 sec)

인덱스 제거

임시 테이블을 Rename할 목적이 아니라면, 기존 인덱스는 필요하지 않습니다. 과감하게 날려줍니다!! Primary Key는 물론 제외하고 날리셔야겠죠? ^^;;

mysql> drop index idx_tb_repair_indt on tb_repair_tmp;
Query OK, 0 rows affected (0.30 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> drop index idx_tb_repair_closedt on tb_repair_tmp;
Query OK, 0 rows affected (0.31 sec)
Records: 0 Duplicates: 0 Warnings: 0

복원을 위해 데이터를 저장할 임시 테이블까지 모두 생성하였습니다.

데이터 추출 및 복원

mysqldump 유틸리티를 활용하여 데이터를 dump하는 동시에 sed유틸리티로 테이블명만 임시 백업 테이블명으로 변경하여 바로 데이터를 입력합니다.

MySQL에서 테이블 스키마를 “무중단”으로 변경해보자!!” 편에서 데이터를 이관한 것과 동일한 방식입니다. ^^ 얼마전 RDS 데이터 복원을 하며 대충 만들어놓은 스크립트를 우연찮게 재사용하게 되었네요. ㅎㅎ

명시적인 명령어는 하단과 같습니다.

$ mysqldump -udbuser -pxxxxx \
  --single-transaction \
  --no-create-db \
  --no-create-info \
  --triggers=false \
  --comments=false \
  --add-locks=false \
  --disable-keys=false \
  --host=xx-master-restore.xx.us-east-1.rds.amazonaws.com \
  --port=3306 \
  --databases targetdb \
  --tables tb_repair \
| sed -r 's/^INSERT INTO `tb_repair`/INSERT INTO `tb_repair_tmp`/gi' \
| mysql -udbuser -pxxxxx \
  --host xx-master.xx.us-east-1.rds.amazonaws.com \
  --port 3306 targetdb

mysqldump에 들어가는 host는 백업 DB 인스턴스 End Point이고, mysql에 들어가는 host는 복원할 서버 End Point입니다. 헷갈리면 안됩니다!!

위 작업이 마무리되면 임시 테이블에 장애 시점 이전의 데이터가 들어있는 것을 확인할 수 있습니다.

데이터 보정

장애 시점 이전 데이터를 구성하였으니, 이제는 구성된 데이터를 활용하여 전체적인 데이터 복구 작업을 마무리합니다. 장애를 유발한 SQL에 따라서 복구 시나리오가 다릅니다.

Case 1 : Drop Table

단순하게 테이블 Rename합니다. 물론 Rename을 하기 위해서는 앞서 진행했던 인덱스 제거 작업을 해서는 안되겠죠.

Case 2 : Delete Table (Truncate Table)

테이블에 데이터는 없으나, 지속적으로 누적되고 있는 경우입니다. 이 경우는 누적되는 데이터를 선별해서 데이터를 복원해야 합니다.

하단과 같이 임시 테이블에는 존재하나 원본 테이블에는 없는 데이터를 선별해서 데이터를 복원합니다. DB에 무리가 가지 않도록 10만 건씩 나눠서 데이터를 복사합니다. 여기서 seq는 각 테이블의 Primary Key입니다.

insert into tb_repair
select a.*
from tb_repair_tmp a
left join tb_repair b on a.seq = b.seq
where b.seq is null
limit 100000;

Case 3 : Update Table

다음과 같이 update 시 inner join을 수행하여 데이터를 보정합니다.  DB에 무리가 가지 않도록 10만 건씩 나눠서 데이터를 업데이트 합니다. 하단은 Primary Key가 Auto_Increment 옵션이 적용된 경우이고, 일반 스트링인 경우에는 limit을 활용하여 적절하게 자르시기 바랍니다.^^ 실수로 특정 필드를 공백으로 업데이트한 경우입니다.

update tb_repair a
inner join tb_repair_tmp b on a.seq = b.seq
set a.passwd = b.passwd
where a.passwd = ''
and a.seq between 1 and 100000;

Conclusion

Amazon RDS에서 제공하는 “Restore To Point In Time” 기능을 사용하면, DB 장애 처리 시 사용하던 복잡한 Dump 명령 없이 간단하게 데이터를 복원할 수 있습니다.

단, “Restore To Point In Time” 사용 시 Binary Log 포지션을 일일이 확인하며 장애 시점 바로 이전까지는 복원이 불가하다는 것을 반드시 인지하시기 바랍니다.

그리고 데이터 보정 후 반드시 검증도 꼭 하시고, 기타 웹로그가 있는 경우에도 충분히 반영을 하셔야 합니다.

데이터를 복구한 이후에는 임시로 생성한 DB 인스턴스는 반드시 제거하세요. (비용이 나갑니다.)

MySQL에서 테이블 스키마를 “무중단”으로 변경해보자!!

Overview

MySQL은 단순 쿼리 처리 능력은 탁월하나 테이블 스키마 변경 시에는 상당히 불편합니다. 일단 테이블 스키마 변경 구문을 실행하면 임시 테이블 생성 후 데이터를 복사하고, 데이터를 복사하는 동안에는 테이블에 READ Lock이 발생하여 데이터 변경 작업을 수행하지 못합니다. (Table Lock이 걸리죠.)

이 같은 현상은 인덱스, 칼럼 추가/삭제 뿐만 아니라 캐릭터셋 변경 시에도 동일하게 발생합니다. (최근 5.5 버전에서는 인덱스 추가/삭제에서는 임시 테이블을 생성하지 않습니다.)

얼마 전 서비스 요구 사항 중 테이블 칼럼을 무중단으로 변경하는 것이 있었는데, 이에 관해 정리 드리겠습니다.^^

요구사항

서비스 요구사항과 개인적인 요구 사항은 다음과 같습니다.

  1. 최대한 서비스 중단 없이 가능해야 함
  2. 테이블에 Lock이 발생하지 말아야 함
  3. 빠르게 구현해야 함(개인 요구 사항)
  4. 재사용 가능해야 함(개인 요구 사항)
  5. 문제 발생 시 복구가 쉬워야 함

개인적으로 빠르게 적용하는 것과 이와 같은 이슈가 추후 재 발생 시 재사용할 수 있도록 모듈화하자는 것이 목표였습니다.

대상 테이블 분석

대상 테이블은 다음과 같은 특징이 있었습니다.

  1. Auto_increment 옵션이 적용되었으며, Primary Key가 존재함
  2. 데이터 변경에는 Insert와 Delete만 발생(Update 없음)
  3. 연관된 프로시저 및 트리거는 없음

무엇보다 Update가 없기 때문에 조금 더 생각을 심플하게 가져갈 수 있었습니다.

작업 시나리오

How to migrate data to different table

트리거로 기존 테이블에 Insert및 Delete 시 변경 분을 별도 테이블에 저장을 합니다. 그리고 임시 테이블을 만들고 해당 테이블에 기존 테이블 데이터를 이관합니다. 물론 임시 테이블에는 원하는 스키마로 변경된 상태겠죠. Import가 마무리되면 트리거에 쌓인 데이터로 변경 분을 적용하고 최종적으로 테이블을 Rename함으로써 프로세스가 마무리됩니다.

조금더 상세하게 풀자면 다음과 같습니다.

  1. 임시 테이블 생성
    • Insert/Delete 시 변경 사항을 저장할 테이블
      (TAB01_INSERT, TAB01_DELETE)
    • 데이터를 임시로 저장할 테이블
  2. 트리거 생성
    • 원본 테이블에 Delete 발생 시 해당 ROW를 임시 테이블에 저장
    • 원본 테이블에 Insert 발생 시 해당 ROW를 임시 테이블에 저장
  3. 원본 테이블 export/import
    • export -> 테이블명 변경 -> import
    • SED로 테이블 명을 임시 테이블 명으로 변경
  4. 원본 테이블과 임시 테이블 RENAME
    • 원본 테이블 : TAB01 -> TAB01_OLD
    • 임시 테이블 : TAB01_TMP -> TAB01
  5. 테이블 변경 분 저장
    • TAB01_INSERT에 저장된 데이터 Insert
    • TAB01_DELETE에 저장된 데이터 Delete
  6. 트리거, 임시테이블 제거
    • 트리거 : Insert트리거, Delete 트리거
    • 테이블 : TAB01_INSERT, TAB01_DELETE

모든 작업을 어느정도 자동화 구현하기 위해 “작업 스크립트”를 만드는 프로시저와 데이터 이관을 위한 Shell 스크립트를 작성하였습니다.

Shell 스크립트는 Export와 동시에 SED 명령을 통해 자동으로 테이블 이름을 변경하여 Import하도록 구현하였습니다.

DB명, 테이블명,  스키마 변경 내용을 다음과 같이 인자 값으로 넘겨서 프로시저를 호출합니다.
(프로시저 소스는 가장 하단 참조)

call print_rb_query('dbatest', 'TAB01', 'MODIFY ACT_DESC VARCHAR(100) CHARACTER SET UTF8MB4 COLLATE UTF8MB4_UNICODE_CI DEFAULT NULL;');

그러면 아래와 같이 결과가 나오는데 Step1, 2, 3을 순차적으로 실행하면 됩니다. 보기 쉽게 주석을 추가하겠습니다.^^

>> Step 1>  Prepare : SQL <<
## 임시 테이블 생성
DROP TABLE IF EXISTS dbatest.TAB01_INSERT;
CREATE TABLE dbatest.TAB01_INSERT LIKE TAB01;
DROP TABLE IF EXISTS dbatest.TAB01_DELETE;
CREATE TABLE dbatest.TAB01_DELETE LIKE TAB01;
DROP TABLE IF EXISTS dbatest.TAB01_TMP;
CREATE TABLE dbatest.TAB01_TMP LIKE TAB01;

## 임시 테이블 스키마 변경
ALTER TABLE dbatest.TAB01_TMP AUTO_INCREMENT = 10660382;
ALTER TABLE dbatest.TAB01_TMP MODIFY ACT_DESC VARCHAR(100) CHARACTER SET UTF8MB4 COLLATE UTF8MB4_UNICODE_CI DEFAULT NULL;
DELIMITER $$

## Delete 트리거 생성
DROP TRIGGER IF EXISTS dbatest.TRG_TAB01_DELETE$$
CREATE TRIGGER dbatest.TRG_TAB01_DELETE
AFTER DELETE ON dbatest.TAB01
FOR EACH ROW
BEGIN
INSERT INTO dbatest.TAB01_DELETE VALUES(
    OLD.ACT_ID,
    OLD.ACT_UID,
    OLD.ACT_USER_NAME,
    OLD.ACT_TIME,
    OLD.TO_UID,
    OLD.TO_USER_NAME,
    OLD.ACT_TYPE,
    OLD.POSTID,
    OLD.TAB01,
    OLD.ACT_DESC,
    OLD.BEFORE_USER_NAME,
    OLD.PHOTO_LINK,
    OLD.THUMB_URL,
    OLD.FROM_SERVICE);
END$$

## Insert 트리거 생성
DROP TRIGGER IF EXISTS dbatest.TRG_TAB01_INSERT$$
CREATE TRIGGER dbatest.TRG_TAB01_INSERT
AFTER INSERT ON dbatest.TAB01
FOR EACH ROW
BEGIN
INSERT INTO dbatest.TAB01_INSERT VALUES(
    NEW.ACT_ID,
    NEW.ACT_UID,
    NEW.ACT_USER_NAME,
    NEW.ACT_TIME,
    NEW.TO_UID,
    NEW.TO_USER_NAME,
    NEW.ACT_TYPE,
    NEW.POSTID,
    NEW.TAB01,
    NEW.ACT_DESC,
    NEW.BEFORE_USER_NAME,
    NEW.PHOTO_LINK,
    NEW.THUMB_URL,
    NEW.FROM_SERVICE);
END$$
DELIMITER ;

>> Step 2>  Data Copy : Shell Script <<
## 데이터 마이그레이션
mig_dif_tab.sh dbatest TAB01 TAB01_TMP

>> Step 3>  Final Job : SQL <<
## 변경분 적용
INSERT INTO dbatest.TAB01_TMP SELECT * FROM dbatest.TAB01_INSERT;
DELETE A FROM dbatest.TAB01_TMP A INNER JOIN dbatest.TAB01_DELETE B ON A.ACT_ID = B.ACT_ID;

## 테이블 Rename(Swap)
RENAME TABLE dbatest.TAB01 TO dbatest.TAB01_OLD, dbatest.TAB01_TMP TO dbatest.TAB01;

## 트리거 제거
DROP TRIGGER IF EXISTS dbatest.TRG_TAB01_DELETE;
DROP TRIGGER IF EXISTS dbatest.TRG_TAB01_INSERT;

##변경분 재 적용 (확인 사살)
INSERT INTO dbatest.TAB01 SELECT * FROM dbatest.TAB01_INSERT;
DELETE A FROM dbatest.TAB01 A INNER JOIN dbatest.TAB01_DELETE B ON A.ACT_ID = B.ACT_ID;

## 임시 테이블 제거
DROP TABLE IF EXISTS dbatest.TAB01_INSERT;
DROP TABLE IF EXISTS dbatest.TAB01_DELETE;

임시 테이블에는 Auto_increment 값을 기존보다 1% 높게 설정하여 테이블명 Swap 후 Primary Key 충돌이 없도록 하였습니다. 그리고 Insert ignore문을 사용하여 Primary Key 중복으로 발생하는 오류는 무시하도록 하였고, 테이블 Rename 후 변경분을 재 적용하는 확인사살도 하였습니다. ^^;;

Shell 스크립트

위에서 Step2에서 사용하는 Shell 스크립트는 다음과 같습니다.

#!/bin/sh
if [ $# -ne 3 ]; then
echo "Usage: ${0} <Database_Name> <Orignal_Table> <Target_Table>"
echo "<Example>"
echo "Database Name : snsdb"
echo "Orignal Table : tab01"
echo "Target Table  : tab01_tmp"
echo "==> ${0} snsdb tab01 tab01_tmp"
exit 1
fi
## Declare Connection Info
export DB_CONNECT_INFO="-u계정 -p패스워드"
export REMOTE_DB_HOST="DB호스트URL"
export REMOTE_DB_PORT="3306"
## Exec profile for mysql user
. ~/.bash_profile
## Dump and Insert Data
mysqldump ${DB_CONNECT_INFO}                           \
  --single-transaction                                 \
  --no-create-db                                       \
  --no-create-info                                     \
  --triggers=false                                     \
  --comments=false                                     \
  --add-locks=false                                    \
  --disable-keys=false                                 \
  --host=${REMOTE_DB_HOST}                             \
  --port=${REMOTE_DB_PORT}                             \
  --databases ${1}                                     \
  --tables ${2}                                        \
| sed -r 's/^INSERT INTO `'${2}'`/INSERT INTO `'${3}'`/gi' \
| mysql ${DB_CONNECT_INFO} -h ${REMOTE_DB_HOST} -P ${REMOTE_DB_PORT} ${1}

Export 값을 파이프(|)로 sed로 넘기고, sed에서는 테이블명만 정규식으로 바꿔서 최종적으로 Import하는 구문입니다.

Create Table As Select 혹은 Insert into Select 구문을 사용하지 않은 이유는 Redo 로그가 비대해짐에 따라 시스템 부하가 따르기 때문입니다. tx_isolation 값을 READ-COMMITTED로 설정하여도 데이터가 전체 들어간 시점에 일시적인 테이블 Lock이 발생합니다. (트랜잭션을 마무리하는 과정에서 발생하는 Lock입니다.) “MySQL 트랜잭션 Isolation Level로 인한 장애 사전 예방 법” 을 참고하세요.^^

프로시저

단일 테이블이라면 위에 있는 스크립트를 약간만 수정하면 되겠지만, 제 경우는 변경할 테이블이 꽤 많아서 스크립트를 생성하는 프로시저를 아래와 같이 구현하였습니다.

사실 모든 과정을 Perl 혹은 Java로 구현하면 Step없이 간단했겠지만, DB 접속 서버에 구성하기 위해서는 절차 상 복잡한 부분이 있어서 프로시저로 작성하였습니다. ㅎㅎ

DELIMITER $$
DROP PROCEDURE print_rb_query $$
CREATE PROCEDURE print_rb_query(IN P_DB VARCHAR(255), IN P_TAB VARCHAR(255), IN P_ALTER_STR VARCHAR(1024))
BEGIN
    ## Declare Variables
    SET @qry = '\n';
    SET @ai_num = 0;
    SET @ai_name = '';
    SET @col_list = '';

    ## Get AUTO_INCREMENT Number for Temporary Table
    SELECT CAST(AUTO_INCREMENT*1.01 AS UNSIGNED) INTO @ai_num
    FROM INFORMATION_SCHEMA.TABLES
    WHERE TABLE_SCHEMA = P_DB
    AND TABLE_NAME = P_TAB;

    ## Get auto_increment Column Name
    SELECT COLUMN_NAME INTO @ai_name
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_SCHEMA = P_DB
    AND TABLE_NAME = P_TAB
    AND EXTRA = 'auto_increment';

    SET @qry = CONCAT(@qry, '>> Step 1>  Prepare : SQL <<\n');
    SET @qry = CONCAT(@qry, 'DROP TABLE IF EXISTS ',P_DB,'.',P_TAB,'_INSERT;\n');
    SET @qry = CONCAT(@qry, 'CREATE TABLE ',P_DB,'.',P_TAB,'_INSERT LIKE ',P_TAB,';\n');
    SET @qry = CONCAT(@qry, 'DROP TABLE IF EXISTS ',P_DB,'.',P_TAB,'_DELETE;\n');
    SET @qry = CONCAT(@qry, 'CREATE TABLE ',P_DB,'.',P_TAB,'_DELETE LIKE ',P_TAB,';\n');
    SET @qry = CONCAT(@qry, 'DROP TABLE IF EXISTS ',P_DB,'.',P_TAB,'_TMP;\n');
    SET @qry = CONCAT(@qry, 'CREATE TABLE ',P_DB,'.',P_TAB,'_TMP LIKE ',P_TAB,';\n');
    SET @qry = CONCAT(@qry, 'ALTER TABLE ',P_DB,'.',P_TAB,'_TMP AUTO_INCREMENT = ',@ai_num,';\n');
    SET @qry = CONCAT(@qry, 'ALTER TABLE ',P_DB,'.',P_TAB,'_TMP ',P_ALTER_STR,'\n');

    ## Change Delimiter
    SET @qry = CONCAT(@qry, 'DELIMITER $$\n');

    ## Get Column List for Delete Trigger
    select GROUP_CONCAT('\n    OLD.',COLUMN_NAME) into @col_list
    from INFORMATION_SCHEMA.COLUMNS
    where TABLE_SCHEMA = P_DB
    and table_name = P_TAB
    order by ORDINAL_POSITION;

    SET @qry = CONCAT(@qry, 'DROP TRIGGER IF EXISTS ',P_DB,'.TRG_',P_TAB,'_DELETE$$\n');
    SET @qry = CONCAT(@qry, 'CREATE TRIGGER ',P_DB,'.TRG_',P_TAB,'_DELETE\n');
    SET @qry = CONCAT(@qry, 'AFTER DELETE ON ',P_DB,'.',P_TAB,'\n');
    SET @qry = CONCAT(@qry, 'FOR EACH ROW\n');
    SET @qry = CONCAT(@qry, 'BEGIN\n');
    SET @qry = CONCAT(@qry, 'INSERT INTO ',P_DB,'.',P_TAB,'_DELETE VALUES(');
    SET @qry = CONCAT(@qry, @col_list);
    SET @qry = CONCAT(@qry, ');\n');
    SET @qry = CONCAT(@qry, 'END$$\n');

    ## Get Column List for Insert Trigger
    select GROUP_CONCAT('\n    NEW.',COLUMN_NAME) into @col_list
    from INFORMATION_SCHEMA.COLUMNS
    where TABLE_SCHEMA = P_DB
    and table_name = P_TAB
    order by ORDINAL_POSITION;
    SET @qry = CONCAT(@qry, 'DROP TRIGGER IF EXISTS ',P_DB,'.TRG_',P_TAB,'_INSERT$$\n');
    SET @qry = CONCAT(@qry, 'CREATE TRIGGER ',P_DB,'.TRG_',P_TAB,'_INSERT\n');
    SET @qry = CONCAT(@qry, 'AFTER INSERT ON ',P_DB,'.',P_TAB,'\n');
    SET @qry = CONCAT(@qry, 'FOR EACH ROW\n');
    SET @qry = CONCAT(@qry, 'BEGIN\n');
    SET @qry = CONCAT(@qry, 'INSERT INTO ',P_DB,'.',P_TAB,'_INSERT VALUES(');
    SET @qry = CONCAT(@qry, @col_list);
    SET @qry = CONCAT(@qry, ');\n');
    SET @qry = CONCAT(@qry, 'END$$\n');

    ## Change Delimiter
    SET @qry = CONCAT(@qry, 'DELIMITER ;\n');
    SET @qry = CONCAT(@qry, '\n\n');

    ## Insert Data
    SET @qry = CONCAT(@qry, '>> Step 2>  Data Copy : Shell Script <<\n');
    SET @qry = CONCAT(@qry, 'mig_dif_tab.sh ',P_DB,' ',P_TAB,' ',P_TAB,'_TMP\n');
    SET @qry = CONCAT(@qry, '\n\n');
    SET @qry = CONCAT(@qry, '>> Step 3>  Final Job : SQL <<\n');

    ## Insert Data into Temporary Table
    SET @qry = CONCAT(@qry, 'INSERT IGNORE INTO ',P_DB,'.',P_TAB,'_TMP SELECT * FROM ',P_DB,'.',P_TAB,'_INSERT;\n');

    ## Delete Data from Temporary Table
    SET @qry = CONCAT(@qry, 'DELETE A FROM ',P_DB,'.',P_TAB,'_TMP A INNER JOIN ',P_DB,'.',P_TAB,'_DELETE B ON A.',@ai_name,' = B.',@ai_name,';\n');

    ## Swap table names
    SET @qry = CONCAT(@qry, 'RENAME TABLE ',P_DB,'.',P_TAB,' TO ',P_DB,'.',P_TAB,'_OLD, ',P_DB,'.',P_TAB,'_TMP TO ',P_DB,'.',P_TAB,';\n');

    ## Drop Triggers
    SET @qry = CONCAT(@qry, 'DROP TRIGGER IF EXISTS ',P_DB,'.TRG_',P_TAB,'_DELETE;\n');
    SET @qry = CONCAT(@qry, 'DROP TRIGGER IF EXISTS ',P_DB,'.TRG_',P_TAB,'_INSERT;\n');

    ## Insert Data
    SET @qry = CONCAT(@qry, 'INSERT IGNORE INTO ',P_DB,'.',P_TAB,' SELECT * FROM ',P_DB,'.',P_TAB,'_INSERT;\n');

    ## Delete Data
    SET @qry = CONCAT(@qry, 'DELETE A FROM ',P_DB,'.',P_TAB,' A INNER JOIN ',P_DB,'.',P_TAB,'_DELETE B ON A.',@ai_name,' = B.',@ai_name,';\n');

    ## Drop Temporary Tables
    SET @qry = CONCAT(@qry, 'DROP TABLE IF EXISTS ',P_DB,'.',P_TAB,'_INSERT;\n');
    SET @qry = CONCAT(@qry, 'DROP TABLE IF EXISTS ',P_DB,'.',P_TAB,'_DELETE;\n');
    SET @qry = CONCAT(@qry, '\n\n');
    SELECT @qry;
END$$
DELIMITER ;

Conclusion

결과적으로 서비스 영향없이 무중단으로 테이블 스키마를 변경하였습니다. 만약 Update가 있는 경우라면 “INSERT INTO .. ON DUPLICATE KEY UPDATE..” 문을 사용하면 해결 가능하다는 생각이 드네요.^^ 물론 Update 트리거에도 Insert와 동일한 액션이 취해져야 하겠죠.

서비스 중단없이 DB 스키마를 변경했다는 점에서 큰 의의가 있었네요.^^