레이블이 Lock인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Lock인 게시물을 표시합니다. 모든 게시물 표시

2010년 12월 26일 일요일

비관적(Pessimistic) 락과 낙관적(Optimistic) 락 (작성중)

비관적(Pessimistic) 락
  • 단어의 의미 그대로 어떠한 처리를 위해서 Lock을 점유하고 해제하는 정책이
    비관적인 가정(내가 업데이트하려고 하는 레코드를 다른 사용자도 업데이트하려고 할 것이다)하에서
    이루어지는 형태
  • 예를 들어서, 어떤 업데이트 작업을 하려고 하는데, 이 작업을 위해서 먼저 검색이 필요하다고 가정해보자.
    이때, 비관적 락 기반으로 작성된 로직이라면, 검색하기 전부터 락을 먼저 점유하고 모든 작업을
    진행하게 될 것이다.
  • 이 방식은 각 프로세스들의 락 점유 시간을 연장시키기 때문에 동시성이 떨어지게 된다.
  • 대신 Deadlock의 발생 가능성이 낮아지게 된다.

낙관적(Optimistic) 락
  • 낙관적 락은 비관적 락과 거의 반대 개념으로 생각하면 될듯 하다.
    (내가 업데이트하려고 하는 레코드를 다른 사용자는 접근하지 않을 것이다.)
  • 위의 업데이트 시나리오로 비교해본다면, 낙관적 락은 검색은 그냥 진행하고,
    최종 업데이트 하는 과정에서만 락을 점유하게 된다.
  • 낙관적 락은 락 점유 시간을 최소화하기 때문에 동시성이 좋아지게 된다.
  • 하지만, 뒤늦은 락 점유로 인해서 Deadlock의 가능성이 높아지게 된다.

InnoDB에서 사용되는 Lock의 종류

레코드 락
  • 의미 그대로 레코드 자체에 대해서 잠그는 락

Gap 락
  • 레코드와 레코드 사이의 간격을 잠그는 락
  • 레코드 자체에 대해서는 잠그지 않음
  • 매뉴얼상에서는 특정 레코드 키 이전의 간격을 잠그는 것으로 표현되어 있지만, 레코드 키 이전과 이후의 간격을 모두 포함하는 것 같기도 함
    -> 예를 들어서 레코드가 [1],[2],[3]이 있을 경우, 레코드 키 [2]에 변경 작업이 발생하는 경우 Gap 락이라 함은
        1<Gap<2 와 2<Gap<3의 두개의 범위를 모두 포함하는 것으로 보임
  • Gap 락의 가장 큰 목적은 아래 두 가지로 보임
    -> Master 와 Slave의 데이터 동기화 (더 정확히는 Binary 로그의 정확한 기록을 위해서)
    -> Phantom 레코드 방지 
  • InnoDB의 Gap 락은 생각보다 많은 Concurrency 저하를 가져온다고 생각됨
    -> 물론 Dead lock도 유발될 가능성이 높음
  • InnoDB에서 대 부분의 변경 작업들은 Gap 락을 유발함
  • Primary Key 업데이트는 레코드 락만 유발하며 Gap 락을 걸진 않음
  • Gap 락을 가지고 있다고 해서 그 간격에 Insert가 허용되는 것은 아님 (단순 예방용 락 일뿐임)
  • 만약 그 간격에 Insert를 하고자 하면, 반드시 충돌되는 락들이 모두 해제되기를 기다려야 함
  • Gap 락은 자동으로 확장 및 다른 트랜잭션의 Gap 락과 통합되기도 함
    -> Gap 락은 순수하게 모든 트랜잭션을 대상으로 해당 Gap에 변경 작업을 하지 못하도록 하는 예방용으로만 사용되기 때문임
    -> 예를 들어 레코드 키 [1], [2], [3]이 있는데, 트랜잭션 1번이 1<Gap<2 를 가지고 있고, 트랜잭션 2번이 2<Gap<3을 가지고 있는데,
        어떤 이유에서 레코드 키 [2]가 삭제된다면, 이 두개의 Gap 락은 1개의 Gap 락( 1< Gap <3)으로 통합되어
        트랜잭션 1번과 트랜잭션 2번이 이 통합된 Gap 락을 공유하게 됨
        (실제로 Purge thread가 레코드 키 [2]를 삭제하는 경우 이런 현상이 발생함) => 별로 중요치 않음

Next-key 락
  • 레코드 락과 Gap 락을 합친 락
    -> 예를 들어서 레코드가 [1],[2],[3]이 있다면, 레코드 [2]에 대해서 변경이 진행된다면 1< Gap + record <3 의 범위에 대해서 락이 걸림

Auto-increment 락
  • Auto Increment 속성의 컬럼을 가진 테이블에 Insert가 실행되는 경우 걸리는 락 (다른 여타의 락과는 조금 다름)
    -> Auto increment 컬럼에 값이 제공되어도 여전히 Auto increment 락은 걸림
  • InnoDB의 다른 락과는 달리 Auto-increment 락은 테이블 레벨의 락임
  • 다른 락들은 트랜잭션 단위의 락이지만, Auto-increment 락은 Statement 레벨의 락임
    -> 트랜잭션과 관계없이 Statement(Query문장)가 종료되면 락이 해제됨
    -> 이 특성으로 유추할 수 있는 것은 InnoDB의 Auto increment는 값이 한번 사용되어지면
        트랜잭션이 Rollback되어도, 이미 사용된 Auto increment 값은 복구되지 않는 다는 것을 예측할 수 있음
  • MySQL 5.0 까지는 위와 같이 작동하며, Concurrent insert가 매우 빈번한 경우에는 Auto increment 락이 병목이 될 가능성이 있음
  • MySQL 5.1에서는 아래와 같이 설정함으로써, 병목 회피 가능
    -> innodb_autoinc_lock_mode = 1 : 단순한 Insert에 대해서는 락을 걸지 않음
    -> innodb_autoinc_lock_mode = 2 : 어떠한 경우에도 락을 걸지 않음 (Row level의 replication 을 사용할 경우에만 사용 가능함)

Insert intention lock
  • Gap (레코드 키 사이에)에 데이터를 Insert하기 위해서 기다릴 때 거는 락

InnoDB에서 사용되는 Lock의 개요

InnoDB lock의 기본 특성
  • Pessimistic locking
  • Graph 를 이용한 데드락 감지
  • 레코드 기반의 락
  • 락이 많아진다고 해서 Escalation되지 않음 (페이지나 테이블 단위의 락으로 업그레이드 되지 않음)
  • Waiting timeout 기능 지원 (상이한 Storage engine간의 무한 대기 방지)
  • 레코드와 Index에 기반한 잠금 -> 다들 그냥 넘어가는 부분이지만, 상당히 중요한 의미를 가짐
  • 묵시적인 락은 페이지 단위로 Bitmap 구조체로 관리됨 (디스크상에서도 관리됨)
    -> 명시적인 락을 유발하지 않는 모든 쿼리 (락 관련 힌트를 포함하지 않는 쿼리)
  • 명시적인 락은 InnoDB Buffer pool의 Bitmap 구조체로 관리됨 (메모리상에서만 관리됨)
    -> FOR UPDATE, LOCK IN SHARE MODE와 같은 락 관련 힌트를 포함한 쿼리
  • 락 정보는 레코드 한건 한건에 대해서 3~8비트를 사용하며, 상당히 캠팩트해서 락 에스컬레이션이 발생하지 않는다고 함

잠금 대상에 따른 락 구분
  • 레코드 락
  • Gap 락
  • Next-key 락
  • Auto-increment 락
  • Insert intention lock

공유 여부에 따른 락 구분
  • Shared lock
  • Exclusive lock

MySQL 와의 호환을 위한 InnoDB의 테이블 락
  • Intention Shared lock
  • Intention Exclusive lock

2010년 12월 25일 토요일

InnoDB의 트랜잭션 및 Lock 내용 확인

간단하게 InnoDB가 실행하고 있는 트랜잭션들의 정보들을 이해하는 방법을 살펴보자.
InnoDB Plug-in 버전에서는 트랜잭션에 대한 정보와 Lock 그리고 Lock 대기에 관련된 정보들을 INFORMATION_SCHEMA에 정의된 테이블들(INNODB_TRX, INNODB_LOCKS, INNODB_LOCK_WAITS)을 조회함으로써 쉽게 파악할 수 있다. (이 부분은 InnoDB Plug-in의 엄청난 발전으로 느껴진다)
하지만, Built-in 버전의 InnoDB에서는 이러한 딕셔너리를 제공하지 않는다. 유일한 방법은 “SHOW ENGINE INNODB STATUS” 명령문밖에 해결책이 없다.

간단히 아래와 같은 테이블을 만들고, 두 개의 세션에서 동일 UPDATE 쿼리 문장을 실행한 상태에서 트랜잭션과 Lock의 상태를 SHOW ENGINE INNODB STATUS로 확인해 보도록 하겠다.

-- //-----------------------------------------------------------------------------------------
-- // Table specification
-- //-----------------------------------------------------------------------------------------
CREATE TABLE article (
  article_id int(10) unsigned NOT NULL AUTO_INCREMENT,
  article_category tinyint(3) unsigned NOT NULL DEFAULT '0',
  article_title varchar(200) NOT NULL DEFAULT '',
  article_type enum('sticky','general') NOT NULL DEFAULT 'general',
  PRIMARY KEY (article_id),
  KEY ix_category_type_id_user (article_category,article_type)
) ENGINE=InnoDB;


-- //-----------------------------------------------------------------------------------------
-- // Session1
-- //-----------------------------------------------------------------------------------------
begin;
update article set article_title='xx' where article_category=112 and article_type='general';


-- //-----------------------------------------------------------------------------------------
-- // Session2
-- //-----------------------------------------------------------------------------------------
begin;
update article set article_title='xx' where article_category=112 and article_type='general';


-- //-----------------------------------------------------------------------------------------
-- // SHOW ENGINE INNODB STATUS 명령의 결과
-- //-----------------------------------------------------------------------------------------
mysql> show engine innodb status;
........
........
01  ------------
02  TRANSACTIONS
03  ------------
04  Trx id counter 0 1809461
05  Purge done for trx's n:o < 0 1809457 undo n:o < 0 0
06  History list length 21
07  LIST OF TRANSACTIONS FOR EACH SESSION:
08 
09  ---TRANSACTION 0 0, not started, process no 5975, OS thread id 1099274560
10  MySQL thread id 6, query id 309 localhost root
11  show engine innodb status
12 
13  ---TRANSACTION 0 1809460, ACTIVE 4 sec, process no 5975, OS thread id 1281038656 starting index read
14  mysql tables in use 1, locked 1
15  LOCK WAIT 2 lock struct(s), heap size 368, 1 row lock(s)
16  MySQL thread id 7, query id 308 localhost root Updating
17  update article set article_title='xx' where article_category=112 and article_type='general'
18  ------- TRX HAS BEEN WAITING 4 SEC FOR THIS LOCK TO BE GRANTED:
19  RECORD LOCKS space id 88 page no 4 n bits 88 index ix_category_type_id_user of table test.article trx id 0 1809460 lock_mode X locks rec but not gap waiting
20  Record lock, heap no 10 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
21   0: len 1; hex 70; asc p;; 1: len 1; hex 02; asc  ;; 2: len 4; hex 00000009; asc     ;; 3: len 4; hex 00000000; asc     ;;
22 
23  ---TRANSACTION 0 1809458, ACTIVE 17251 sec, process no 5975, OS thread id 1099008320
24  3 lock struct(s), heap size 368, 2 row lock(s), undo log entries 1
25  MySQL thread id 4, query id 301 localhost root
........
........

  • 우선 각 트랜잭션은 “TRANSACTION 999999 999999”으로 시작된다. (TRANSACTION ID는 8바이트 숫자 값인데, 이러한 형태의 8바이트 숫자 값은 모두 상위 4바이트와 하위 4바이트 두 영역으로 나뉘어서 출력된다.)
  • 그러므로, 여기 예제에서는 현재 3개의 트랜잭션 (09번 라인, 13번 라인, 23번 라인)이 존재한다는 것을 확인할 수 있다.
  • 각 TRANSACTION 라인에는 현재 해당 트랜잭션이 어떤 작업을 하고 있는지 어떤 상태인지를 알려 주는 키워드가 표시되며, 해당 트랜잭션이 몇 초 동안 진행 중인지도 보여 준다.- 09번 라인 : not started è 현재 트랜잭션이 진행 중이지 않음을 의미- 13번 라인 : ACTIVE è 현재 트랜잭션이 4초 동안 활성화된 상태임 (경우에 따라 문장 끝에 어떤 작업 중인지를 표시해 줌 : starting index read)- 23번 라인 : ACTIVE è 현재 트랜잭션이 17251초 동안 활성화된 상태임
  • 각각의 트랜잭션들은 “MySQL thread id”라는 항목(10, 16, 24번 라인)을 가지는데, 이 thread id값은 MySQL에서 “SHOW PROCESSLIST” 명령의 결과에 보여지는 “id”와 동일한 값을 가지게 된다. 즉, 현재 트랜잭션 목록에서 제일 밑 트랜잭션 (TRANSACTION 0 1809458)을 종료하고자 하면, “kill 4”명령으로 트랜잭션을 종료시킬 수 있다.
  • 13번 라인의 트랜잭션은 “TRX HAS BEEN WAITING 4 SEC FOR THIS LOCK TO BE GRANTED:”이라는 항목을 가지고 있는데, 이는 현재 트랜잭션 (TRANSACTION 0 1809460)이 다른 트랜잭션이 이미 점유한 Lock 때문에 Blocking(대기) 상태임을 표시하며, 어떤 Lock을 기다리고 있는지 상세히 보여 준다. 하지만, 안타깝게도 이 결과로는 어떤 트랜잭션이 그 Lock을 가지고 있는지는 알아낼 수 없다. 대략 짐작해 보건데 그 밑에 있는 트랜잭션이 지금 17251초 동안 트랜잭션이 ACTIVE 상태인 것으로 보아서 문제를 유발하고 있을 것이라는 것 정도는 짐작해 볼 수 있게 된다. 조금 더 자세히 확인하기 위해서는 “innodb_lock_monitor” 가 필요한데, 이 내용은 하단의 “InnoDB Lock monitoring” 을 참조 바란다.
  • 19번 라인을 보면, 레코드 Lock에 관련된 정보가 나오는데, 이것은 현재 트랜잭션이 대기 중인 레코드(인덱스)를 의미하며, 그 인덱스의 정보를 20, 21번 라인에 걸쳐서 보여 주고 있다. 19번 라인에는 지금 대기하고 있는 Lock이 Shared-lock인지 Exclusive-lock인지를 그리고 Gap까지 잠그고 있는지 아닌지를 보여 준다. 일반적으로 Gap lock이 아니라고 표현되는 경우를 제외하고는 거의 모두 Gap까지 잠그고 있다고 생각하면 될 듯 하다. (Gap을 잠그고 있는지 아닌지 판단하는 또 다른 방법은 “undo entries” 항목의 수가 “row lock(s)”의 수보다 작으면 대부분 Gap lock 으로 판단할 수 있다. 하지만 이 방법도 정확한 것은 아니다. 예를 들어서 한 트랜잭션에서 하나의 레코드만 계속 업데이트하게 되면 undo entries가 row locks보다 커질 수 있기 때문이다.)
  • 21번 라인을 보면, 드디어 이해 불가능의 단어들이 출력되는데, 사실 이 부분이 의외로 문제 해결에 도움이 될 수 있다. 우선 이 라인에 표시되는 내용은 인덱스 레코드의 필드 값들을 출력해서 보여 주는데, HEX값과 ASCII값을 동시에 보여 준다. 21번 라인의 내용을 정리해 보면 아래와 같이 잘라서 생각해 볼 수 있다. (구분자는 ;; 임)- 0: len 1; hex 70; asc p;;- 1: len 1; hex 02; asc  ;;- 2: len 4; hex 00000009; asc     ;;- 3: len 4; hex 00000000; asc     ;;각 라인은 [인덱스상에서의 번호 : 필드 길이 : 16진수 필드 값(HEX) : ASCII 필드 값(ASC) ] 포맷으로 구성되어 있다. 그리고 이 필드들의 개수는 20번 라인의 n_fields와 동일한 값을 가지게 된다.이 값들과 19번 라인의 테이블 및 인덱스 명을 이용하면 어느 테이블의 어떤 레코드를 지금 기다리고 있는지를 알아낼 수 있다.
  • "SHOW ENGINE INNODB STATUS"의 "DEADLOCK" 섹션에 출력되는 내용도 지금까지의 설명한 내용과 동일한 패턴으로 출력되므로, DEADLOCK 정보를 확인할 때에도 이와 같은 방식으로 해석할 수 있다.

지금까지 간단히 트랜잭션의 내용을 읽는 방법을 확인해 보았다.


InnoDB Lock monitoring
우선 InnoDB의 Lock을 모니터링 하기 위해서는 아래와 같은 테이블을 생성해야 한다.

Create table innodb_lock_monitor (fd1 int) engine=innodb;

  • 이 테이블을 생성하면, 매 몇 초 단위로 SHOW ENGINE INNODB STATUS 결과를 MySQL 에러 로그 파일에 기록하므로 모니터링이완료되면 테이블을 삭제 해주는 것이 좋다.
  • 위의 테이블을 생성 후, “SHOW ENGINE INNODB STATUS” 명령을 실행하면 아래와 같은 좀 더 상세한 트랜잭션 정보를 확인할 수 있다.
  • 어떤 트랜잭션이 다른 트랜잭션의 처리를 Blocking(막고) 하고 있는지를 판단하기 위해서는, 각 트랜잭션에서 가지고 있거나 또는 기다리고 있는 테이블 및 인덱스 그리고 그 인덱스 페이지 번호로 추적해 볼 수 있다. (물론 이 방법도 정확하게 찾을 수 있는 방법은 아니다. 하지만 더 자세히는 지금의 InnoDB 에서는 무리일지도 모른다)
  • 아래 빨간색으로 표기된 부분을 비교해 봄으로써 두 개의 트랜잭션이 서로 Racing 상태임을 확인할 수 있다. (동일 테이블의 동일 인덱스에서 같은 인덱스 페이지 번호 4를 참조하고 있음)

------------
TRANSACTIONS
------------
01 Trx id counter 0 1809465
02 Purge done for trx's n:o < 0 1809462 undo n:o < 0 0
03 History list length 22
04 LIST OF TRANSACTIONS FOR EACH SESSION:
05 ---TRANSACTION 0 0, not started, process no 5975, OS thread id 1099274560
06 MySQL thread id 10, query id 442 localhost root
07 show engine innodb status
08
09
10 ---TRANSACTION 0 1809464, ACTIVE 12 sec, process no 5975, OS thread id 1281038656 starting index read
11 mysql tables in use 1, locked 1
12 LOCK WAIT 2 lock struct(s), heap size 368, 1 row lock(s)
13 MySQL thread id 9, query id 400 localhost root Updating
14 update article set article_title='xx' where article_category=112 and article_type='general'
15 ------- TRX HAS BEEN WAITING 12 SEC FOR THIS LOCK TO BE GRANTED:
16 RECORD LOCKS space id 88 page no 4 n bits 88 index ix_category_type_id_user of table test.article trx id 0 1809464 lock_mode X locks rec but not gap waiting
17 Record lock, heap no 10 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
18  0: len 1; hex 70; asc p;; 1: len 1; hex 02; asc  ;; 2: len 4; hex 00000009; asc     ;; 3: len 4; hex 00000000; asc     ;;
19
20 TABLE LOCK table test.article trx id 0 1809464 lock mode IX
21 RECORD LOCKS space id 88 page no 4 n bits 88 index ix_category_type_id_user of table test.article trx id 0 1809464 lock_mode X locks rec but not gap waiting
22 Record lock, heap no 10 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
23  0: len 1; hex 70; asc p;; 1: len 1; hex 02; asc  ;; 2: len 4; hex 00000009; asc     ;; 3: len 4; hex 00000000; asc     ;;
24
25
26 ---TRANSACTION 0 1809463, ACTIVE 21 sec, process no 5975, OS thread id 1099008320
27 3 lock struct(s), heap size 368, 2 row lock(s), undo log entries 1
28 MySQL thread id 8, query id 355 localhost root
29 TABLE LOCK table test.article trx id 0 1809463 lock mode IX
20 RECORD LOCKS space id 88 page no 4 n bits 88 index ix_category_type_id_user of table test.article trx id 0 1809463 lock_mode X locks rec but not gap
21 Record lock, heap no 10 PHYSICAL RECORD: n_fields 4; compact format; info bits 0
22  0: len 1; hex 70; asc p;; 1: len 1; hex 02; asc  ;; 2: len 4; hex 00000009; asc     ;; 3: len 4; hex 00000000; asc     ;;
23
24 RECORD LOCKS space id 88 page no 6 n bits 80 index PRIMARY of table test.article trx id 0 1809463 lock_mode X locks rec but not gap
25 Record lock, heap no 11 PHYSICAL RECORD: n_fields 10; compact format; info bits 0
26  0: len 4; hex 00000009; asc     ;; 1: len 6; hex 0000001b9c37; asc      7;; 2: len 7; hex 000000002d1f51; asc     - Q;; 3: len 1; hex 70; asc p;; 4: len 2; hex 7878; asc xx;; 5: len 30; hex 3c703e3c7370616e207374796c653d22666f6e742d73697a653a20313470; asc

INSERT INTO ... SELECT ... FROM 형태의 쿼리 사용시 주의 사항

INSERT INTO target_table
SELECT ... FROM source_table1, source_table2 WHERE ...

이 형태의 쿼리는 간단한 통계나 집계를 생성할 때 자주 사용된다.
일반적인 MySQL InnoDB 테이블에 대한 SELECT 쿼리는 Lock을 걸지 않으며, 
LOCK IN SHARE MODE 또는 FOR UPDATE 가 사용된 SELECT 문장에 대해서만 Lock이 필요하다.
하지만, 이 쿼리를 실행하게 되면 source_table1과 source_table2의 조회 대상 레코드에도 Read Lock이 걸리게 된다.
만약, 하위 SELECT 쿼리의 조회 범위나 처리 작업이 복잡하다면, 실시간 서비스에 영향을 미칠 수 있게 된다.
또한, 이 쿼리는 Lock 모드에서 SELECT를 실행하기 때문에 부분적으로 MVCC를 무시하고 최종으로 Commit된 
레코드를 읽게 되기 때문에 REPATABLE_READ 모드에서 실행되어도 실제적으로는 READ_COMMITED 모드로 작동하게 된다.
이러한 문제점은 MySQL의 GapLock과 연관이 있으며, 아래와 같이 2가지 방법으로 이러한 조회 테이블의 Lock을 피할 수 있다.
  • innodb_locks_unsafe_for_binlog 시스템 변수 값을 ON으로 설정
  • ROW-based replication과 READ-COMMITTED Isolation level 사용
하지만, 두 가지 대안 모두 현재로써는 좋은 해결책이 아닌 것으로 보인다.
첫 번째는 Master와 Slave의 데이터 부정합을 유발하게 될 것이며, 두 번째 Row-based replication은 아직 적용하기에는 시기 상조인듯하다.
(참고로 두 번째 방법은 이 쿼리를 위한 전용 옵션이 아니라, MySQL에서 GapLock을 제거하는 방법이기도 하다.)
그래서 이러한 부분을 해결하기 위한 방법으로는 아래와 같이 SELECT INTO OUTFILE과 LOAD DATA INFILE을 
혼용해서 사용하는 방법을 사용해보는 것이 좋을 듯 하다.

아래 예제에서는 article이라는 테이블을 내용을 group1_id, group2_id 컬럼을 이용하여 group by 한 결과를
다른 집계 테이블에 넣어 두고, 필요시 간단히 조회해서 사용할 수 있도록 하는 시나리오를 가정한 것이다.
아래의 쿼리는 article 테이블에 read lock을 사용하게 되므로, 실시간 변경 트랜잭션에 영향을 미치게 된다.
insert into temp$summary (summary_id, group1_id, group2_id, ...)
select null, group1_id, group2_id, ...
from article al
group by al.group1_id, al.group2_id
order by NULL;

그래서, 아래와 같이 SELECT -> Disk 파일 -> LOAD INTO -> RENAME TABLE과 같은 방법으로 해결할 수 있다.
-- // 임시 작업용 테이블 생성
create temp$summary(
  summary_id integer unsigned not null auto_increment,
  group1_id bigint not null,
  group2_id bigint not null,
  article_count integer default 0 not null,
  ...
  primary key(summary_id)
);

-- // Grouping 결과를 임시 파일로 저장
SELECT al.group1_id, al.group2_id, count(*) as article_count, ...
INTO OUTFILE '/tmp/temp_aritcle_summary.dat'
FIELDS TERMINATED BY ',' ENCLOSED BY '"' ESCAPED BY '\\'
from article al
group by al.group1_id, al.group2_id
order by NULL;

-- // 저장된 임시 파일을 테이블로 적재
LOAD DATA INFILE '/tmp/temp_aritcle_summary.dat'
INTO TABLE temp$summary
FIELDS TERMINATED BY ',' ENCLOSED BY '"' ESCAPED BY '\\'
  (group1_id, group2_id, article_count, ...)
SET summary_id=null;

-- // 데이터 적재후 추가적으로 필요한 인덱스 생성
alter table temp$summary add index ix_group1id_group2id (group1_id, group2_id);

-- // 준비된 임시 테이블을 서비스용 테이블로 이름 변경
-- // 아래와 같이 한 명령으로 필요한 테이블의 이름 변경을 한꺼번에 실행하게 되면, 
-- // 실시간 서비스라 하더라도 순단 현상(일시적으로 TABLE NOT FOUND)을 피할 수 있다.
rename table summary_yesterday to summary_old,
             summary to summary_yesterday,
             temp$summary to summary;

-- // 이틀 전 데이터를 가지고 있는 테이블은 삭제
drop table summary_old;