> ## 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의 JSON

> MySQL에서 JSON 데이터를 저장, 쿼리, 인덱싱하는 방법: JSON 열 타입, 경로 표현식, JSON 함수, 그리고 생성 열로 JSON 필드에 인덱스를 만드는 방법입니다.

> [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의 네이티브 `JSON` 열 타입은 삽입 시 JSON을 검증하고, 효율적인 접근을 위해 바이너리 형식으로 저장하며, 전체 문서를 파싱하지 않고 개별 필드를 읽고 수정하는 함수 집합을 제공합니다.

## JSON 저장하기

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

INSERT INTO products (name, attributes) VALUES (
    'Laptop',
    '{"brand": "Acme", "ram_gb": 16, "tags": ["portable", "pro"]}'
);
```

MySQL은 삽입 시 JSON을 검증합니다. 잘못된 형식의 JSON을 삽입하면 오류가 발생합니다.

## JSON 필드 읽기

`JSON_EXTRACT(column, path)` 또는 축약형 `->`를 사용하세요:

```sql theme={null}
-- These are equivalent
SELECT JSON_EXTRACT(attributes, '$.brand') FROM products;
SELECT attributes -> '$.brand' FROM products;
```

둘 다 JSON 값을 반환합니다 — 따옴표가 붙은 문자열 `"Acme"`입니다. 따옴표 없는 일반 문자열을 얻으려면 `->>`를 사용하세요(`JSON_UNQUOTE(JSON_EXTRACT(...))`의 축약형입니다):

```sql theme={null}
SELECT attributes ->> '$.brand' FROM products;  -- returns: Acme
```

경로 구문:

* `$.key` — 객체 필드에 접근
* `$.array[0]` — 인덱스로 배열 요소에 접근
* `$.array[*]` — 배열의 모든 요소
* `$.**` — 재귀 와일드카드(모든 깊이의 모든 값)

## JSON 필드로 필터링하기

```sql theme={null}
-- Find all 16GB RAM products
SELECT name FROM products
WHERE attributes ->> '$.ram_gb' = '16';

-- Check if a key exists
SELECT name FROM products
WHERE JSON_EXTRACT(attributes, '$.brand') IS NOT NULL;

-- Check if an array contains a value
SELECT name FROM products
WHERE JSON_CONTAINS(attributes -> '$.tags', '"portable"');
```

`->>`는 문자열을 반환하므로, `CAST(attributes ->> '$.ram_gb' AS UNSIGNED)`처럼 명시적으로 캐스팅하지 않는 한 (`16`이 아니라) `'16'`과 비교하는 것이 올바릅니다.

## JSON 수정하기

```sql theme={null}
-- Add or update a field
UPDATE products
SET attributes = JSON_SET(attributes, '$.color', 'silver')
WHERE id = 1;

-- Remove a field
UPDATE products
SET attributes = JSON_REMOVE(attributes, '$.tags')
WHERE id = 1;

-- Insert only if the key doesn't exist
UPDATE products
SET attributes = JSON_INSERT(attributes, '$.warranty_years', 2)
WHERE id = 1;
```

핵심 함수:

| 함수 | 동작 |
| :- | :- |
| `JSON_SET(doc, path, val)` | 값을 설정 — 삽입 또는 교체 |
| `JSON_INSERT(doc, path, val)` | 경로가 존재하지 않을 때만 삽입 |
| `JSON_REPLACE(doc, path, val)` | 경로가 존재할 때만 교체 |
| `JSON_REMOVE(doc, path)` | 경로의 값을 제거 |

## JSON 필드에 인덱스 만들기

JSON 열 자체에는 직접 인덱스를 만들 수 없습니다. 특정 JSON 필드에 인덱스를 만들려면, 생성 열을 만들고 그것에 인덱스를 만드세요:

```sql theme={null}
ALTER TABLE products
    ADD COLUMN brand VARCHAR(100) GENERATED ALWAYS AS (attributes ->> '$.brand') VIRTUAL;

CREATE INDEX idx_brand ON products (brand);
```

이제 `WHERE attributes ->> '$.brand' = 'Acme'`가 `idx_brand`를 사용할 수 있습니다. MySQL은 생성 열 표현식이 쿼리 조건과 일치한다는 것을 인식합니다.

이것은 [MySQL 데이터 타입 선택하기](https://villagesql.com/docs/ko/guides/choosing-data-types.md)에서 더 자세히 다룹니다 — 인덱스가 필요할 만큼 JSON 필드를 자주 쿼리할 때마다 권장되는 패턴입니다.

## JSON 대 정규화된 열

| | JSON 열 | 정규화된 열 |
| :- | :- | :- |
| 스키마 유연성 | 어떤 구조든 가능, 마이그레이션 불필요 | 고정 구조, 마이그레이션 필요 |
| 인덱싱 | 생성 열을 통해서만 | 어떤 열이든 직접 인덱스 |
| 쿼리 복잡성 | 경로 표현식, JSON 함수 | 표준 SQL |
| 조인 | JSON 키로는 불가능 | 완전한 JOIN 지원 |
| 저장 공간 | 더 큼(바이너리 JSON 오버헤드) | 더 작음 |
| 데이터 무결성 | 열 수준 제약 조건 없음 | `NOT NULL`, `DEFAULT`, 외래 키 |

구조가 정말로 가변적일 때(사용자가 설정할 수 있는 속성, 이벤트 메타데이터, 서드파티 API 페이로드) JSON을 사용하세요. 구조가 고정되어 있고 필드에 대해 필터링, 조인, 집계가 필요할 때 정규화된 열을 사용하세요.

## 자주 묻는 질문

#### 포함 쿼리를 위해 JSON 배열에 인덱스를 만들 수 있나요?

표준 인덱스로는 직접 만들 수 없습니다. `JSON_CONTAINS` 쿼리에서는 배열을 별도의 자식 테이블로 추출(정규화)하거나 배열을 평탄화하는 생성 열에 전문 인덱스를 사용하지 않는 한, 유일한 선택지는 전체 테이블 스캔입니다. 처리량이 높은 포함 쿼리에는 정규화가 더 나은 길입니다.

#### `->`와 `->>`의 차이는 무엇인가요?

`->`는 JSON 값을 반환합니다(문자열은 둘러싼 따옴표를 포함합니다). `->>`는 따옴표를 뗀 문자열을 반환합니다. 표시, 연결, 그리고 텍스트를 소비하는 모든 것에는 `->>`가 원하는 것입니다. 그래도 동등 비교는 `->`를 통해서도 동작합니다: MySQL이 다른 피연산자를 먼저 JSON으로 강제 변환하므로, `attributes -> '$.brand' = 'Acme'`는 일치합니다.

#### JSON을 저장하면 성능이 나빠지나요?

큰 JSON 문서에서 단일 필드를 읽으려면 바이너리 구조를 파싱해야 하는데, 이것은 텍스트를 파싱하는 것보다 빠르지만 네이티브 열을 읽는 것보다 느립니다. 자주 접근하는 필드에는 생성 열 + 인덱스 패턴이 읽기 성능을 네이티브 열과 같은 수준으로 끌어올립니다.

## 문제 해결

| 문제 | 해결 방법 |
| :- | :- |
| 삽입 시 `ERROR 3140: Invalid JSON` | JSON 구문을 고치기 — MySQL은 삽입 시 검증함 |
| `WHERE attributes -> '$.key' = 'value'`가 행을 반환하지 않음 | 따옴표는 원인이 아님 — 그 비교는 JSON을 인식하며 일치함. 경로 자체와, 값이 숫자가 아니라 JSON 문자열인지 확인 |
| JSON 필드 쿼리가 인덱스를 사용하지 않음 | 경로 표현식에 대한 생성 열을 추가하고 그것에 인덱스 만들기 |
| 중첩 배열에 대한 `JSON_CONTAINS`가 일치하지 않음 | 경로와 따옴표를 확인 — `JSON_CONTAINS` 안의 문자열 값은 따옴표가 붙은 JSON이어야 함: `'value'`가 아니라 `'"value"'` |

## 함께 보기

* [MySQL 데이터 타입 선택하기](https://villagesql.com/docs/ko/guides/choosing-data-types.md) — JSON 대 정규화된 열을 언제 사용할지
* [MySQL의 생성 열](https://villagesql.com/docs/ko/guides/generated-columns.md) — 가상 생성 열로 JSON 필드에 인덱스 만들기
* [SQL에서 HTTP 요청 보내기](https://villagesql.com/docs/ko/guides/http-requests-in-mysql.md) — HTTP API 응답에서 JSON 파싱하기


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