【MySQL】JSON型の使い方:半構造化データをMySQLで扱う

MySQL

こんにちは、かつコーチです。

前回はENUM・SET・TEXT系といったMySQL固有のデータ型を解説しました。

今回はMySQL 5.7から導入されたJSON型を使って、半構造化データを扱う方法を解説します。

JSON型とは?

半構造化データを直接保存できる型

JSON型とは、JSON形式のデータをそのまま1つの列に保存できるデータ型です。

半構造化データとは、行と列できっちり定義しきれない、項目数や構造が可変なデータのことを指します。

商品ごとに異なる仕様項目(サイズ、色、素材など)を持つECサイトの商品情報や、外部APIから受け取った可変な形式のレスポンスなどが典型例です。

CREATE TABLE products (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(255) NOT NULL,
  attributes JSON
);
INSERT INTO products (name, attributes) VALUES
  ('Tシャツ', '{"color": "白", "size": ["S", "M", "L"]}'),
  ('マグカップ', '{"color": "青", "material": "陶器"}');

このように、商品ごとに項目数が違うデータでも、テーブル構造を変えずに1つの列で柔軟に保存できます。

TEXT型で保存するのとの違い

JSON型が登場する前は、JSON文字列をTEXT型でそのまま保存するという方法が使われていました。

JSON型はTEXT型と違い、保存時に自動でフォーマットの妥当性がチェックされ、不正なJSON文字列は保存時にエラーになります。

-- 不正なJSON(閉じ括弧がない)を入れようとするとエラーになる
INSERT INTO products (name, attributes) VALUES ('ペン', '{"color": "黒"');
ERROR 3140 (22032): Invalid JSON text

TEXT型ではこうしたバリデーションが効かないため、JSON型を使う方がデータの整合性を保ちやすくなります。

基本の使い方

手順1:JSON列から値を取り出す

保存したJSONの中の特定の値を取り出すには、->演算子またはJSON_EXTRACT関数を使います。

SELECT name, attributes->'$.color' AS color
FROM products;

$.color$はJSONのルートを表し、.colorでその中のcolorキーを指定しています。

手順2:JSONの値を条件に検索する

WHERE句でJSON内の値を条件にすることもできます。

SELECT name FROM products
WHERE attributes->>'$.color' = '白';

->>演算子(アンクォート抽出演算子)を使うと、取り出した値からダブルクォートを取り除いた状態で比較できます。

->だけだとJSON文字列としての比較("白"のようにクォート付き)になるため、通常の文字列比較を行いたい場合は->>を使うのが基本です。

手順3:JSONの値を更新する

列全体を上書きせず、JSON内の一部の値だけを更新したい場合はJSON_SET関数を使います。

UPDATE products
SET attributes = JSON_SET(attributes, '$.color', '赤')
WHERE id = 1;

このように、他のキーはそのままにcolorの値だけを書き換えられます。

つまずきやすいポイント:JSON列にインデックスが直接効かない

検索が遅くなる落とし穴

❌ Before:JSON列を条件にした検索にそのままインデックスを期待する

SELECT * FROM products WHERE attributes->>'$.color' = '白';

JSON型の列そのものには、通常のインデックスを直接張ることができません。

商品データが数万件を超えたあたりから、このクエリが徐々に遅くなっていくのを実際の案件で経験しました。

原因をEXPLAINで確認したところ、インデックスが使われずフルテーブルスキャンになっていることが分かりました。

✅ After:生成列(Generated Column)にインデックスを張る

MySQLでは、JSON内の特定の値を取り出した生成列を定義し、そこにインデックスを張ることで、JSON内の値を使った検索を高速化できます。

ALTER TABLE products
  ADD COLUMN color VARCHAR(50)
  GENERATED ALWAYS AS (attributes->>'$.color') VIRTUAL,
  ADD INDEX idx_products_color (color);
SELECT * FROM products WHERE color = '白';

生成列colorは、JSON内のcolorの値を自動的に反映する仮想的な列で、通常の列と同じようにインデックスを効かせられます。

このチューニングを行った結果、フルスキャンだった検索がインデックススキャンに変わり、検索速度が大幅に改善しました。

JSON型は柔軟な反面、頻繁に検索条件として使うキーがあらかじめ分かっているなら、生成列で列として切り出しておくのが実務上の定石です。

応用:JSON型を使うべきか、正規化すべきかの判断軸

JSON型が向いているケース

  • 項目数や構造が商品・レコードごとに大きく異なる
  • 検索条件としてほとんど使わない、表示専用の付加情報である
  • 外部APIのレスポンスをそのまま保存しておきたい

正規化されたテーブルが向いているケース

  • 特定の項目を条件に頻繁に検索・集計する
  • 項目ごとに型や制約(NOT NULLなど)をきちんと効かせたい
  • 複数のテーブルと結合して扱う必要がある

JSON型は便利ですが、何でもJSON列に詰め込んでしまうと、リレーショナルデータベースの強みである制約やインデックス、JOINの恩恵を受けにくくなります。

「構造が決まっていて、検索にもよく使うデータ」は通常の列に、「構造が可変で、主に表示用の付加情報」はJSON型に、という基準で使い分けるのが実務でのバランスの取り方です。

まとめ

この記事のポイント

  • JSON型は半構造化データをそのまま保存でき、TEXT型と違い保存時にフォーマットが検証される
  • ->->>演算子で値の取得・比較ができ、JSON_SETで部分更新ができる
  • JSON列そのものには直接インデックスが張れないため、頻繁に検索する項目は生成列で切り出す
  • 何でもJSON型にせず、検索頻度や構造の安定性を基準に正規化との使い分けを判断する

次に読むべき記事

データ型編はここまでです。

次のインデックス・パフォーマンス編では、今回登場したインデックスの種類そのものについてさらに詳しく掘り下げていきます。

→ 次の記事:MySQLのインデックスの種類(B-Tree・フルテキスト・空間インデックス)

タグ: MySQL, 中級者向け, データ型

タイトルとURLをコピーしました