JSON型は、配列やオブジェクト形式のデータを1つのカラムにそのまま保存できるという柔軟さがあり、MySQLにおいても8.
ただし、こうした柔軟な構造を活かしつつも、実際にクエリで中身を参照したり、要素ごとに展開したりしようとすると、一筋縄ではいかない場面もあります。
たとえば、1レコードに複数の商品をJSON配列として保持している場合、それらを個別の行として扱いたくなることがあるでしょう。そんなときに活用できるのが、MySQL 8.
サンプルデータの準備
まずは、検証用にJSON配列を格納するテーブルを用意します。
CREATE TABLE orders ( id INT PRIMARY KEY, customer_name VARCHAR(100), items JSON ); INSERT INTO orders VALUES (1, 'Alice', JSON_ARRAY( JSON_OBJECT('product', 'Pen', 'qty', 2), JSON_OBJECT('product', 'Notebook', 'qty', 3) )), (2, 'Bob', JSON_ARRAY( JSON_OBJECT('product', 'Pencil', 'qty', 5) ));
今回はこのように、ordersテーブルのitemsカラムに複数の商品データをJSON配列として保存してみました。
JSON_EXTRACT()を使った基本的な抽出
まずは第197回 MySQLでJSONを活用してみる
mysql> SELECT JSON_EXTRACT(items, '$[0].product') FROM orders WHERE id = 1; +-------------------------------------+ | JSON_EXTRACT(items, '$[0].product') | +-------------------------------------+ | "Pen" | +-------------------------------------+ 1 row in set (0.00 sec) SELECT id, JSON_EXTRACT(items, '$[0].product') AS product FROM orders UNION ALL SELECT id, JSON_EXTRACT(items, '$[1].product') AS product FROM orders; +----+------------+ | id | product | +----+------------+ | 1 | "Pen" | | 2 | "Pencil" | | 1 | "Notebook" | | 2 | NULL | +----+------------+ 4 rows in set (0.00 sec)
特定の商品だけを取得したい場合、従来のJSON_
JSON_TABLE()を使って展開
JSON_
mysql> SELECT -> o.id, -> o.customer_name, -> jt.product, -> jt.qty -> FROM orders o, -> JSON_TABLE( -> o.items, -> '$[*]' COLUMNS ( -> product VARCHAR(100) PATH '$.product', -> qty INT PATH '$.qty' -> ) -> ) AS jt; +----+---------------+----------+------+ | id | customer_name | product | qty | +----+---------------+----------+------+ | 1 | Alice | Pen | 2 | | 1 | Alice | Notebook | 3 | | 2 | Bob | Pencil | 5 | +----+---------------+----------+------+ 3 rows in set (0.00 sec)
1レコードJSON_
という構文で、JSON配列やオブジェクトの中身を列として取り出すことができます。JSON_
JSON_TABLE(json,'$path' COLUMNS() {on empty} {on error})
$[*]
は配列のすべての要素を対象とするパス- COLUMNS内で各項目をSQLのカラム型にマッピング
- PATHにJSON内のキーへのパスを指定
オプションとして{on empty}
や{on error}
にERROR ON ERROR
やNULL ON ERROR
などのエラーハンドリング指定も可能です。
JSON_TABLEのエラー制御と安全な展開
JSONの中にキーが欠損している、またはNULL値が含まれている場合に備えて、エラー制御のオプションを使うと安全に展開できます。まずは検証用に、キーが欠損したデータを追加してみます。
INSERT INTO orders VALUES (3, 'Carol', JSON_ARRAY( JSON_OBJECT('product', 'Eraser'), JSON_OBJECT('qty', 10) ));
このデータには、1つ目の要素にqtyがなく、2つ目の要素にproductがありません。先ほどのJSON_
mysql> SELECT -> o.id, -> o.customer_name, -> jt.product, -> jt.qty -> FROM orders o, -> JSON_TABLE( -> o.items, -> '$[*]' COLUMNS ( -> product VARCHAR(100) PATH '$.product', -> qty INT PATH '$.qty' -> ) -> ) AS jt; +----+---------------+----------+------+ | id | customer_name | product | qty | +----+---------------+----------+------+ | 1 | Alice | Pen | 2 | | 1 | Alice | Notebook | 3 | | 2 | Bob | Pencil | 5 | | 3 | Carol | Eraser | NULL | | 3 | Carol | NULL | 10 | +----+---------------+----------+------+ 5 rows in set (0.00 sec)
そこで、以下のようにDEFAULTやNULL ON EMPTYを指定することで、安全に展開できます。
SELECT o.id, o.customer_name, jt.product, jt.qty FROM orders o, JSON_TABLE( o.items, '$[*]' COLUMNS ( product VARCHAR(100) PATH '$.product' DEFAULT '"不明"' ON EMPTY DEFAULT '"不明"' ON ERROR, qty INT PATH '$.qty' DEFAULT '1' ON EMPTY DEFAULT '0' ON ERROR ) ) AS jt; +----+---------------+----------+------+ | id | customer_name | product | qty | +----+---------------+----------+------+ | 1 | Alice | Pen | 2 | | 1 | Alice | Notebook | 3 | | 2 | Bob | Pencil | 5 | | 3 | Carol | Eraser | 1 | | 3 | Carol | 不明 | 10 | +----+---------------+----------+------+
DEFAULT '"不明"' ON ERROR
は、productの取得に失敗した場合に代替値を返す
実際に
ネストしたJSON構造を展開する
続いて、検証用のデータとして、次のようなネストされた構造のレコードをordersテーブルに追加してみます。
INSERT INTO orders VALUES (4, 'Dave', JSON_OBJECT( 'customer', JSON_OBJECT('name', 'Dave', 'email', 'dave@example.com'), 'items', JSON_ARRAY( JSON_OBJECT('product', 'Marker', 'qty', 4), JSON_OBJECT('product', 'Stapler', 'qty', 1) ) ));
このようにitemsがオブジェクト内にネストされた構造を持つ場合でも、JSON_
mysql> SELECT -> o.id, -> o.customer_name, -> jt.product, -> jt.qty -> FROM orders o, -> JSON_TABLE( -> JSON_EXTRACT(o.items, '$.items'), -> '$[*]' COLUMNS ( -> product VARCHAR(100) PATH '$.product', -> qty INT PATH '$.qty' -> ) -> ) AS jt -> WHERE o.id = 4; +----+---------------+---------+------+ | id | customer_name | product | qty | +----+---------------+---------+------+ | 4 | Dave | Marker | 4 | | 4 | Dave | Stapler | 1 | +----+---------------+---------+------+
続いては以下のようなデータを追加してみましょう。
INSERT INTO orders VALUES (5, 'Ellen', JSON_OBJECT( 'items', JSON_ARRAY( JSON_OBJECT('product', 'Pen', 'tags', JSON_ARRAY('stationery', 'writing')), JSON_OBJECT('product', 'Stapler', 'tags', JSON_ARRAY('office', 'tools')) ) ));
読みやすくするためにjqコマンドを実行すると、以下のようなJSONです。
{
"items": [
{
"tags": [
"stationery",
"writing"
],
"product": "Pen"
},
{
"tags": [
"office",
"tools"
],
"product": "Stapler"
}
]
}
各商品のtagsが配列として入れ子になっている構造です。このような場合はNESTED PATHを利用します。
SELECT o.id, o.customer_name, jt.product, jt.tag FROM orders o, JSON_TABLE( JSON_EXTRACT(o.items, '$.items'), '$[*]' COLUMNS ( product VARCHAR(100) PATH '$.product', NESTED PATH '$.tags[*]' COLUMNS ( tag VARCHAR(100) PATH '$' ) ) ) AS jt WHERE o.id = 5; +----+---------------+---------+------------+ | id | customer_name | product | tag | +----+---------------+---------+------------+ | 5 | Ellen | Pen | stationery | | 5 | Ellen | Pen | writing | | 5 | Ellen | Stapler | office | | 5 | Ellen | Stapler | tools | +----+---------------+---------+------------+ 4 rows in set (0.00 sec)
ネストされた配列tagsに対してNESTED PATHを使うことで、1つのJSON_
パフォーマンス上の注意点
実際に実行計画を見てもらうとわかる通り、JSON_
大量データを扱う場面では、一時テーブルに展開しておく、または事前に正規化した構造へ保存しておくといった運用も検討しましょう。
まとめ
JSON_
実際にJSONで受け取ったイベントログや履歴の集計、正規化せず柔軟にスキーマ設計したいデータなどはJSONをそのまま格納しておき、必要に応じてJSON_
ON EMPTY
やNESTED PATH
の使い方など、より詳細な内容については、公式ドキュメントのJSON テーブル関数を参考にしてください。