---
title: "wait/lock/metadata/sql/mdl로 MDL 병목 관찰하기"
description: "MySQL Performance Schema에서 Metadata Lock의 대기 관계를 식별하고 DDL 지연과 운영 장애를 진단하는 방법을 정리한다."
tags: [ MySQL, 락, 운영, DBA ]
image: "mysql-report-bg.png"
published: "2026-08-19"
updated: "2026-08-19"
author: "MySQL 기술 노트"
source_url: ""
---

MySQL에서 `ALTER TABLE`이 오래 멈춰 있을 때 원인은 DDL 자체의 연산량이 아닐 수 있다. 먼저 시작된 트랜잭션이 테이블의 Metadata Lock(MDL)을 계속 보유하면, DDL은 실행 단계에 진입하지 못한 채 배타적 MDL을 기다린다. 더 까다로운 점은 대기 중인 DDL 뒤로 평범한 조회까지 줄지어 대기하여 장애 범위가 빠르게 넓어질 수 있다는 사실이다.

`wait/lock/metadata/sql/mdl`은 이러한 MDL의 획득과 대기를 Performance Schema에 노출하는 핵심 instrument다. 이 글에서는 MDL의 내부 역할, `performance_schema.metadata_locks`를 읽는 방법, 재현 가능한 대기 시나리오, 운영 진단 순서와 Aurora MySQL에서의 주의점을 다룬다.

## 1. MDL은 무엇을 보호하는가

MDL은 InnoDB의 record lock이나 gap lock과 목적이 다르다. InnoDB 잠금은 주로 행 데이터의 동시 변경과 트랜잭션 격리를 보호하지만, MDL은 SQL 계층의 객체 정의가 문장 실행 중에 갑자기 바뀌지 않도록 보호한다.

예를 들어 한 세션이 `orders`를 읽는 동안 다른 세션이 같은 테이블을 삭제하거나 컬럼 정의를 바꾸면, 이미 해석된 실행 계획과 실제 객체 정의가 어긋날 수 있다. MySQL 서버는 조회 세션에 공유 계열 MDL을 부여하고, DDL에는 배타 계열 MDL을 요구하여 이 충돌을 직렬화한다.

```mermaid
flowchart LR
    A[세션 A: 트랜잭션 시작] --> B[orders 조회]
    B --> C[SHARED_READ MDL 보유]
    D[세션 B: ALTER TABLE] --> E[EXCLUSIVE MDL 요청]
    C --> F{호환 가능한가?}
    E --> F
    F -->|아니오| G[세션 B: PENDING]
    G --> H[세션 A COMMIT 또는 ROLLBACK]
    H --> I[EXCLUSIVE MDL 획득]
    I --> J[DDL 실행]
```

MDL의 중요한 특성은 다음과 같다.

- 객체 단위로 관리된다. 테이블뿐 아니라 schema, stored program, tablespace 등에도 적용될 수 있다.
- 문장 또는 트랜잭션 수명을 가질 수 있다. 트랜잭션에서 접근한 테이블의 MDL은 일반적으로 트랜잭션 종료까지 유지된다.
- SQL 계층에서 관리되므로 InnoDB 행 잠금만 조회해서는 보이지 않는다.
- DDL이 대기열에 들어가면 이후의 공유 요청도 공정성 및 우선순위 규칙에 따라 대기할 수 있다.

따라서 “DDL이 아무 일도 하지 않는데 CPU도 낮다”는 현상은 비정상이 아니라, 필요한 MDL을 아직 얻지 못한 상태일 수 있다.

## 2. wait/lock/metadata/sql/mdl instrument의 역할

Performance Schema의 instrument 이름은 관측 대상의 계층을 나타낸다.

- `wait`: 대기 이벤트 계열
- `lock`: 잠금 획득 대기
- `metadata`: 데이터가 아니라 객체 메타데이터
- `sql/mdl`: SQL 계층의 Metadata Lock 구현

이 instrument가 활성화되면 현재 보유 중이거나 요청 중인 MDL을 `performance_schema.metadata_locks`에서 관찰할 수 있다. 먼저 대상 버전과 활성화 상태를 확인한다.

```sql
SELECT VERSION() AS mysql_version;

SELECT NAME, ENABLED, TIMED
FROM performance_schema.setup_instruments
WHERE NAME = 'wait/lock/metadata/sql/mdl';

SHOW TABLES FROM performance_schema LIKE 'metadata_locks';
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT VERSION() AS mysql_version;

+---------------+
| mysql_version |
+---------------+
| 8.0.46        |
+---------------+
1 row in set (0.00 sec)

mysql> SELECT NAME, ENABLED, TIMED
    -> FROM performance_schema.setup_instruments
    -> WHERE NAME = 'wait/lock/metadata/sql/mdl';

+----------------------------+---------+-------+
| NAME                       | ENABLED | TIMED |
+----------------------------+---------+-------+
| wait/lock/metadata/sql/mdl | YES     | YES   |
+----------------------------+---------+-------+
1 row in set (0.00 sec)

mysql> SHOW TABLES FROM performance_schema LIKE 'metadata_locks';

+-----------------------------------------------+
| Tables_in_performance_schema (metadata_locks) |
+-----------------------------------------------+
| metadata_locks                                |
+-----------------------------------------------+
1 row in set (0.00 sec)
```

`ENABLED='YES'`이면 이벤트를 수집한다. `TIMED='YES'`는 해당 instrument의 시간 측정을 허용한다. 다만 MDL 장애의 핵심 증거는 대개 `metadata_locks`의 `GRANTED`와 `PENDING` 관계, 그리고 요청 세션의 SQL이다. 시간 집계 하나만으로 blocker를 특정할 수는 없다.

운영 환경에서 비활성 상태라면 다음과 같이 활성화할 수 있다. Performance Schema 설정 변경은 서버 재시작 후 유지된다고 가정해서는 안 되며, 배포 표준과 parameter 관리 정책에 반영해야 한다.

```sql
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME = 'wait/lock/metadata/sql/mdl';

SELECT NAME, ENABLED, TIMED
FROM performance_schema.setup_instruments
WHERE NAME = 'wait/lock/metadata/sql/mdl';
```

실행 결과(MySQL 8.0.x):

```text
mysql> UPDATE performance_schema.setup_instruments
    -> SET ENABLED = 'YES', TIMED = 'YES'
    -> WHERE NAME = 'wait/lock/metadata/sql/mdl';

Query OK, 0 rows affected (0.00 sec)
Rows matched: 1  Changed: 0  Warnings: 0

mysql> SELECT NAME, ENABLED, TIMED
    -> FROM performance_schema.setup_instruments
    -> WHERE NAME = 'wait/lock/metadata/sql/mdl';

+----------------------------+---------+-------+
| NAME                       | ENABLED | TIMED |
+----------------------------+---------+-------+
| wait/lock/metadata/sql/mdl | YES     | YES   |
+----------------------------+---------+-------+
1 row in set (0.00 sec)
```

관측 기능을 켜는 작업과 문제를 해결하는 작업은 구분해야 한다. instrument 활성화는 잠금을 해제하지 않으며, 이미 누적된 대기열을 자동으로 정리하지도 않는다.

## 3. metadata_locks의 핵심 열 읽기

`performance_schema.metadata_locks`는 MDL 요청을 한 행으로 표현한다. 실무에서 먼저 볼 열은 다음과 같다.

| 열 | 의미 | 운영 해석 |
|---|---|---|
| `OBJECT_TYPE` | 잠금 대상 객체 종류 | `TABLE`, `SCHEMA` 등 범위를 구분한다. |
| `OBJECT_SCHEMA` | schema 이름 | 동일한 테이블명이 여러 schema에 있을 때 구분한다. |
| `OBJECT_NAME` | 객체 이름 | 장애 대상 DDL과 직접 연결한다. |
| `LOCK_TYPE` | 요청한 MDL 모드 | 공유 계열인지 배타 계열인지 확인한다. |
| `LOCK_DURATION` | 잠금 수명 | `STATEMENT`, `TRANSACTION`, `EXPLICIT`를 구분한다. |
| `LOCK_STATUS` | 획득 상태 | `GRANTED`는 보유, `PENDING`은 대기를 뜻한다. |
| `OWNER_THREAD_ID` | Performance Schema thread ID | `performance_schema.threads`와 연결하는 키다. |
| `OWNER_EVENT_ID` | 요청 이벤트 ID | statement event와의 상관관계에 활용한다. |

`LOCK_TYPE` 이름만 보고 blocker를 단정해서는 안 된다. 같은 객체의 `GRANTED` 행과 `PENDING` 행을 함께 모으고, 각 행의 `OWNER_THREAD_ID`를 세션과 SQL에 연결해야 한다. 또한 schema-level lock과 table-level lock이 동시에 관측될 수 있으므로 객체 이름만 필터링하면 필요한 상위 객체 잠금을 놓칠 수 있다.

## 4. MDL 대기 재현과 관찰

다음 예제는 Event Scheduler를 두 번째 서버 세션처럼 사용한다. 현재 세션이 `mdl_demo`를 읽은 뒤 트랜잭션을 끝내지 않고, 예약된 `ALTER TABLE`이 배타적 MDL을 요청하도록 만든다. `SLEEP()` 중 DDL 세션이 대기열에 들어가며, 이어지는 조회에서 `GRANTED`와 `PENDING`을 함께 확인한다.

이 예제는 전용 검증 인스턴스를 위한 것이다. 운영 서버에서 Event Scheduler를 임의로 활성화하거나 동일한 DDL을 실행하지 않는다.

```sql
SET GLOBAL event_scheduler = ON;
DROP EVENT IF EXISTS mdl_wait_event;
DROP TABLE IF EXISTS mdl_demo;

CREATE TABLE mdl_demo (
    id BIGINT NOT NULL PRIMARY KEY,
    payload VARCHAR(100) NOT NULL
) ENGINE = InnoDB;

INSERT INTO mdl_demo VALUES (1, 'metadata lock demo');

CREATE EVENT mdl_wait_event
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 SECOND
ON COMPLETION PRESERVE
DO ALTER TABLE mdl_demo ADD COLUMN note VARCHAR(30) NULL;

START TRANSACTION;
SELECT * FROM mdl_demo WHERE id = 1;
DO SLEEP(4);

SELECT ml.OBJECT_TYPE,
       ml.OBJECT_SCHEMA,
       ml.OBJECT_NAME,
       ml.LOCK_TYPE,
       ml.LOCK_DURATION,
       ml.LOCK_STATUS,
       t.PROCESSLIST_ID,
       t.PROCESSLIST_STATE
FROM performance_schema.metadata_locks AS ml
LEFT JOIN performance_schema.threads AS t
  ON t.THREAD_ID = ml.OWNER_THREAD_ID
WHERE ml.OBJECT_SCHEMA = DATABASE()
  AND ml.OBJECT_NAME = 'mdl_demo'
ORDER BY ml.LOCK_STATUS, ml.LOCK_TYPE;

COMMIT;
DO SLEEP(3);
SHOW COLUMNS FROM mdl_demo;

DROP EVENT IF EXISTS mdl_wait_event;
DROP TABLE mdl_demo;
SET GLOBAL event_scheduler = OFF;
```

실행 결과(MySQL 8.0.x):

아래 결과는 준비·정리 DDL의 반복 메시지를 줄이고, MDL 대기와 DDL 완료를 입증하는 핵심 구간만 발췌한 것이다.

```text
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM mdl_demo WHERE id = 1;
+----+--------------------+
| id | payload            |
+----+--------------------+
|  1 | metadata lock demo |
+----+--------------------+
1 row in set (0.00 sec)

mysql> SELECT ml.OBJECT_TYPE, ...
    -> FROM performance_schema.metadata_locks AS ml ...;
+-------------+-----------------+-------------+-------------------+---------------+-------------+----------------+---------------------------------+
| OBJECT_TYPE | OBJECT_SCHEMA   | OBJECT_NAME | LOCK_TYPE         | LOCK_DURATION | LOCK_STATUS | PROCESSLIST_ID | PROCESSLIST_STATE               |
+-------------+-----------------+-------------+-------------------+---------------+-------------+----------------+---------------------------------+
| TABLE       | mysql_tech_note | mdl_demo    | SHARED_READ       | TRANSACTION   | GRANTED     |             13 | executing                       |
| TABLE       | mysql_tech_note | mdl_demo    | SHARED_UPGRADABLE | TRANSACTION   | GRANTED     |             14 | Waiting for table metadata lock |
| TABLE       | mysql_tech_note | mdl_demo    | EXCLUSIVE         | TRANSACTION   | PENDING     |             14 | Waiting for table metadata lock |
+-------------+-----------------+-------------+-------------------+---------------+-------------+----------------+---------------------------------+
3 rows in set (0.01 sec)

mysql> COMMIT;
Query OK, 0 rows affected (0.00 sec)

mysql> SHOW COLUMNS FROM mdl_demo;
+---------+--------------+------+-----+---------+-------+
| Field   | Type         | Null | Key | Default | Extra |
+---------+--------------+------+-----+---------+-------+
| id      | bigint       | NO   | PRI | NULL    |       |
| payload | varchar(100) | NO   |     | NULL    |       |
| note    | varchar(30)  | YES  |     | NULL    |       |
+---------+--------------+------+-----+---------+-------+
3 rows in set (0.00 sec)
```

관찰 시점에는 조회 트랜잭션의 공유 계열 잠금이 `GRANTED`, DDL이 요구하는 배타 계열 잠금이 `PENDING`으로 나타나야 한다. `COMMIT` 이후에는 공유 잠금이 해제되고 DDL이 진행되어 `note` 컬럼이 추가된다.

이 재현이 보여주는 핵심은 DDL 세션이 원인이 아니라 피해자일 수 있다는 점이다. 대기 중인 `ALTER TABLE`을 종료하면 당장의 대기열은 완화될 수 있지만, 장기 트랜잭션이 남아 있으면 다음 DDL에서도 같은 문제가 반복된다.

## 5. 운영 진단 쿼리

### 5.1 PENDING 요청과 세션 연결

다음 쿼리는 대기 중인 MDL 요청을 세션 정보 및 현재 statement와 연결한다. `events_statements_current.SQL_TEXT`는 instrumentation 상태와 문장 수명에 따라 `NULL`일 수 있으므로, 빈 값만으로 세션이 유휴 상태라고 단정하지 않는다.

```sql
SELECT ml.OBJECT_TYPE,
       ml.OBJECT_SCHEMA,
       ml.OBJECT_NAME,
       ml.LOCK_TYPE,
       ml.LOCK_DURATION,
       ml.LOCK_STATUS,
       t.PROCESSLIST_ID AS connection_id,
       t.PROCESSLIST_USER AS user_name,
       t.PROCESSLIST_TIME AS state_seconds,
       t.PROCESSLIST_STATE AS process_state,
       esc.SQL_TEXT AS current_sql
FROM performance_schema.metadata_locks AS ml
LEFT JOIN performance_schema.threads AS t
  ON t.THREAD_ID = ml.OWNER_THREAD_ID
LEFT JOIN performance_schema.events_statements_current AS esc
  ON esc.THREAD_ID = ml.OWNER_THREAD_ID
WHERE ml.LOCK_STATUS = 'PENDING'
ORDER BY t.PROCESSLIST_TIME DESC, ml.OBJECT_SCHEMA, ml.OBJECT_NAME;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT ml.OBJECT_TYPE,
    ->        ml.OBJECT_SCHEMA,
    ->        ml.OBJECT_NAME,
    ->        ml.LOCK_TYPE,
    ->        ml.LOCK_DURATION,
    ->        ml.LOCK_STATUS,
    ->        t.PROCESSLIST_ID AS connection_id,
    ->        t.PROCESSLIST_USER AS user_name,
    ->        t.PROCESSLIST_TIME AS state_seconds,
    ->        t.PROCESSLIST_STATE AS process_state,
    ->        esc.SQL_TEXT AS current_sql
    -> FROM performance_schema.metadata_locks AS ml
    -> LEFT JOIN performance_schema.threads AS t
    ->   ON t.THREAD_ID = ml.OWNER_THREAD_ID
    -> LEFT JOIN performance_schema.events_statements_current AS esc
    ->   ON esc.THREAD_ID = ml.OWNER_THREAD_ID
    -> WHERE ml.LOCK_STATUS = 'PENDING'
    -> ORDER BY t.PROCESSLIST_TIME DESC, ml.OBJECT_SCHEMA, ml.OBJECT_NAME;

Empty set (0.00 sec)
```

결과가 `Empty set`이면 조회 순간에 노출된 PENDING MDL이 없다는 뜻이다. 과거에 대기가 없었다는 뜻도 아니고, instrument가 꺼진 상태와도 자동으로 구분되지 않는다. 반드시 `setup_instruments` 상태를 함께 확인한다.

### 5.2 sys schema에서 blocker 관계 보기

MySQL의 `sys.schema_table_lock_waits`는 table MDL의 waiting/blocking 관계를 운영자가 읽기 쉬운 형태로 제공한다. 원본 `metadata_locks`를 직접 조인하기 전에 빠르게 상황을 파악하는 데 유용하다.

```sql
SELECT object_schema,
       object_name,
       waiting_pid,
       waiting_account,
       waiting_lock_type,
       waiting_query_secs,
       blocking_pid,
       blocking_account,
       blocking_lock_type
FROM sys.schema_table_lock_waits
ORDER BY waiting_query_secs DESC;
```

실행 결과(MySQL 8.0.x):

```text
mysql> SELECT object_schema,
    ->        object_name,
    ->        waiting_pid,
    ->        waiting_account,
    ->        waiting_lock_type,
    ->        waiting_query_secs,
    ->        blocking_pid,
    ->        blocking_account,
    ->        blocking_lock_type
    -> FROM sys.schema_table_lock_waits
    -> ORDER BY waiting_query_secs DESC;

Empty set (0.00 sec)
```

이 view가 비어 있어도 Performance Schema 설정, 대상 객체 종류, 관찰 시점을 확인해야 한다. 또한 `sql_kill_blocking_connection` 같은 생성형 열이 제공되더라도 내용을 검토하지 않고 실행해서는 안 된다. blocker가 중요한 배치나 복구 트랜잭션일 수 있으며, 연결 종료는 rollback 비용과 추가 부하를 유발한다.

### 5.3 장기 트랜잭션과 함께 해석하기

MDL blocker는 현재 활발히 SQL을 실행하는 세션이 아니라, 문장 실행을 마친 뒤 트랜잭션만 열린 세션일 수 있다. 따라서 다음 세 신호를 함께 본다.

1. `metadata_locks`: 어떤 객체의 어떤 잠금이 `GRANTED` 또는 `PENDING`인가
2. `threads`와 statement events: 어느 연결이 요청했으며 현재 무엇을 하는가
3. `information_schema.INNODB_TRX`: 트랜잭션 시작 시각, 상태, 변경 행 수가 어떠한가

MDL만 보고 가장 오래된 연결을 종료하거나, 반대로 `INNODB_TRX`만 보고 행 잠금 문제로 결론 내리는 것은 모두 위험하다. 객체 잠금과 트랜잭션 수명을 연결해야 한다.

## 6. 대기열 증폭: 왜 조회까지 멈추는가

대표적인 장애 순서는 다음과 같다.

1. 세션 A가 트랜잭션을 시작하고 테이블을 조회한다.
2. 세션 A가 `COMMIT` 또는 `ROLLBACK` 없이 유휴 상태가 된다.
3. 세션 B의 DDL이 `EXCLUSIVE` MDL을 요청하고 `PENDING`이 된다.
4. 이후 세션 C, D의 조회가 같은 객체로 들어온다.
5. 선행 배타 요청과 MDL 대기열 정책 때문에 후속 조회도 기다릴 수 있다.
6. 애플리케이션 connection pool이 대기 세션으로 채워지고 장애가 확대된다.

이 현상은 단순히 “DDL 한 개가 느리다”가 아니다. DDL 배포가 오래된 트랜잭션을 드러내는 계기가 되고, 대기열이 애플리케이션 자원 고갈로 연결되는 복합 장애다.

운영 대응의 우선순위는 보통 다음과 같다.

- 신규 대기 증가를 막기 위해 문제 DDL의 중단 여부를 판단한다.
- 원래의 `GRANTED` blocker와 열린 트랜잭션을 식별한다.
- 업무 영향과 rollback 비용을 평가한 뒤 정상 종료, 애플리케이션 취소, 연결 종료 중 하나를 선택한다.
- DDL을 재시도하기 전에 트랜잭션 수명과 배포 사전 점검 절차를 수정한다.

## 7. 흔한 오해와 실패 패턴

### 7.1 `SHOW PROCESSLIST`만 보면 충분하다

`SHOW PROCESSLIST`는 `Waiting for table metadata lock` 같은 상태를 보여줄 수 있지만, 잠금 대상과 보유자 관계를 완전하게 표현하지 않는다. waiting 세션을 발견한 뒤 `metadata_locks.OWNER_THREAD_ID`와 `threads.THREAD_ID`를 연결해야 한다.

### 7.2 DDL을 실행한 세션이 blocker다

대기 상태의 DDL은 흔히 blocker가 아니라 waiter다. 실제 blocker는 더 일찍 테이블에 접근한 장기 트랜잭션이다. `PENDING` 행만 보지 말고 동일 객체의 `GRANTED` 행을 함께 찾는다.

### 7.3 유휴 세션에는 잠금이 없다

클라이언트 관점에서 아무 SQL도 실행하지 않는 세션이라도 열린 트랜잭션은 MDL과 InnoDB 잠금을 유지할 수 있다. `Sleep` 상태와 트랜잭션 종료는 같은 개념이 아니다.

### 7.4 DDL timeout만 늘리면 해결된다

timeout 증가는 대기 허용 시간을 늘릴 뿐 blocker를 제거하지 않는다. 오히려 connection pool 점유 시간이 늘어 장애 범위를 키울 수 있다. DDL의 허용 대기 시간, 애플리케이션 query timeout, 배포 rollback 정책을 함께 설계해야 한다.

### 7.5 `performance_schema.metadata_locks`가 비어 있으므로 MDL이 없다

instrument가 비활성화되었거나 관찰 시점을 놓쳤을 수 있다. `wait/lock/metadata/sql/mdl`의 `ENABLED` 상태를 먼저 확인하고, 재발성 문제라면 주기 수집 또는 경보 체계를 마련한다.

### 7.6 모든 `GRANTED` 잠금을 종료해야 한다

정상적인 문장도 실행 중 MDL을 보유한다. `GRANTED` 자체는 장애가 아니다. 동일 객체의 `PENDING`, 보유 기간, 트랜잭션 수명, 업무 중요도를 함께 판단해야 한다.

## 8. Aurora MySQL에서의 운영 해석

Aurora MySQL도 MySQL 호환 SQL 계층에서 MDL을 사용하므로 기본 진단 원리는 같다. `performance_schema.metadata_locks`, `performance_schema.threads`, `sys.schema_table_lock_waits`를 우선 확인한다. 다만 관리형 환경에서는 다음 차이를 고려한다.

- Performance Schema 설정과 메모리 관련 옵션은 DB parameter group 및 엔진 버전의 영향을 받는다.
- writer instance에서 수행되는 DDL과 트랜잭션이 진단의 중심이다. reader endpoint의 조회 지연과 writer의 MDL 대기를 같은 현상으로 혼동하지 않는다.
- Performance Insights 또는 Database Insights의 대기 분류는 병목이 발생한 시간대를 찾는 보조 수단이다. 최종 blocker 관계는 당시의 세션 및 잠금 자료와 연결해야 한다.
- failover는 MDL 문제의 일반적인 해결책이 아니다. 연결과 진행 중 트랜잭션에 큰 영향을 주며, 원인이 애플리케이션의 긴 트랜잭션 수명이라면 새 writer에서도 재발할 수 있다.
- DDL 배포 전 writer의 장기 트랜잭션, 활성 세션 수, 배포 도구의 timeout과 재시도 정책을 확인한다.

Aurora의 분산 스토리지 구조는 백업과 복제 방식에 큰 차이를 만들지만, SQL 계층 객체 정의의 일관성을 보호해야 한다는 요구는 사라지지 않는다.

## 9. 예방 설계

### 애플리케이션 트랜잭션

- 트랜잭션 범위를 HTTP 요청이나 작업 단위보다 불필요하게 길게 잡지 않는다.
- 예외 경로에서도 `COMMIT` 또는 `ROLLBACK`이 보장되도록 한다.
- connection pool 반환 전에 열린 트랜잭션을 정리한다.
- 사용자 입력이나 외부 API 응답을 기다리는 동안 DB 트랜잭션을 유지하지 않는다.

### DDL 배포

- 배포 직전에 대상 객체의 `PENDING` MDL과 장기 트랜잭션을 확인한다.
- 낮은 `lock_wait_timeout`을 배포 세션에 적용하여 무한정 대기하지 않도록 검토한다.
- 온라인 DDL 옵션은 작업 단계의 동시성을 개선할 수 있지만, 시작과 종료에 필요한 MDL까지 없애지는 못한다.
- 재시도는 이전 DDL 세션이 실제로 종료되었는지 확인한 뒤 수행한다.
- 대규모 변경은 rollback 조건과 업무 영향도를 사전에 문서화한다.

### 관측과 경보

- `PENDING` MDL 개수뿐 아니라 가장 오래된 대기 시간과 대상 객체를 수집한다.
- 동일 객체의 waiting/blocking 세션 관계를 함께 보존한다.
- 장기 트랜잭션, connection pool 사용률, DDL 배포 이벤트와 같은 시간축에 배치한다.
- 순간적인 정상 MDL 획득을 과도하게 경보하지 않도록 지속 시간 조건을 둔다.

## 10. 장애 대응 체크리스트

- [ ] `wait/lock/metadata/sql/mdl` instrument가 활성화되어 있는가?
- [ ] `metadata_locks`에서 `PENDING`인 객체와 잠금 모드를 확인했는가?
- [ ] 동일 객체의 `GRANTED` 행을 찾아 잠재 blocker를 확인했는가?
- [ ] `OWNER_THREAD_ID`를 `threads`에 연결하여 connection ID와 계정을 확인했는가?
- [ ] 현재 SQL이 비어 있어도 열린 트랜잭션과 최근 statement를 확인했는가?
- [ ] 대기 DDL 뒤로 일반 조회가 누적되는지 확인했는가?
- [ ] 연결 종료 전에 업무 중요도와 rollback 비용을 평가했는가?
- [ ] DDL 취소와 blocker 종료 중 어떤 조치가 더 안전한지 판단했는가?
- [ ] Aurora MySQL이라면 writer instance와 parameter group 상태를 확인했는가?
- [ ] 조치 후 대기열 감소, connection pool 회복, DDL 상태를 다시 확인했는가?
- [ ] 재발 방지를 위해 트랜잭션 수명과 DDL 사전 점검 절차를 수정했는가?

## 11. 결론

`wait/lock/metadata/sql/mdl`은 DDL 지연을 막연한 “테이블 락”이 아니라 관찰 가능한 waiting/blocking 관계로 바꾸는 출발점이다. 핵심은 `metadata_locks`의 `PENDING` 요청만 보는 것이 아니라, 같은 객체의 `GRANTED` 보유자와 열린 트랜잭션, 현재 또는 최근 SQL을 함께 연결하는 데 있다.

MDL 장애는 대개 DDL 한 문장의 문제가 아니라 긴 트랜잭션, 배포 timeout, 대기열 증폭, connection pool 고갈이 결합된 운영 문제다. 다음 단계에서는 Performance Schema의 stage와 statement 자료를 함께 사용하여 DDL이 MDL 획득 이후 실제 어느 실행 단계에서 시간을 소비하는지 구분하는 방법을 다룰 수 있다.
