> ## Documentation Index
> Fetch the complete documentation index at: https://villagesql.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# MySQL의 외래 키

> MySQL 외래 키의 동작 방식, 정의하는 방법, CASCADE와 RESTRICT 옵션, 사용하기 전에 이해해야 할 성능 트레이드오프입니다.

> [card] [VillageSQL is a drop-in replacement for MySQL with extensions.](https://villagesql.com/docs/en/8.4/stable/install-server.md)
>
> All examples on this page work on VillageSQL. Install Now →

외래 키는 두 테이블 사이의 관계를 강제하는 제약 조건입니다. 한 테이블 열의 값이 다른 테이블에 존재함을 보장하고, 참조된 행이 삭제되거나 갱신될 때 무슨 일이 일어나는지 정의합니다.

## 외래 키 정의하기

```sql theme={null}
CREATE TABLE customers (
    id   INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL
);

CREATE TABLE orders (
    id          INT PRIMARY KEY AUTO_INCREMENT,
    customer_id INT NOT NULL,
    amount      DECIMAL(10,2) NOT NULL,
    CONSTRAINT fk_orders_customer
        FOREIGN KEY (customer_id) REFERENCES customers (id)
        ON DELETE RESTRICT
        ON UPDATE CASCADE
);
```

제약 조건 이름(`fk_orders_customer`)은 선택 사항이지만 포함할 가치가 있습니다 — 오류 메시지에 나타나고 `ALTER TABLE` 작업을 쉽게 만듭니다.

외래 키에는 InnoDB가 필요합니다 — MyISAM은 조용히 무시합니다.

## 참조 동작

`ON DELETE`와 `ON UPDATE` 절은 참조된 부모 행이 삭제되거나 갱신될 때 자식 행에 무슨 일이 일어나는지 정의합니다.

| 동작 | DELETE 시 | UPDATE 시 |
| :- | :- | :- |
| `RESTRICT` | 자식 행이 있으면 삭제를 차단 | 자식 행이 있으면 갱신을 차단 |
| `NO ACTION` | `RESTRICT`와 동일(기본값) | `RESTRICT`와 동일 |
| `CASCADE` | 자식 행을 자동으로 삭제 | 자식 FK 값을 일치하도록 갱신 |
| `SET NULL` | 자식 FK 열을 NULL로 설정 | 자식 FK 열을 NULL로 설정 |

`SET DEFAULT`는 SQL 표준의 일부입니다. InnoDB는 불평 없이 구문을 받아들이지만 구현하지는 않습니다 — 런타임에는 `RESTRICT`와 정확히 같게 동작합니다(ERROR 1451). 사용하지 마세요: 이름이 암시하는 것과 정반대로 조용히 동작합니다.

\*\*DELETE 시 `CASCADE`\*\*는 편리하지만 위험합니다. 고객을 삭제하면 그 고객의 모든 주문이 자동으로 삭제됩니다 — 오류도 경고도 없습니다. 종속 행이 부모 없이는 정말로 의미가 없을 때(예: 주문 없는 주문 품목)만 사용하세요.

\*\*`RESTRICT`\*\*가 더 안전한 기본값입니다 — 애플리케이션이 삭제를 명시적으로 처리하도록 강제하여 우발적인 데이터 손실을 막습니다.

## 필수 인덱스

MySQL은 외래 키 열에 인덱스를 요구합니다. 인덱스가 없으면 InnoDB가 자동으로 만듭니다.

```sql theme={null}
-- InnoDB creates this index automatically for the FK above
-- But you can create it explicitly for control over the name
CREATE INDEX idx_orders_customer ON orders (customer_id);
```

이 인덱스가 외래 키 검사를 빠르게 만드는 것입니다 — 없으면 모든 INSERT나 DELETE에 전체 테이블 스캔이 필요합니다. FK 열에 대한 JOIN 쿼리도 빠르게 하므로, 명시적인 FK 제약 조건을 사용하지 않더라도 일대다 관계에서는 외래 키 열에 항상 인덱스를 만들어야 합니다.

## 외래 키 검사 확인 및 비활성화

대량 데이터 로드 중에는 외래 키 검사가 모든 삽입을 느리게 합니다. 일시적으로 비활성화하세요:

```sql theme={null}
SET FOREIGN_KEY_CHECKS = 0;

-- Bulk insert or LOAD DATA here

SET FOREIGN_KEY_CHECKS = 1;
```

**중요**: 검사를 비활성화하면 고아 행을 삽입할 수 있습니다. 검사를 다시 활성화해도 기존 데이터를 소급하여 검증하지 않습니다. 검사를 끈 채로 데이터를 로드했다면, 검사를 다시 켜기 전에 참조 무결성을 수동으로 검증하세요.

## 기존 외래 키 보기

```sql theme={null}
-- List all foreign keys in a database
SELECT
    TABLE_NAME,
    CONSTRAINT_NAME,
    COLUMN_NAME,
    REFERENCED_TABLE_NAME,
    REFERENCED_COLUMN_NAME
FROM information_schema.KEY_COLUMN_USAGE
WHERE REFERENCED_TABLE_SCHEMA = DATABASE()
ORDER BY TABLE_NAME;
```

## 자주 묻는 질문

#### 외래 키가 성능을 해치나요?

자식 테이블에 대한 모든 INSERT는 부모 테이블에 대한 검사(인덱스 조회)를 촉발합니다. 부모 테이블에서의 모든 DELETE는 자식 행에 대한 검사를 촉발합니다. 처리량이 높은 테이블에서는 이 검사들이 쌓입니다. 쓰기 처리량을 우선하는 팀은 대신 애플리케이션 계층에서 참조 무결성을 강제하는 경우가 많습니다. 어느 선택도 보편적으로 옳지는 않습니다 — 트레이드오프는 데이터베이스 수준의 안전성과 쓰기 성능 사이에 있습니다.

#### 기존 테이블에 외래 키를 추가할 수 있나요?

예, `ALTER TABLE`로 가능합니다:

```sql theme={null}
ALTER TABLE orders
    ADD CONSTRAINT fk_orders_customer
    FOREIGN KEY (customer_id) REFERENCES customers (id)
    ON DELETE RESTRICT;
```

`orders`의 기존 행에 `customers`에 존재하지 않는 `customer_id` 값이 있으면 `ALTER TABLE`이 실패합니다. 먼저 데이터를 고치세요.

#### MySQL은 왜 외래 키에 대해 인덱스를 자동으로 만드나요?

InnoDB는 부모 행이 삭제되거나 갱신될 때 자식 행을 빠르게 검사해야 합니다. FK 열에 인덱스가 없으면 그 검사에 자식 테이블의 전체 테이블 스캔이 필요합니다. 자동 생성된 인덱스가 그것을 막습니다. 먼저 (의미 있는 이름으로) 자체 인덱스를 만들면 InnoDB는 두 번째 인덱스를 만드는 대신 그것을 사용합니다.

## 문제 해결

| 문제 | 해결 방법 |
| :- | :- |
| `ERROR 1452: Cannot add or update a child row` | FK 값이 부모 테이블에 존재하지 않음 — 부모 행을 먼저 삽입 |
| `ERROR 1451: Cannot delete or update a parent row` | 자식 행이 존재하고 동작이 `RESTRICT`임 — 자식을 먼저 삭제하거나 `CASCADE`를 사용 |
| FK 추가 시 `ALTER TABLE` 실패 | 기존 데이터에 고아 행이 있음 — LEFT JOIN 검사로 찾아서 고치기 |
| FK 열에 대한 INSERT가 느림 | FK 열에 인덱스가 없음 — InnoDB가 자동 생성해야 하지만 `SHOW INDEX`로 확인 |
| `SET FOREIGN_KEY_CHECKS = 0`이 유지되지 않음 | 세션 변수임 — 대량 로드 전에 같은 세션에서 다시 설정 |

## 함께 보기

* [MySQL의 정규화](https://villagesql.com/docs/ko/guides/normalization.md) — 외래 키가 강제하는 스키마 설계
* [MySQL의 트랜잭션](https://villagesql.com/docs/ko/guides/transactions.md) — 외래 키 검사는 트랜잭션 안에서 일어납니다
* [MySQL JOIN 성능과 흔한 실수](https://villagesql.com/docs/ko/guides/joins.md) — 외래 키가 정의하는 관계에 인덱스 만들기
* [MySQL의 소프트 삭제](https://villagesql.com/docs/ko/guides/soft-deletes.md) — 캐스케이드가 소프트 삭제를 무시하는 이유
* [MySQL INNER JOIN](https://villagesql.com/docs/ko/tutorial/inner-join.md) — 이 관계들을 가로질러 쿼리하는 튜토리얼 레슨
* [MySQL 조인의 동작 방식](https://villagesql.com/docs/ko/tutorial/joins.md) — 애초에 데이터가 여러 테이블로 나뉘는 이유


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.