카테고리 : MySQL/기술노트

MySQL 트랜잭션 처리 개요: autocommit, 명시적 트랜잭션, implicit commit

MySQL에서 autocommit, 명시적 트랜잭션, implicit commit이 실행 경로와 운영 안정성에 미치는 영향을 정리한다.

저자: MySQL 기술 노트 작성: 2026.05.21 약 13분 7,400자
다운로드

MySQL 트랜잭션 처리 개요: autocommit, 명시적 트랜잭션, implicit commit

MySQL 운영에서 트랜잭션은 단순히 COMMITROLLBACK을 호출하는 문법 문제가 아니다. 애플리케이션이 언제 트랜잭션을 시작하고, 어느 시점에 잠금을 보유하며, 어떤 SQL이 의도치 않게 트랜잭션 경계를 끊는지에 따라 장애 양상이 크게 달라진다. 특히 InnoDB를 사용하는 OLTP 시스템에서는 짧은 트랜잭션을 안정적으로 유지하는 것이 성능, 복구 시간, 복제 지연, 온라인 스키마 변경, 장애 대응의 공통 기반이다.

이 글은 MySQL의 트랜잭션 처리 방식을 autocommit, 명시적 트랜잭션, implicit commit 세 축으로 정리한다. 목표는 문법 암기가 아니라 운영자가 현장에서 “왜 이 세션이 오래 잠금을 잡고 있는가”, “왜 ROLLBACK이 DDL을 되돌리지 못했는가”, “왜 배치 이후 replica lag가 커졌는가”를 해석할 수 있는 기준을 세우는 것이다.

1. MySQL에서 트랜잭션 경계가 중요한 이유

트랜잭션 경계는 다음 네 가지 운영 요소를 동시에 결정한다.

  1. 잠금 보유 시간
    InnoDB row lock, gap lock, metadata lock은 트랜잭션이 끝날 때까지 유지될 수 있다. 트랜잭션이 길어지면 다른 세션의 UPDATE, DELETE, ALTER TABLE, 심지어 단순 조회까지 영향을 받을 수 있다.

  2. MVCC read view 수명
    REPEATABLE READ에서 일관 읽기 read view는 트랜잭션 단위로 유지될 수 있다. 오래 열린 트랜잭션은 purge가 오래된 undo record를 정리하지 못하게 만들고, undo tablespace 증가와 성능 저하로 이어진다.

  3. redo/undo 및 binlog 반영 단위
    InnoDB는 변경 내용을 undo log와 redo log에 기록하고, MySQL 서버 계층은 binary log와 트랜잭션 커밋 순서를 조정한다. 큰 트랜잭션은 커밋 시점의 I/O 부담과 복제 적용 부담을 키운다.

  4. 장애 복구와 롤백 비용
    이미 많은 행을 변경한 트랜잭션이 실패하면 rollback 자체가 큰 작업이 된다. 운영자는 “실행 중인 쿼리 종료”와 “트랜잭션 정리 완료”를 구분해야 한다.

즉 트랜잭션은 애플리케이션 로직의 원자성을 보장하는 장치인 동시에, 데이터베이스 내부 자원의 보유 기간을 정의하는 운영 단위다.

2. autocommit의 의미와 기본 실행 경로

MySQL의 기본값은 일반적으로 autocommit=1이다. 이 상태에서는 각 SQL 문이 독립된 트랜잭션처럼 실행된다. 예를 들어 다음 문장은 별도의 START TRANSACTION이 없어도 하나의 트랜잭션으로 처리되고 문장 종료 시 자동 커밋된다.

DROP TABLE IF EXISTS account_ledger;
DROP TABLE IF EXISTS accounts;

CREATE TABLE accounts (
    account_id BIGINT PRIMARY KEY,
    balance BIGINT NOT NULL
) ENGINE=InnoDB;

CREATE TABLE account_ledger (
    ledger_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    account_id BIGINT NOT NULL,
    amount BIGINT NOT NULL,
    memo VARCHAR(100) NOT NULL,
    created_at DATETIME NOT NULL
) ENGINE=InnoDB;

INSERT INTO accounts VALUES (101, 50000), (202, 30000);

UPDATE accounts
   SET balance = balance - 10000
 WHERE account_id = 101;

내부적으로는 대략 다음 흐름으로 이해할 수 있다.

  1. 서버 계층이 SQL을 파싱하고 실행 계획을 만든다.
  2. InnoDB가 필요한 record lock 또는 gap lock을 획득한다.
  3. 변경 전 이미지를 undo log에 기록하고 변경 내용을 buffer pool page에 반영한다.
  4. redo log에 복구 가능한 변경 기록을 남긴다.
  5. 문장 실행이 성공하면 MySQL은 해당 단일 문장을 트랜잭션으로 커밋한다.
  6. 커밋 후 잠금이 해제되고, binlog가 활성화되어 있으면 트랜잭션 이벤트가 복제 대상에 전달될 수 있는 상태가 된다.

autocommit=1의 장점은 트랜잭션이 짧다는 것이다. 대부분의 단건 OLTP 요청은 이 방식이 운영상 안전하다. 그러나 여러 SQL을 하나의 원자적 작업으로 묶어야 하는 업무에서는 부족하다. 예를 들어 한 계좌에서 차감하고 다른 계좌에 입금하는 두 문장은 각각 성공할 수 있어도 두 문장 사이 장애가 발생하면 업무 불일치가 생긴다.

-- autocommit=1 상태에서 이 두 문장은 서로 다른 트랜잭션이다.
UPDATE accounts SET balance = balance - 10000 WHERE account_id = 101;
UPDATE accounts SET balance = balance + 10000 WHERE account_id = 202;

따라서 autocommit=1은 “트랜잭션이 없다”가 아니라 “각 문장이 자동으로 커밋되는 짧은 트랜잭션”이라고 이해해야 한다.

3. 명시적 트랜잭션: START TRANSACTION, COMMIT, ROLLBACK

여러 SQL을 하나의 원자적 단위로 묶으려면 명시적으로 트랜잭션을 시작해야 한다.

START TRANSACTION;

UPDATE accounts
   SET balance = balance - 10000
 WHERE account_id = 101;

UPDATE accounts
   SET balance = balance + 10000
 WHERE account_id = 202;

INSERT INTO account_ledger(account_id, amount, memo, created_at)
VALUES (101, -10000, 'transfer out', NOW()),
       (202,  10000, 'transfer in',  NOW());

COMMIT;

중간에 오류가 발생하면 다음처럼 되돌릴 수 있다.

START TRANSACTION;

UPDATE accounts
   SET balance = balance - 10000
 WHERE account_id = 101;

-- 애플리케이션 검증 실패 또는 후속 SQL 실패
ROLLBACK;

명시적 트랜잭션이 시작되면 autocommit=1 상태에서도 COMMIT 또는 ROLLBACK 전까지 자동 커밋되지 않는다. 이 구간에서 획득한 InnoDB 잠금과 read view는 트랜잭션 종료 시점까지 유지될 수 있다. 따라서 운영 관점에서는 명시적 트랜잭션을 사용할 때 다음 원칙이 중요하다.

  • 사용자 입력 대기, 외부 API 호출, 파일 업로드, 메시지 큐 대기 같은 느린 작업을 트랜잭션 안에 넣지 않는다.
  • 같은 업무 단위 안에서도 잠금이 필요한 SQL을 가능한 뒤쪽에 배치한다.
  • 대량 변경은 한 번에 하나의 거대한 트랜잭션으로 처리하지 말고 작은 배치로 나눈다.
  • 예외 발생 시 애플리케이션 connection pool에 열린 트랜잭션이 반환되지 않도록 반드시 정리한다.

4. SET autocommit=0의 특성

SET autocommit=0은 세션의 자동 커밋 동작을 끄는 설정이다. 이 모드에서는 명시적으로 START TRANSACTION을 쓰지 않아도 변경 SQL 실행 시 트랜잭션이 시작되고, COMMIT 또는 ROLLBACK 후 다음 SQL에서 다시 새 트랜잭션이 시작될 수 있다.

DROP TABLE IF EXISTS inventory;

CREATE TABLE inventory (
    sku VARCHAR(30) PRIMARY KEY,
    stock INT NOT NULL
) ENGINE=InnoDB;

INSERT INTO inventory VALUES ('SKU-001', 10);

SET autocommit = 0;

UPDATE inventory
   SET stock = stock - 1
 WHERE sku = 'SKU-001';

-- 여기서 COMMIT하지 않으면 잠금이 계속 유지될 수 있다.
COMMIT;

SET autocommit = 1;
DROP TABLE inventory;

운영 환경에서 autocommit=0은 신중해야 한다. 일부 배치 도구나 클라이언트가 세션 설정을 바꾼 뒤 명확히 되돌리지 않으면 connection pool을 통해 다음 요청에 영향을 줄 수 있다. MySQL 클라이언트에서 수동으로 점검할 때도 autocommit=0 상태를 잊고 터미널을 방치하면 긴 트랜잭션이 남을 수 있다.

현재 세션의 상태는 다음처럼 확인한다.

SELECT @@session.autocommit AS session_autocommit,
       @@global.autocommit  AS global_autocommit,
       @@transaction_isolation AS transaction_isolation;

실행 결과(MySQL 8.0.46):

+--------------------+-------------------+-----------------------+
| session_autocommit | global_autocommit | transaction_isolation |
+--------------------+-------------------+-----------------------+
|                  1 |                 1 | REPEATABLE-READ       |
+--------------------+-------------------+-----------------------+

운영 표준으로는 대부분의 웹 OLTP 서비스에서 autocommit=1을 유지하고, 필요한 업무 단위만 START TRANSACTION으로 묶는 방식이 명확하고 안전하다.

5. implicit commit: 트랜잭션을 암묵적으로 끊는 SQL

MySQL에는 실행 전후로 현재 트랜잭션을 암묵적으로 커밋하는 문장이 있다. 이를 implicit commit이라고 한다. 대표적으로 많은 DDL 문장이 여기에 해당한다.

START TRANSACTION;

UPDATE accounts
   SET balance = balance - 10000
WHERE account_id = 101;

ALTER TABLE accounts ADD COLUMN last_reviewed_at datetime NULL;

ROLLBACK;

SELECT * 
FROM accounts 
WHERE account_id = 101;

위 예처럼 트랜잭션 중간에 ALTER TABLE 같은 DDL을 섞는 설계를 기대하면 위험하다. MySQL에서는 ALTER TABLE 같은 문장이 실행되기 전에 기존 트랜잭션이 커밋될 수 있고, DDL 자체도 별도의 트랜잭션 경계를 가진다. MySQL 8.0의 atomic DDL은 DDL의 데이터 딕셔너리 변경과 스토리지 엔진 변경을 더 원자적으로 처리하도록 개선한 기능이지, 사용자가 DDL을 일반 DML 트랜잭션과 함께 ROLLBACK할 수 있다는 뜻이 아니다.

일반적으로 implicit commit을 유발할 수 있는 범주는 다음과 같다.

범주 예시 운영상 의미
DDL CREATE TABLE, ALTER TABLE, DROP TABLE, TRUNCATE TABLE 기존 DML 트랜잭션과 같은 롤백 단위로 묶을 수 없다고 봐야 한다.
계정/권한 관리 CREATE USER, ALTER USER, GRANT, REVOKE 운영 변경 작업과 업무 DML을 같은 세션 트랜잭션에 섞지 않는다.
트랜잭션 제어 START TRANSACTION, BEGIN, LOCK TABLES, UNLOCK TABLES 현재 트랜잭션 경계를 바꿀 수 있으므로 도구가 자동 실행하는지 확인한다.
테이블 관리 ANALYZE TABLE, OPTIMIZE TABLE, REPAIR TABLE 관리 작업을 애플리케이션 트랜잭션 세션과 분리한다.

정확한 목록은 MySQL 버전별 공식 문서를 확인해야 하지만, 운영 원칙은 단순하다. DDL과 관리 명령은 업무 DML 트랜잭션과 같은 세션에서 섞지 않는다. 배포 도구, 마이그레이션 도구, 수동 작업 세션을 업무 처리 세션과 분리하면 대부분의 사고를 예방할 수 있다.

6. InnoDB 내부 관점: 잠금, read view, undo, purge

트랜잭션이 열린 상태에서는 InnoDB 내부에 여러 흔적이 남는다.

  • 변경된 행의 이전 버전은 undo log를 통해 보존된다.
  • 일관 읽기를 위해 read view가 유지될 수 있다.
  • 변경한 행의 row lock은 커밋 또는 롤백까지 유지된다.
  • 격리 수준과 조건에 따라 gap lock 또는 next-key lock이 유지될 수 있다.
  • purge thread는 더 이상 참조되지 않는 undo record를 정리하지만, 오래된 read view가 남아 있으면 정리를 지연한다.

이 때문에 “쿼리는 끝났는데 트랜잭션이 남아 있는” 상태가 특히 위험하다. 예를 들어 애플리케이션이 START TRANSACTIONSELECT ... FOR UPDATE를 실행하고 예외 처리에서 ROLLBACK을 빠뜨리면, 해당 connection이 pool에 반환되어도 세션에는 잠금이 남을 수 있다. 이후 다른 요청이 같은 connection을 재사용하면 원인을 추적하기 더 어려워진다.

긴 트랜잭션은 다음 증상으로 나타난다.

  • SHOW ENGINE INNODB STATUS에서 History list length가 계속 증가한다.
  • 특정 행 변경이 lock wait timeout 또는 deadlock으로 실패한다.
  • 온라인 DDL이 metadata lock 대기로 멈춘다.
  • replica에서 큰 트랜잭션 적용이 오래 걸려 지연이 증가한다.
  • Aurora MySQL에서 purge 지연과 undo 관련 지표가 악화되며, writer 부하가 커질 수 있다.

7. 진단 SQL: 현재 열린 트랜잭션과 잠금 대기 확인

운영자는 먼저 “누가 트랜잭션을 열어 두었는가”를 확인해야 한다. MySQL 8.0에서는 information_schema.innodb_trx와 Performance Schema를 함께 보는 방식이 유용하다.

SELECT trx.trx_id,
       trx.trx_state,
       trx.trx_started,
       TIMESTAMPDIFF(SECOND, trx.trx_started, NOW()) AS trx_age_seconds,
       trx.trx_mysql_thread_id AS processlist_id,
       ps.USER,
       ps.HOST,
       ps.DB,
       ps.COMMAND,
       ps.TIME,
       LEFT(ps.INFO, 200) AS current_sql
  FROM information_schema.innodb_trx AS trx
  LEFT JOIN information_schema.processlist AS ps
    ON ps.ID = trx.trx_mysql_thread_id
 ORDER BY trx.trx_started;

잠금 대기를 확인하려면 Performance Schema의 data lock 테이블을 사용할 수 있다.

SELECT w.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx_id,
       w.BLOCKING_ENGINE_TRANSACTION_ID   AS blocking_trx_id,
       rl.OBJECT_SCHEMA,
       rl.OBJECT_NAME,
       rl.INDEX_NAME,
       rl.LOCK_TYPE,
       rl.LOCK_MODE AS waiting_lock_mode,
       bl.LOCK_MODE AS blocking_lock_mode
  FROM performance_schema.data_lock_waits AS w
  JOIN performance_schema.data_locks AS rl
    ON rl.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_ID
  JOIN performance_schema.data_locks AS bl
    ON bl.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID;

metadata lock 대기는 DDL 장애의 핵심 원인이다. 다음 질의는 어떤 세션이 metadata lock을 기다리거나 보유하는지 파악하는 출발점이 된다.

SELECT ml.OBJECT_TYPE,
       ml.OBJECT_SCHEMA,
       ml.OBJECT_NAME,
       ml.LOCK_TYPE,
       ml.LOCK_DURATION,
       ml.LOCK_STATUS,
       th.PROCESSLIST_ID,
       th.PROCESSLIST_USER,
       th.PROCESSLIST_HOST,
       th.PROCESSLIST_DB,
       th.PROCESSLIST_COMMAND,
       th.PROCESSLIST_TIME,
       LEFT(th.PROCESSLIST_INFO, 200) AS processlist_info
  FROM performance_schema.metadata_locks AS ml
  JOIN performance_schema.threads AS th
    ON th.THREAD_ID = ml.OWNER_THREAD_ID
 WHERE ml.OBJECT_SCHEMA NOT IN ('mysql', 'performance_schema', 'sys')
 ORDER BY ml.OBJECT_SCHEMA, ml.OBJECT_NAME, ml.LOCK_STATUS;

긴 트랜잭션 후보를 찾은 뒤에는 즉시 KILL하기보다 업무 영향도를 확인해야 한다. 단순 조회 트랜잭션이라도 오래된 read view 때문에 purge를 막고 있을 수 있고, 대량 변경 트랜잭션을 강제 종료하면 rollback이 오래 걸릴 수 있다.

-- 세션 종료가 필요하다고 판단한 뒤에만 실제 KILL 문장을 만든다.
SELECT CONCAT('KILL ', ID, ';') AS kill_statement
FROM information_schema.processlist
WHERE ID = CONNECTION_ID();

실행 결과(MySQL 8.0.46):

+----------------+
| kill_statement |
+----------------+
| KILL 21;       |
+----------------+

8. 애플리케이션과 connection pool에서의 주의점

트랜잭션 문제의 상당수는 SQL 자체보다 connection 생명주기에서 발생한다. 일반적인 위험 패턴은 다음과 같다.

1. connection pool에서 connection 획득
2. START TRANSACTION 실행
3. 일부 DML 실행
4. 애플리케이션 예외 발생
5. COMMIT/ROLLBACK 없이 connection 반환
6. 다음 요청이 같은 connection을 재사용

이 패턴을 막으려면 애플리케이션 계층에서 다음을 강제해야 한다.

  • 트랜잭션 블록은 언어별 try/finally, defer, context manager, transaction template 등으로 감싼다.
  • 예외 발생 시 rollback을 보장한다.
  • connection 반환 전 autocommit, 격리 수준, session variable을 표준 상태로 되돌린다.
  • connection pool의 validation query가 트랜잭션을 의도치 않게 유지하지 않는지 확인한다.
  • ORM이 lazy loading 또는 stream result를 트랜잭션 밖으로 끌고 나가지 않도록 한다.

예를 들어 의사 코드 수준의 안전한 구조는 다음과 같다.

connection = pool.get()
try:
    connection.begin()
    execute_business_dml()
    connection.commit()
except:
    connection.rollback()
    raise
finally:
    reset_session_state_if_needed()
    pool.release(connection)

운영자는 “애플리케이션에서 트랜잭션을 쓴다”는 사실보다 “실패 경로에서도 트랜잭션이 닫힌다”는 사실을 검증해야 한다.

9. Aurora MySQL에서의 운영 해석

Aurora MySQL은 MySQL 호환 SQL 계층을 제공하지만 스토리지 구조와 복제 방식이 다르다. 트랜잭션 경계 자체의 SQL 의미는 MySQL과 호환되지만, 운영 관찰 지점은 다를 수 있다.

  • Aurora의 writer는 분산 스토리지 계층에 redo 중심의 변경을 반영한다. 큰 트랜잭션은 커밋과 복구 경로에서 더 큰 부담을 줄 수 있다.
  • reader endpoint를 사용하는 읽기 트래픽은 writer와 분리되지만, 긴 트랜잭션과 purge 지연이 writer의 내부 정리 작업에 영향을 줄 수 있다.
  • replica lag 해석은 Community MySQL의 전통적 replica와 지표 체계가 다르다. CloudWatch, Performance Insights, Database Insights에서 트랜잭션 대기, lock wait, commit latency, DML 부하를 함께 봐야 한다.
  • parameter group에서 격리 수준, lock wait timeout, binlog 관련 설정을 바꿀 때는 클러스터 단위 영향과 재시작 필요 여부를 확인해야 한다.

Aurora에서도 원칙은 동일하다. 트랜잭션을 짧게 유지하고, 큰 변경은 작은 단위로 나누며, DDL과 DML 트랜잭션을 섞지 않는다. Aurora가 스토리지를 자동 복제한다고 해서 애플리케이션 트랜잭션 경계의 책임이 사라지는 것은 아니다.

10. 장애와 오해 사례

10.1 ROLLBACK하면 모든 것이 되돌아간다는 오해

DML은 트랜잭션 안에서 롤백될 수 있지만, DDL은 implicit commit을 동반할 수 있다. 운영 중 스키마 변경과 데이터 보정 작업을 같은 트랜잭션으로 묶어 “한 번에 되돌릴 수 있다”고 가정하면 안 된다. 데이터 보정은 별도의 검증 가능한 배치로 만들고, 스키마 변경은 온라인 DDL 전략과 rollback plan을 따로 준비해야 한다.

10.2 autocommit=1이면 잠금 문제가 없다는 오해

autocommit=1에서도 단일 문장이 오래 실행되면 잠금은 오래 유지된다. 예를 들어 인덱스가 없는 조건으로 대량 UPDATE를 수행하면 문장 하나가 큰 트랜잭션이 된다.

DROP TABLE IF EXISTS orders;

CREATE TABLE orders (
    order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
    customer_id BIGINT NOT NULL,
    status VARCHAR(20) NOT NULL,
    expire_at DATETIME NOT NULL,
    updated_at DATETIME NULL,
    KEY ix_status_expire_order (status, expire_at, order_id),
    KEY ix_customer (customer_id)
) ENGINE=InnoDB;

INSERT INTO orders (customer_id, status, expire_at) VALUES
(1001, 'PENDING', '2026-05-01 00:00:00'),
(1001, 'PAID', '2026-06-01 00:00:00'),
(1002, 'PENDING', '2026-05-02 00:00:00');

UPDATE orders
   SET status = 'EXPIRED'
 WHERE expire_at < NOW()
   AND status = 'PENDING';

위 문장에 적절한 인덱스가 없고 대상 행이 많으면 autocommit=1이어도 운영 장애를 만들 수 있다. 자동 커밋은 짧은 트랜잭션을 유도할 뿐, 나쁜 실행 계획이나 대량 변경의 위험을 제거하지 않는다.

10.3 SELECT만 했으므로 영향이 없다는 오해

격리 수준과 SQL 형태에 따라 조회도 영향을 줄 수 있다. SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE 또는 FOR SHARE는 잠금을 획득한다. 일반 consistent read도 오래 열린 트랜잭션 안에서는 purge를 지연시킬 수 있다.

START TRANSACTION;
SELECT *
  FROM orders
 WHERE customer_id = 1001
 FOR UPDATE;
-- 이후 애플리케이션이 대기하면 해당 행 잠금이 계속 유지된다.
ROLLBACK;

실행 결과(MySQL 8.0.46):

+----------+-------------+---------+---------------------+------------+
| order_id | customer_id | status  | expire_at           | updated_at |
+----------+-------------+---------+---------------------+------------+
|        1 |        1001 | EXPIRED | 2026-05-01 00:00:00 | NULL       |
|        2 |        1001 | PAID    | 2026-06-01 00:00:00 | NULL       |
+----------+-------------+---------+---------------------+------------+

10.4 마이그레이션 도구의 자동 트랜잭션을 과신하는 문제

일부 마이그레이션 도구는 여러 변경문을 트랜잭션으로 묶는 옵션을 제공하지만, MySQL DDL의 implicit commit 특성 때문에 기대와 다르게 동작할 수 있다. 도구가 “transactional migration”을 지원한다고 표시하더라도 MySQL dialect에서 실제 rollback 가능 범위를 확인해야 한다.

11. 운영 체크리스트

설계 및 개발 단계

  • 기본 세션은 autocommit=1

배포 및 운영 단계

  • 장애 대응 시 KILL
  • 운영 수동 작업 세션은 작업 종료 후 SELECT @@autocommit

SQL 리뷰 기준

  • ALTER, CREATE, DROP, TRUNCATE, GRANT, REVOKE

12. 실무 예시: 대량 상태 변경을 작은 트랜잭션으로 나누기

오래된 주문 상태를 한 번에 변경하는 대신 primary key 범위로 나누면 잠금 보유 시간과 복제 적용 부담을 줄일 수 있다.

-- 대상 후보를 먼저 확인한다.
SELECT COUNT(*) AS target_count
  FROM orders
 WHERE status = 'PENDING'
   AND expire_at < NOW();

실행 결과(MySQL 8.0.46):

+--------------+
| target_count |
+--------------+
|            0 |
+--------------+

애플리케이션 또는 배치 스크립트에서는 다음과 같은 패턴을 사용한다.

START TRANSACTION;

UPDATE orders
   SET status = 'EXPIRED',
       updated_at = NOW()
 WHERE status = 'PENDING'
   AND expire_at < NOW()
 ORDER BY order_id
 LIMIT 1000;

COMMIT;

DROP TABLE orders;
DROP TABLE account_ledger;
DROP TABLE accounts;

반복 실행 사이에는 짧은 휴지 시간을 두고, 각 배치의 변경 건수와 lock wait 발생 여부를 기록한다. MySQL에서 UPDATE ... ORDER BY ... LIMIT는 상황에 따라 적절한 인덱스가 없으면 여전히 비용이 클 수 있으므로 (status, expire_at, order_id)와 같은 접근 경로를 검토해야 한다.

운영 셸에서는 반복 배치 실행 전후로 상태를 확인할 수 있다.

mysql --defaults-extra-file=/path/to/client.cnf -e \
"SELECT NOW() AS checked_at, COUNT(*) AS pending_expired
   FROM app.orders
  WHERE status = 'PENDING'
    AND expire_at < NOW();"

위 예시는 명령 형식을 보여 주기 위한 것이다. 실제 운영에서는 인증 파일 권한, 실행 계정, 감사 로그, 변경 승인 절차를 별도로 관리해야 한다.

13. 결론

MySQL의 트랜잭션 처리는 autocommit=1이라는 기본값 때문에 단순해 보이지만, 실제 운영에서는 명시적 트랜잭션과 implicit commit의 경계가 장애 원인을 좌우한다. autocommit=1은 각 문장을 짧은 트랜잭션으로 만든다. START TRANSACTION은 여러 문장을 하나의 원자적 단위로 묶지만, 그만큼 잠금과 read view의 수명을 늘린다. DDL과 관리 명령은 implicit commit을 유발할 수 있으므로 DML 트랜잭션과 같은 롤백 단위로 취급해서는 안 된다.

운영자가 가져야 할 핵심 기준은 명확하다. 트랜잭션은 짧게 유지하고, 경계를 코드에서 명시하며, 실패 경로에서 반드시 닫고, DDL과 DML을 섞지 않는다. 이 기준은 이후 격리 수준, InnoDB locking read, deadlock 분석, metadata lock, 복제 지연을 이해하는 기초가 된다. 다음 단계에서는 트랜잭션 격리 수준과 MVCC read view가 실제 쿼리 결과와 잠금 범위에 어떤 차이를 만드는지 더 깊게 살펴볼 수 있다.