> ## 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 생성 열의 동작 방식: VIRTUAL과 STORED의 차이, 표현식과 인덱싱, 함수형 인덱스 패턴, 그리고 생성 열이 애플리케이션 계층의 계산을 대체하는 시점.

> [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 →

생성 열은 애플리케이션이 값을 저장하는 대신 표현식에서 값이 파생되는 열입니다. MySQL이 자동으로 계산합니다 — 스키마에 공식을 한 번 정의하면 MySQL이 일관성을 유지합니다.

## 구문

```sql theme={null}
column_name data_type [GENERATED ALWAYS] AS (expression) [VIRTUAL | STORED]
```

```sql theme={null}
CREATE TABLE orders (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    subtotal    DECIMAL(10,2) NOT NULL,
    tax_rate    DECIMAL(5,4) NOT NULL,
    tax_amount  DECIMAL(10,2) AS (subtotal * tax_rate) VIRTUAL,
    total       DECIMAL(10,2) AS (subtotal + subtotal * tax_rate) STORED
);
```

## VIRTUAL과 STORED 비교

| | VIRTUAL | STORED |
| :- | :- | :- |
| 저장 | 디스크 저장 없음 — 읽을 때 계산 | INSERT/UPDATE 시 디스크에 기록 |
| 계산 시점 | 모든 SELECT | 모든 INSERT/UPDATE |
| 디스크 공간 | 공간 절약 | 일반 열처럼 공간 사용 |
| 인덱스 가능 여부 | 예(인덱스가 계산된 값을 저장) | 예 |
| 외래 키 대상 가능 여부 | 아니요 | 아니요 |
| 기본값 | 예 — 생략하면 VIRTUAL이 기본 | 명시적 선택 |

계산 비용이 크고 열을 쓰는 것보다 훨씬 자주 읽을 때 `STORED`를 사용하세요. 저장 공간이 걱정되거나 쓰기가 자주 일어날 때는 `VIRTUAL`(기본값)을 사용하세요.

## 표현식

생성 열 표현식은 같은 행의 다른 열을 참조할 수 있습니다. 다른 생성 열을 참조하거나 비결정적 함수를 호출할 수는 없습니다.

```sql theme={null}
CREATE TABLE users (
    id         INT AUTO_INCREMENT PRIMARY KEY,
    first_name VARCHAR(50),
    last_name  VARCHAR(50),
    full_name  VARCHAR(101) AS (CONCAT(first_name, ' ', last_name)) VIRTUAL,
    email      VARCHAR(100),
    email_lower VARCHAR(100) AS (LOWER(email)) STORED
);
```

생성 열에 허용되는 함수는 결정적이어야 합니다 — 같은 입력은 항상 같은 출력을 생성해야 합니다. 허용되지 않는 것: `NOW()`, `RAND()`, `UUID()`, 서브쿼리, 저장 함수.

JSON에서 필드를 추출하는 것은 흔한 사용 사례입니다:

```sql theme={null}
CREATE TABLE events (
    id      INT AUTO_INCREMENT PRIMARY KEY,
    payload JSON,
    user_id INT AS (payload->>'$.user_id') STORED,
    action  VARCHAR(50) AS (payload->>'$.action') VIRTUAL
);
```

## 생성 열 인덱싱

생성 열에 인덱스를 추가할 수 있습니다. `VIRTUAL` 열의 경우 MySQL은 계산된 값을 인덱스에 구체화합니다.

```sql theme={null}
CREATE TABLE events (
    id         INT AUTO_INCREMENT PRIMARY KEY,
    occurred_at DATETIME NOT NULL,
    ym          VARCHAR(7) AS (DATE_FORMAT(occurred_at, '%Y-%m')) VIRTUAL
);

-- Index the computed ym. (Note: YEAR_MONTH is a reserved word — the
-- INTERVAL unit in INTERVAL '1-2' YEAR_MONTH — so an unquoted column
-- can't be named year_month.)
ALTER TABLE events ADD INDEX idx_ym (ym);

-- This query now uses the index
SELECT * FROM events WHERE ym = '2024-06';
```

이것은 **함수형 인덱스** — 원시 열 값이 아니라 열에 대한 함수에 거는 인덱스 — 를 만드는 방법이기도 합니다. MySQL 8.0.13 이상은 함수형 인덱스를 직접 지원하며(`INDEX (LOWER(email))`), 내부적으로 숨겨진 생성 열을 만듭니다:

```sql theme={null}
-- Functional index syntax (MySQL 8.0.13+)
CREATE TABLE users (
    id    INT PRIMARY KEY,
    email VARCHAR(100),
    INDEX idx_email_lower ((LOWER(email)))  -- note the double parentheses
);

-- Equivalent to:
CREATE TABLE users (
    id          INT PRIMARY KEY,
    email       VARCHAR(100),
    email_lower VARCHAR(100) AS (LOWER(email)) VIRTUAL,
    INDEX idx_email_lower (email_lower)
);
```

## 삽입과 갱신

`INSERT`나 `UPDATE`에서 생성 열에 값을 할당할 수 없습니다. 열 값은 항상 표현식에서 파생됩니다. `DEFAULT`를 명시적으로 쓸 수는 있지만, 그것이 전부입니다:

```sql theme={null}
-- Correct: omit the generated column entirely
INSERT INTO orders (subtotal, tax_rate) VALUES (100.00, 0.08);

-- Correct: DEFAULT is allowed
INSERT INTO orders (subtotal, tax_rate, total) VALUES (100.00, 0.08, DEFAULT);

-- Error: cannot assign a value to a generated column
INSERT INTO orders (subtotal, tax_rate, total) VALUES (100.00, 0.08, 108.00);
```

## ALTER TABLE

기존 테이블에 생성 열 추가하기:

```sql theme={null}
ALTER TABLE orders
ADD COLUMN discount_pct DECIMAL(5,2)
    AS (CASE WHEN subtotal > 1000 THEN 0.10 ELSE 0.00 END) VIRTUAL;
```

생성 열의 표현식 변경하기. `VIRTUAL`/`STORED` 저장 클래스는 그대로 유지되어야 합니다 — 전환하면 `ERROR 3106: 'Changing the STORED status' is not supported for generated columns`가 발생하므로, 저장 방식을 바꾸려면 열을 삭제하고 다시 추가해야 합니다:

```sql theme={null}
ALTER TABLE orders
MODIFY COLUMN tax_amount DECIMAL(10,2)
    AS (ROUND(subtotal * tax_rate, 2)) VIRTUAL;
```

## 자주 묻는 질문

#### WHERE 절에서 생성 열을 사용할 수 있나요?

예. 인덱스가 없는 `VIRTUAL` 열의 경우 MySQL은 스캔 중에 표현식을 계산합니다. `STORED` 열이나 인덱스가 있는 `VIRTUAL` 열의 경우 값은 이미 구체화되어 있습니다. 인덱스가 사용되는지 확인하려면 `EXPLAIN`을 사용하세요.

#### 원본 열이 바뀌면 생성 열이 자동으로 갱신되나요?

예. MySQL은 `VIRTUAL` 생성 열을 읽을 때마다 다시 계산합니다. `STORED` 생성 열은 표현식의 열에 영향을 주는 모든 `INSERT`나 `UPDATE`에서 다시 계산되어 기록됩니다.

#### 생성 열을 파티션 키로 사용할 수 있나요?

예. `STORED`와 `VIRTUAL` 생성 열 모두 가능합니다.

## 문제 해결

| 문제 | 해결 방법 |
| :- | :- |
| `ERROR 3105: The value specified for generated column is not allowed` | INSERT/UPDATE에서 생성 열에 명시적 값을 할당했습니다 — 생략하거나 DEFAULT를 사용하세요 |
| `ERROR 3763: Expression of generated column '<col>' contains a disallowed function: <name>` | 표현식이 `NOW()`, `RAND()`, `UUID()` 또는 서브쿼리를 사용합니다 — 결정적 대안으로 바꾸세요 |
| 생성 열이 인덱스를 사용하지 않음 | `EXPLAIN`을 확인하세요 — WHERE 절 표현식이 생성 열의 표현식과 정확히 일치해야 합니다. `WHERE LOWER(email)`은 `LOWER(email)` 생성 열 인덱스를 사용하지만 `WHERE LOWER(TRIM(email))`은 사용하지 않습니다 |
| `STORED` 열이 쓰기를 느리게 함 | 모든 INSERT/UPDATE가 표현식을 다시 계산하고 저장합니다 — 읽기 성능이 우선이 아니라면 `VIRTUAL`로 전환하세요 |
| 생성 열을 추가하는 `ALTER TABLE`이 느림 | MySQL은 `STORED` 생성 열에 대해 테이블을 재구축합니다. `VIRTUAL` 열 추가는 훨씬 빠릅니다(많은 경우 메타데이터만 변경) |

## 참고

* [MySQL의 JSON](https://villagesql.com/docs/ko/guides/json-in-mysql.md) — 주요 사용 사례: 생성 열로 JSON 필드 인덱싱
* [MySQL 인덱스의 동작 방식](https://villagesql.com/docs/ko/guides/mysql-indexes.md) — 생성 열의 인덱스는 일반 인덱스처럼 동작
* [MySQL 데이터 타입 선택](https://villagesql.com/docs/ko/guides/choosing-data-types.md) — 파생 값을 저장하는 것보다 생성 열이 나은 경우
* [MySQL의 소프트 삭제](https://villagesql.com/docs/ko/guides/soft-deletes.md) — 소프트 삭제된 행에 대한 고유 제약 해결책


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