2026年9月16日
2026年9月16日
MySQLのJSON型の使い方 値の取り出し・更新・インデックスとWordPressでの注意点
MySQL の JSON 型は、JSON の文書をそのまま1つの列に保存し、SQL の中で特定のキーを取り出したり書き換えたりできるデータ型です。保存時に JSON として正しいかが検査され、内部では読み取りやすい形式で保持されます。この記事では、JSON 列の作り方、値の取り出し(-> と ->>)、部分的な更新、検索とインデックスの付け方を順に説明し、最後に WordPress で使うときの注意点(コアのテーブルは JSON 型ではないこと、update_post_meta() で JSON を保存するときのつまずき)を整理します。
JSON 型を使う前に確かめること
JSON 型は MySQL 5.7.8 で加わり、MySQL 8.0 以降で関数やインデックスの機能が増えました。この記事の例は MySQL 8.0 以降を前提にしています(2026年10月現在)。
MariaDB にも JSON という型名はありますが、中身は LONGTEXT の別名で、JSON として正しいかを検査する制約が付く形です。MySQL のような内部形式での保存ではなく、-> や ->> の演算子も使えない版があります。MariaDB では JSON_EXTRACT() などの関数で書くと両方で動きやすくなります。自分のサーバーがどちらかは SELECT VERSION(); で確かめてください。
また、JSON 型は「列の構造を決めずに何でも入れる場所」ではありません。検索や並べ替えに頻繁に使う値、他のテーブルと結び付ける値は、通常の列にしたほうが扱いやすく速くなります。JSON 型が向いているのは、商品ごとに項目が異なる属性、外部 API から受け取った応答の保存、設定値のまとまりなど、構造が揃わないデータです。
JSON 列を作ってデータを入れる
CREATE TABLE products (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(191) NOT NULL,
attrs JSON NOT NULL
);
INSERT INTO products (name, attrs) VALUES
('Tシャツ', '{"color": "navy", "size": ["S", "M", "L"], "price": 2500}'),
('マグカップ', JSON_OBJECT('color', 'white', 'capacity_ml', 350, 'tags', JSON_ARRAY('sale')));
JSON は文字列として直接書くか、JSON_OBJECT() や JSON_ARRAY() で組み立てて入れます。関数で組み立てると、引用符のエスケープを自分で書かずに済みます。JSON として壊れた文字列を入れようとすると Invalid JSON text のエラーになり、行は保存されません。この検査が、文字列型の列に JSON を入れる場合との大きな違いです。
値を取り出す:JSON_EXTRACT と -> / ->>
SELECT
name,
JSON_EXTRACT(attrs, '$.color') AS color_json, -- "navy"(引用符付き)
attrs->'$.color' AS color_json2, -- 上と同じ
attrs->>'$.color' AS color, -- navy(引用符なし)
attrs->>'$.size[0]' AS first_size -- S
FROM products;
$ は文書全体を表し、$.color はキー、$.size[0] は配列の先頭の要素を指します。-> は JSON_EXTRACT() の短縮形で、結果は JSON の値なので文字列なら引用符が付いたままです。->> は JSON_UNQUOTE(JSON_EXTRACT(...)) の短縮形で、引用符を外した文字列を返します。画面に表示したり、文字列と比較したりするときは ->> を使うと分かりやすくなります。
存在しないキーを指定するとエラーではなく NULL が返ります。キーの綴りを間違えても静かに NULL になるため、結果が空のときはまずパスの綴りを疑ってください。
条件で絞り込む
-- 色が navy の商品
SELECT id, name FROM products WHERE attrs->>'$.color' = 'navy';
-- 価格が 2000 以上の商品(数値として比較される)
SELECT id, name FROM products WHERE attrs->'$.price' >= 2000;
-- tags 配列に sale を含む商品
SELECT id, name FROM products WHERE JSON_CONTAINS(attrs->'$.tags', '"sale"');
SELECT id, name FROM products WHERE 'sale' MEMBER OF (attrs->'$.tags');
JSON_CONTAINS() の第2引数は JSON の値として渡すため、文字列は '"sale"' のように JSON の引用符を含めて書きます。ここは間違えやすい点です。MEMBER OF は MySQL 8.0.17 以降で使える書き方で、配列にその値が含まれるかを素直に表せます。
ただし、このままではどの条件もテーブルの全行を読みます。行数が多いテーブルで頻繁に使う条件には、次のインデックスが必要です。
インデックスを付ける
JSON 列そのものにはインデックスを付けられません。取り出した値に対してインデックスを作ります。
生成列(generated column)を使う方法
ALTER TABLE products
ADD COLUMN color VARCHAR(32)
GENERATED ALWAYS AS (JSON_UNQUOTE(JSON_EXTRACT(attrs, '$.color'))) VIRTUAL,
ADD INDEX idx_color (color);
SELECT id, name FROM products WHERE color = 'navy';
JSON から取り出した値を仮想の列として定義し、その列にインデックスを付けます。生成列は MySQL 5.7 以降と MariaDB の両方で使え、クエリも普通の列と同じように書けるため、最初に検討しやすい方法です。
複数値インデックス(multi-valued index)
ALTER TABLE products
ADD INDEX idx_tags ((CAST(attrs->'$.tags' AS CHAR(32) ARRAY)));
配列の各要素にインデックスを付ける方法で、MySQL 8.0.17 以降で使えます。MEMBER OF、JSON_CONTAINS()、JSON_OVERLAPS() の条件で使われます。
どちらの方法でも、追加後は EXPLAIN で key 列に作ったインデックスの名前が出ているかを確かめます。大きなテーブルへの ALTER TABLE は時間がかかり、版や操作によってはテーブルがロックされることがあるため、バックアップを取り、ステージングで所要時間を測ってから本番で実行してください。
一部だけ書き換える
-- price を変更(無ければ追加)
UPDATE products SET attrs = JSON_SET(attrs, '$.price', 2200) WHERE id = 1;
-- 無いときだけ追加 / 有るときだけ置き換え
UPDATE products SET attrs = JSON_INSERT(attrs, '$.stock', 10) WHERE id = 1;
UPDATE products SET attrs = JSON_REPLACE(attrs, '$.color', 'black') WHERE id = 1;
-- キーを削除
UPDATE products SET attrs = JSON_REMOVE(attrs, '$.stock') WHERE id = 1;
JSON_SET() は「有れば置き換え、無ければ追加」、JSON_INSERT() は「無いときだけ追加」、JSON_REPLACE() は「有るときだけ置き換え」です。アプリ側で JSON 全体を読み込んで書き戻すと、同時に別の処理が更新した内容を上書きしてしまうことがありますが、これらの関数で必要なキーだけを書き換えれば、その危険を減らせます。WHERE の付け忘れには、通常の UPDATE と同じく注意します。
JSON を行に展開する:JSON_TABLE
SELECT p.name, s.size
FROM products AS p,
JSON_TABLE(p.attrs, '$.size[*]' COLUMNS (size VARCHAR(8) PATH '$')) AS s;
配列の要素を1行ずつに展開し、普通のテーブルのように JOIN や GROUP BY で扱えるようにする関数です。MySQL 8.0 以降で使えます。サイズ別の在庫集計など、配列の中身を集計したいときに役立ちます。
WordPress で使うときの注意点
コアのテーブルは JSON 型ではない
wp_postmeta の meta_value や wp_options の option_value は LONGTEXT で、配列は PHP のシリアライズ形式で保存されます。ここを JSON 型に変更すると、WordPress や多くのプラグインが前提にしている形式と合わなくなり、更新時に問題が起きるおそれがあるため、コアのテーブルの列の型は変えないでください。JSON 型を使うなら、自作プラグインの独自テーブルで使います。独自テーブルはプラグインの有効化時に作成し、接頭辞は $wpdb->prefix から組み立てます。
update_post_meta() で JSON を保存するときのスラッシュ
カスタムフィールドに JSON 文字列を入れること自体は可能です(列は JSON 型ではなく文字列として扱われます)。ここでつまずきやすいのが、update_post_meta() が保存前に値のバックスラッシュを取り除く(wp_unslash() を通す)仕様です。JSON の中のエスケープ(\" や あ など)が消えて、壊れた JSON になることがあります。
$data = array(
'color' => 'navy',
'note' => '"引用符" を含む文字列',
);
// wp_slash() で包んでから渡すと、取り除かれる分を見越して保存できる
update_post_meta( $post_id, '_myplugin_attrs', wp_slash( wp_json_encode( $data ) ) );
// 読み出し: 第2引数 true で連想配列にする
$attrs = json_decode( (string) get_post_meta( $post_id, '_myplugin_attrs', true ), true );
if ( is_array( $attrs ) ) {
echo esc_html( $attrs['color'] ?? '' );
}
そもそも PHP の配列を JSON にせずそのまま update_post_meta() に渡せば、WordPress がシリアライズして保存してくれます。JSON 文字列で保存するのは、JavaScript や外部のシステムとそのままやり取りしたい場合に限るのが無難です。エンコードには json_encode() より文字コードの扱いに配慮した wp_json_encode() を使います。
$wpdb から JSON のパスを渡す
独自テーブルを $wpdb で検索するときは、パスや値も $wpdb->prepare() のプレースホルダーで渡します。JSON_UNQUOTE(JSON_EXTRACT(attrs, %s)) = %s のように書き、'$.color' と検索値を引数に入れれば、利用者の入力をクエリへ直接埋め込まずに済みます。
まとめ
- JSON 型は保存時に検査され、
->(JSON のまま)と->>(引用符を外す)で値を取り出せる。MariaDB の JSON は LONGTEXT の別名で、演算子が使えない版がある - 検索に使う値は、生成列か複数値インデックスでインデックスを付け、EXPLAIN で使われているかを確かめる
- 一部の更新は
JSON_SET()・JSON_INSERT()・JSON_REPLACE()・JSON_REMOVE()を使い分ける - 頻繁に検索・結合する値は通常の列にする。JSON 型は構造が揃わないデータに向く
- WordPress のコアテーブルの型は変えない。
update_post_meta()に JSON 文字列を渡すときはwp_slash()で包む