---
title: "Metadata Lock: ALTER, DDL, 긴 SELECT가 서로 막히는 원리"
description: "MySQL Metadata Lock의 획득과 대기 원리를 이해하고 긴 트랜잭션과 DDL 차단을 진단하고 예방하는 방법을 정리한다."
tags: [ MySQL, 트랜잭션, 락, 운영, DBA ]
image: "mysql-report-bg.png"
published: "2026-08-05"
updated: "2026-08-05"
author: "MySQL 기술 노트"
source_url: ""
---

## 1. 왜 `ALTER TABLE`이 갑자기 멈추는가

MySQL에서 `ALTER TABLE`이 오래 걸린다고 해서 항상 테이블 복사나 인덱스 생성이 느린 것은 아니다. 실제 작업을 시작하기도 전에 **Metadata Lock(MDL)** 을 얻지 못해 대기하는 경우가 많다. 반대로 DDL이 대기열에 들어온 뒤 새로 실행한 짧은 `SELECT`까지 줄줄이 멈출 수 있다. 이때 CPU 사용률과 디스크 처리량은 낮고, InnoDB row lock 조회에도 원인이 나타나지 않을 수 있다.

MDL은 테이블 이름, 정의, trigger, stored program 같은 데이터베이스 객체의 메타데이터가 사용 중에 바뀌지 않도록 보호한다. 실행 중인 문장이 참조하는 테이블을 다른 세션이 제거하거나, 트랜잭션이 같은 테이블을 계속 사용해야 하는 동안 정의가 바뀌면 SQL의 의미와 실행 계획을 안전하게 유지할 수 없기 때문이다.

운영에서 특히 중요한 사실은 다음 세 가지다.

1. 평범한 `SELECT`도 MDL을 획득한다.
2. 명시적 `BEGIN`뿐 아니라 `autocommit=0`에서 끝나지 않은 트랜잭션도 MDL을 오래 보유할 수 있다.
3. `ALGORITHM=INSTANT` 또는 `LOCK=NONE`인 DDL도 MDL이 전혀 필요 없는 것은 아니다.

따라서 MDL 장애는 “DDL 문제”가 아니라 **세션 수명, 트랜잭션 경계, 배포 절차가 만나는 운영 문제**로 보아야 한다.

## 2. MDL이 보호하는 범위

InnoDB row lock과 MDL은 보호 대상이 다르다.

| 구분 | 보호 대상 | 대표 대기 원인 | 주요 관측 위치 |
|---|---|---|---|
| Metadata Lock | 테이블·스키마·프로그램 객체의 정의와 이름 | 긴 트랜잭션 뒤의 DDL, DDL 뒤의 후속 쿼리 | `performance_schema.metadata_locks` |
| InnoDB record lock | 인덱스 record와 gap | 같은 행의 `UPDATE`, 범위 잠금, 외래 키 검사 | `performance_schema.data_locks`, `data_lock_waits` |
| Table lock | storage engine 또는 SQL 계층의 테이블 접근 | `LOCK TABLES`, 일부 엔진 작업 | `performance_schema.table_handles` 등 |

`SELECT * FROM orders WHERE id = 10`은 행을 변경하지 않더라도 `orders`의 정의가 실행 중에 사라지거나 바뀌지 않도록 보통 공유 성격의 MDL을 얻는다. `ALTER TABLE orders ...`는 정의 변경을 위해 더 강한 배타 성격의 MDL이 필요하다. 기존 공유 lock과 배타 lock이 양립하지 않으면 DDL은 기다린다.

MDL의 이름과 조합은 SQL 종류와 실행 단계에 따라 `SHARED_READ`, `SHARED_WRITE`, `SHARED_UPGRADABLE`, `EXCLUSIVE` 등으로 나타날 수 있다. 운영자가 모든 조합을 암기하기보다는 다음 세 열을 우선 해석하는 편이 실용적이다.

- `LOCK_TYPE`: 요청하거나 보유한 lock의 성격
- `LOCK_DURATION`: `STATEMENT`, `TRANSACTION`, `EXPLICIT` 중 수명 범위
- `LOCK_STATUS`: 현재 획득한 `GRANTED`인지 기다리는 `PENDING`인지

### 2.1 문장 수명과 트랜잭션 수명

MDL은 항상 SQL 문장이 끝날 때 해제되는 것이 아니다. 트랜잭션의 일관성과 객체 정의 안정성을 위해 **트랜잭션 종료까지** 유지되는 lock이 있다. 다음과 같은 애플리케이션 패턴이 위험하다.

```text
세션 A
SET autocommit = 0;
SELECT * FROM orders WHERE order_id = 1001;
-- 애플리케이션이 외부 API 응답을 기다림
-- COMMIT 또는 ROLLBACK이 아직 실행되지 않음

세션 B
ALTER TABLE orders ADD COLUMN source_code varchar(20);
-- 세션 A의 트랜잭션이 끝날 때까지 MDL 대기 가능
```

세션 A의 `SELECT`가 이미 끝났더라도 트랜잭션이 열려 있으면 세션 B가 기다릴 수 있다. “현재 실행 중인 긴 SQL이 없다”는 사실만으로 MDL blocker가 없다고 판단하면 안 되는 이유다.

## 3. 대기열이 만드는 연쇄 차단

MDL 장애가 어려운 이유는 최초 blocker 하나보다 **대기열 뒤에 쌓이는 후속 요청**의 영향이 더 크게 보이기 때문이다.

```mermaid
sequenceDiagram
    participant A as 세션 A: 긴 트랜잭션
    participant M as MDL 관리자
    participant B as 세션 B: ALTER TABLE
    participant C as 세션 C: 새 SELECT

    A->>M: SHARED_READ 획득
    M-->>A: GRANTED
    B->>M: EXCLUSIVE 요청
    M-->>B: PENDING
    C->>M: SHARED_READ 요청
    Note over M,C: 선행 DDL과 스케줄링 정책 때문에<br/>후속 요청도 대기할 수 있음
    A->>M: COMMIT
    M-->>B: EXCLUSIVE GRANTED
    B->>M: DDL 완료 후 해제
    M-->>C: SHARED_READ GRANTED
```

현재 공유 lock과 새 `SELECT`의 공유 요청만 비교하면 서로 양립할 수 있어 보인다. 그러나 이미 배타 DDL이 기다리는 상황에서 공유 요청을 계속 통과시키면 DDL이 영원히 굶을 수 있다. MySQL의 MDL 스케줄링은 일반적으로 쓰기 성격 요청을 우선해 기아를 방지하므로, **DDL 뒤에 도착한 읽기까지 기다리는 현상**이 발생할 수 있다. `max_write_lock_count`는 이 정책과 관련되지만, 운영 장애를 이 변수 조정으로 해결하는 것은 권장하지 않는다. 긴 트랜잭션과 무제한 DDL 대기라는 원인을 제거하는 것이 우선이다.

이 연쇄는 다음 순서로 서비스 장애를 확대한다.

1. 오래 열린 트랜잭션이 테이블 MDL을 보유한다.
2. 배포 작업의 `ALTER TABLE`이 배타 MDL을 기다린다.
3. 같은 테이블에 접근하는 신규 요청이 DDL 뒤에서 대기한다.
4. connection pool이 소진되고 애플리케이션 timeout이 증가한다.
5. 재시도가 새 connection과 대기를 더 만들어 장애를 증폭한다.

## 4. Performance Schema에서 MDL을 관측하는 방법

MySQL 8.0에서는 `performance_schema.metadata_locks`를 기본 관측점으로 사용한다. 먼저 대상 버전과 instrumentation 상태를 확인한다.

```sql
SELECT VERSION() AS mysql_version;

SHOW TABLES FROM performance_schema LIKE 'metadata_locks';

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

실행 결과(MySQL 8.0.x):

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

+---------------+
| mysql_version |
+---------------+
| 8.0.46        |
+---------------+
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.01 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)
```

`metadata_locks`가 존재하더라도 instrumentation이 비활성화되어 있으면 필요한 행이 보이지 않을 수 있다. 운영 서버에서 설정을 바꾸기 전에는 성능 영향과 사내 변경 절차를 검토해야 한다. 일반적인 MySQL 8.0 배포에서는 해당 instrument를 사용할 수 있지만, 관리형 서비스의 버전과 parameter 정책은 별도로 확인한다.

### 4.1 재현: 긴 읽기와 대기 중인 ALTER

다음 예제는 MySQL Event Scheduler를 두 번째 서버 세션처럼 사용한다. 첫 event가 테이블을 읽으면서 `SLEEP()`하고, 두 번째 event가 같은 테이블을 변경하려고 기다리는 동안 현재 세션이 MDL 상태를 조회한다. **학습용 임시 인스턴스에서 실행하는 예제**이며, 운영 서버에서 Event Scheduler를 임의로 활성화하지 않는다.

```sql
SET GLOBAL event_scheduler = ON;

DROP EVENT IF EXISTS mdl_note_long_read;
DROP EVENT IF EXISTS mdl_note_waiting_alter;
DROP TABLE IF EXISTS mdl_note_demo;

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

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

CREATE EVENT mdl_note_long_read
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 SECOND
DO
  SELECT SLEEP(8), payload
  FROM mdl_note_demo
  WHERE id = 1;

CREATE EVENT mdl_note_waiting_alter
ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 2 SECOND
DO
  ALTER TABLE mdl_note_demo
    ADD COLUMN created_at TIMESTAMP NULL;

SELECT SLEEP(4) AS wait_for_events;

SELECT ml.OBJECT_TYPE,
       CONCAT_WS('.', ml.OBJECT_SCHEMA, ml.OBJECT_NAME) AS object_name,
       ml.LOCK_TYPE,
       ml.LOCK_DURATION,
       ml.LOCK_STATUS,
       t.PROCESSLIST_ID,
       LEFT(t.PROCESSLIST_INFO, 100) 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
WHERE ml.OBJECT_SCHEMA = DATABASE()
  AND ml.OBJECT_NAME = 'mdl_note_demo'
ORDER BY FIELD(ml.LOCK_STATUS, 'PENDING', 'GRANTED'),
         t.PROCESSLIST_ID;

SELECT SLEEP(7) AS wait_for_completion;

SHOW COLUMNS FROM mdl_note_demo LIKE 'created_at';

DROP EVENT IF EXISTS mdl_note_long_read;
DROP EVENT IF EXISTS mdl_note_waiting_alter;
DROP TABLE mdl_note_demo;
```

실행 결과(MySQL 8.0.x):

다음은 검증 출력에서 준비·정리 문장을 줄이고 MDL 상태와 DDL 완료 여부를 중심으로 발췌한 결과다. `PROCESSLIST_ID`는 실행마다 달라질 수 있다.

```text
mysql> CREATE TABLE mdl_note_demo (...);
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO mdl_note_demo VALUES (1, 'metadata lock demo');
Query OK, 1 row affected (0.00 sec)

mysql> CREATE EVENT mdl_note_long_read ...;
Query OK, 0 rows affected (0.00 sec)

mysql> CREATE EVENT mdl_note_waiting_alter ...;
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT ml.OBJECT_TYPE, ..., ml.LOCK_TYPE,
    ->        ml.LOCK_DURATION, ml.LOCK_STATUS, ...
    -> FROM performance_schema.metadata_locks AS ml ...;

+-------------+-------------------------------+-------------------+---------------+-------------+
| OBJECT_TYPE | object_name                   | LOCK_TYPE         | LOCK_DURATION | LOCK_STATUS |
+-------------+-------------------------------+-------------------+---------------+-------------+
| TABLE       | mysql_tech_note.mdl_note_demo | EXCLUSIVE         | TRANSACTION   | PENDING     |
| TABLE       | mysql_tech_note.mdl_note_demo | SHARED_READ       | TRANSACTION   | GRANTED     |
| TABLE       | mysql_tech_note.mdl_note_demo | SHARED_UPGRADABLE | TRANSACTION   | GRANTED     |
+-------------+-------------------------------+-------------------+---------------+-------------+
3 rows in set (0.00 sec)

mysql> SHOW COLUMNS FROM mdl_note_demo LIKE 'created_at';

+------------+-----------+------+-----+---------+-------+
| Field      | Type      | Null | Key | Default | Extra |
+------------+-----------+------+-----+---------+-------+
| created_at | timestamp | YES  |     | NULL    |       |
+------------+-----------+------+-----+---------+-------+
1 row in set (0.01 sec)

mysql> DROP TABLE mdl_note_demo;
Query OK, 0 rows affected (0.01 sec)
```

조회 시점에는 보통 긴 읽기가 가진 `GRANTED` lock과 `ALTER TABLE`이 요청한 `PENDING` lock을 함께 볼 수 있다. `ALTER TABLE`은 실행 단계에서 복수의 MDL을 획득하거나 lock을 승격할 수 있으므로 행 수와 `LOCK_TYPE` 조합은 minor version과 관측 순간에 따라 달라질 수 있다. 고정된 행 수보다 다음 증거를 연결해서 읽어야 한다.

- 같은 `OBJECT_SCHEMA`, `OBJECT_NAME`에 `GRANTED`와 `PENDING`이 공존하는가
- `OWNER_THREAD_ID`를 `performance_schema.threads`와 연결했을 때 어느 connection인가
- blocker가 현재 SQL을 실행 중인가, 트랜잭션만 열린 채 idle 상태인가
- 대기 문장이 실제 DDL인지, DDL 뒤에서 기다리는 후속 DML인지

## 5. 실전 진단 절차

### 5.1 먼저 증상을 분류한다

`ALTER TABLE`이 보이지 않는다고 바로 종료해서는 안 된다. 대기 중인 DDL이 애플리케이션 timeout으로 이미 사라졌더라도 오래 열린 트랜잭션은 남아 있을 수 있다. 다음 순서로 범위를 좁힌다.

1. `SHOW FULL PROCESSLIST` 또는 `performance_schema.threads`에서 대기 세션과 실행 시간을 확인한다.
2. `performance_schema.metadata_locks`에서 대상 객체의 `PENDING`/`GRANTED` 관계를 확인한다.
3. `information_schema.INNODB_TRX`에서 열린 트랜잭션의 시작 시각, 상태, 변경 행 수를 확인한다.
4. `performance_schema.events_statements_current`와 history가 활성화되어 있다면 최근 문장을 보완한다.
5. 배포 시스템, migration 도구, application connection pool의 owner를 식별한다.

`PROCESSLIST_INFO`가 `NULL`이라고 해서 무해한 세션은 아니다. `Sleep` 상태인 connection도 commit되지 않은 트랜잭션과 MDL을 보유할 수 있다. 반대로 `INNODB_TRX`에 보이는 모든 오래된 행이 현재 MDL blocker인 것도 아니다. 반드시 `metadata_locks.OWNER_THREAD_ID`와 thread 정보를 연결해 대상 객체의 보유 관계를 확인한다.

### 5.2 `sys.schema_table_lock_waits` 활용

`sys` schema를 사용할 수 있는 환경이라면 `sys.schema_table_lock_waits`가 waiting session, blocking session, 대기 SQL을 읽기 쉬운 형태로 정리해 준다. 다만 운영 표준 쿼리를 만들 때는 view 정의와 권한, MySQL minor version을 확인하고, 최종 근거는 `performance_schema.metadata_locks`와 thread/transaction 정보로 교차 검증하는 편이 안전하다.

### 5.3 종료 대상은 blocker와 waiter 중 무엇인가

장애 대응에서 가장 위험한 행동은 process ID만 보고 무조건 `KILL`하는 것이다.

- **대기 중 DDL을 중단**하면 후속 application query가 빠르게 회복될 수 있다. 온라인 서비스 복구가 우선일 때 보통 첫 선택이다.
- **blocker 세션을 종료**하면 rollback이 발생할 수 있다. 변경 행이 많은 트랜잭션이면 rollback 자체가 오래 걸리고 부하가 커질 수 있다.
- blocker가 중요한 업무 트랜잭션이면 owner와 영향도를 확인한 뒤 정상 commit/rollback을 유도하는 편이 낫다.
- DDL client의 timeout만 끝났어도 서버에서 작업이 계속되는지 반드시 확인한다. client 연결 종료 방식과 도구 동작에 따라 판단이 달라질 수 있다.

즉, 복구 순서는 “가장 오래된 connection 종료”가 아니라 **서비스 연쇄 차단을 만든 waiter를 먼저 제거할지, 원래 blocker를 끝낼지 영향도를 비교하는 결정**이다.

## 6. Online DDL과 `ALGORITHM=INSTANT`에 대한 오해

`LOCK=NONE`은 DDL 전체 수명 동안 모든 lock이 없다는 뜻이 아니다. 많은 online DDL은 대략 다음 단계를 거친다.

1. 작업 준비를 위해 MDL을 얻는다.
2. 허용되는 경우 동시 DML과 함께 내부 작업을 수행한다.
3. 새 정의를 게시하고 dictionary 상태를 전환하는 마지막 단계에서 다시 강한 MDL이 필요할 수 있다.

`ALGORITHM=INSTANT`도 데이터 page를 전부 다시 쓰지 않아 실행 시간이 짧을 뿐, schema 정의를 원자적으로 바꾸기 위한 MDL은 필요하다. 따라서 아주 짧을 것으로 예상한 DDL도 기존 긴 트랜잭션 때문에 시작 지점에서 오래 기다릴 수 있다.

DDL 명령의 `LOCK` 절은 “기다리지 말라”는 timeout 설정도 아니다. DDL 대기 시간을 제한하려면 작업 세션에서 `lock_wait_timeout`을 명시적으로 검토한다. 이 변수는 InnoDB row lock의 `innodb_lock_wait_timeout`과 다른 범위를 다룬다. 배포 도구가 무제한에 가까운 기본값을 그대로 사용하면 대기 중 DDL이 서비스 query를 막는 시간이 길어질 수 있다.

온라인 schema 변경 도구도 MDL을 완전히 제거하지 않는다. shadow table 생성, trigger 또는 복제형 변경, 마지막 rename/swap 단계에서 짧고 강한 MDL 구간이 남는다. 도구 이름보다 다음 항목을 확인해야 한다.

- 최종 cutover가 획득할 MDL과 timeout
- 긴 트랜잭션 사전 점검 여부
- 실패 시 재시도와 cleanup 방식
- replica lag 또는 Aurora reader 영향에 따른 중단 기준
- DDL을 취소했을 때 원본·임시 객체의 상태

## 7. 흔한 실패 모드와 오해

### 7.1 `SHOW PROCESSLIST`에 긴 SELECT가 없으니 blocker가 없다는 판단

문장은 끝났지만 트랜잭션이 끝나지 않은 idle connection이 원인일 수 있다. application의 transaction boundary와 `autocommit` 설정을 함께 확인한다.

### 7.2 DDL이 pending인데 계속 기다리는 운영

“언젠가 끝날 것”이라고 기다리는 동안 신규 query가 DDL 뒤에 쌓일 수 있다. connection pool 포화 전에 DDL을 취소하는 자동 안전장치가 필요하다.

### 7.3 `innodb_lock_wait_timeout`만 조정

MDL 대기는 InnoDB record lock 대기와 같은 문제가 아니다. DDL용 `lock_wait_timeout`, migration 도구 timeout, application statement timeout을 별도로 설계한다.

### 7.4 `KILL QUERY`와 `KILL CONNECTION`의 차이를 무시

현재 문장만 중단해도 열린 트랜잭션과 보유 자원이 기대대로 정리되는지 확인해야 한다. 명시적 트랜잭션을 보유한 connection을 남겨 두면 장애 원인이 지속될 수 있다. 반대로 connection 종료는 rollback 비용을 유발한다.

### 7.5 관측 쿼리의 권한 부족을 “대기가 없음”으로 해석

관리 계정의 권한과 Performance Schema 설정에 따라 다른 세션의 SQL text가 가려지거나 instrumentation 정보가 부족할 수 있다. 빈 결과를 곧바로 정상 상태로 해석하지 말고 object 존재, instrument 활성화, 계정 권한을 먼저 확인한다.

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

Aurora MySQL도 MySQL 호환 SQL 계층에서 MDL을 사용하므로, writer instance의 긴 트랜잭션과 DDL 사이에서 발생하는 기본 원리는 같다. 분산 storage가 DDL의 MDL 필요성을 없애 주지는 않는다.

다만 topology를 함께 고려해야 한다.

- DDL은 writer에서 실행되므로 우선 writer의 `performance_schema.metadata_locks`, thread, transaction을 조사한다.
- reader에서 실행한 일반 읽기는 writer의 동일 process 내 MDL holder와 같다고 단정할 수 없다. 그러나 schema 변경 전파, reader 재시작 가능성, 장기 query 처리 방식은 Aurora MySQL 버전과 DDL 종류에 따라 달라질 수 있으므로 변경 창에서 별도로 검증한다.
- Performance Insights 또는 Database Insights의 wait 차원은 급증 시점을 찾는 데 유용하지만, wait 이름만으로 blocker를 확정하지 않는다. Performance Schema의 객체·thread 관계와 연결한다.
- failover는 connection을 끊어 lock을 사라지게 할 수 있지만 정상적인 MDL 해소 수단이 아니다. transaction 중단, 재연결 폭증, DDL 상태 확인 등 더 큰 위험을 만든다.
- cluster parameter group과 instance parameter group의 적용 범위, 재부팅 필요 여부를 확인한 뒤 instrumentation 또는 timeout 관련 설정을 변경한다.

Aurora에서 안전한 DDL 운영을 설계할 때는 writer의 MDL 사전 점검뿐 아니라 reader lag, long-running read, failover 가능성, schema 변경 후 reader 상태까지 하나의 change plan에 포함한다.

## 9. 배포 전·장애 대응 체크리스트

### 배포 전

- [ ] 대상 테이블을 참조하는 오래된 트랜잭션과 idle-in-transaction connection이 없는지 확인한다.
- [ ] DDL의 `ALGORITHM`과 `LOCK` 지원 여부를 대상 MySQL minor version에서 확인한다.
- [ ] 배포 세션의 `lock_wait_timeout`과 도구 자체 timeout을 유한한 값으로 정한다.
- [ ] MDL을 획득하지 못하면 빠르게 중단하고 재시도하도록 설계한다.
- [ ] 예상 rollback, 임시 공간, 복제 지연, Aurora reader 영향을 검토한다.
- [ ] cutover 단계와 실패 후 cleanup 절차를 rehearsal 환경에서 검증한다.

### 장애 대응

- [ ] 대기 중인 DDL과 최초 blocker를 구분한다.
- [ ] `metadata_locks`의 객체명, `LOCK_STATUS`, owner thread를 확인한다.
- [ ] idle connection의 열린 트랜잭션까지 확인한다.
- [ ] 후속 query와 connection pool 포화 여부를 확인한다.
- [ ] 서비스 회복을 위해 waiter DDL부터 취소할지 판단한다.
- [ ] blocker 종료 전 rollback 비용과 업무 영향도를 평가한다.
- [ ] 복구 후 migration 도구의 timeout·재시도 정책과 application transaction 경계를 수정한다.

## 10. 결론

Metadata Lock은 MySQL이 객체 정의를 일관되게 유지하기 위한 필수 안전장치다. 문제는 lock 자체보다 트랜잭션이 의도보다 오래 열리고, 배타 DDL이 제한 없이 기다리며, 그 뒤로 신규 요청이 쌓이는 운영 경로에서 발생한다.

MDL 장애를 정확히 해석하려면 `performance_schema.metadata_locks`의 `GRANTED`와 `PENDING`, owner thread, 열린 트랜잭션을 하나의 관계로 읽어야 한다. `INSTANT`와 online DDL도 MDL에서 자유롭지 않으므로, 안전한 schema 변경은 알고리즘 선택뿐 아니라 사전 blocker 점검, 짧은 timeout, 빠른 취소, 재시도 통제를 포함해야 한다.

다음 단계에서는 실제 운영에서 DDL 대기를 줄이는 online schema change 전략과 cutover 시점의 안전 조건을 더 구체적으로 다룰 수 있다.
