2021年6月23日水曜日

MySQL で JSON 型を使ってみた

MySQL で JSON 型を使ってみた

概要

MySQL 5.7.8 以降で使える JSON 型を試したみました JSON データの挿入方法から特殊な JSON クエリを使った検索方法まで紹介したいと思います

環境

  • macOS 11.4
  • MySQL 8.0.25

準備

まずは JSON 型のカラムを持つテーブルの定義しましょう

  • CREATE DATABASE test;
  • USE test;
  • CREATE TABLE user (id int(11) NOT NULL AUTO_INCREMENT, profile json DEFAULT NULL, PRIMARY KEY (id));

型名にはその名の通り「json」という型があるのでこれを使います ちゃんと json 型で登録されています

  • SHOW CREATE TABLE user \G
*************************** 1. row ***************************
       Table: user
Create Table: CREATE TABLE `user` (
  `id` int NOT NULL AUTO_INCREMENT,
  `profile` json DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
1 row in set (0.01 sec)

データ登録

次に JSON 型のデータを登録してみます

データを登録する際はシングルクオートで全体を囲って中のキーや値はダブルクオートで囲います

  • INSERT INTO user VALUES (null, ‘{“name”:“hawksnowlog”}’);
  • INSERT INTO user VALUES (null, ‘{“name”:“hawk”}’);
  • INSERT INTO user VALUES (null, ‘{“name”:“snowlog”}’);

JSON_EXTRACT

ここからは特殊な JSON 型のクエリを使っていきます

JSON_EXTRACT クエリは特定の Json ドキュメントを取得するためのクエリです

例えば name キーの値だけを取得する場合は以下のようにします

  • SELECT JSON_EXTRACT(profile, ‘$.name’) FROM user;
+---------------------------------+
| JSON_EXTRACT(profile, '$.name') |
+---------------------------------+
| "hawksnowlog"                   |
| "hawk"                          |
| "snowlog"                       |
+---------------------------------+
3 rows in set (0.00 sec)

これと WHERE 句を組み合わせて特定の JSON ドキュメントを含んだレコードのみを取得することもできます

  • SELECT * FROM user WHERE JSON_EXTRACT(profile, ‘$.name’) = ‘hawksnowlog’;
+----+-------------------------+
| id | profile                 |
+----+-------------------------+
|  1 | {"name": "hawksnowlog"} |
+----+-------------------------+
1 row in set (0.00 sec)

LIKE 句と組み合わせるとより柔軟な検索が可能になります

  • SELECT * FROM user WHERE JSON_EXTRACT(profile, ‘$.name’) LIKE ‘%hawk%’;
+----+-------------------------+
| id | profile                 |
+----+-------------------------+
|  1 | {"name": "hawksnowlog"} |
|  2 | {"name": "hawk"}        |
+----+-------------------------+
2 rows in set (0.00 sec)

JSON_SEARCH

JSON_SEARCH クエリは特定のドキュメントが存在するパスを取得することができるクエリです

2 つ目の引数には「one」or「all」のどちらかを指定します 前者は一致した最初の JSON ドキュメントのパスだけを取得します 後者は一致したすべての JSON ドキュメントのパスを取得します 今回は一致する JSON ドキュメントが 1 つしかないので one でも all でもパスは 1 つになります

  • SELECT JSON_SEARCH(profile, ‘one’, ‘hawksnowlog’) FROM user;
+--------------------------------------------+
| JSON_SEARCH(profile, 'one', 'hawksnowlog') |
+--------------------------------------------+
| "$.name"                                   |
| NULL                                       |
| NULL                                       |
+--------------------------------------------+
3 rows in set (0.00 sec)

応用して先程の JSON_EXTRACT と組み合わせることもできます

  • SELECT JSON_SEARCH(profile, ‘all’, ‘%hawk%’) FROM user WHERE JSON_EXTRACT(profile, ‘$.name’) LIKE ‘%hawk%’;
+---------------------------------------+
| JSON_SEARCH(profile, 'all', '%hawk%') |
+---------------------------------------+
| "$.name"                              |
| "$.name"                              |
+---------------------------------------+
2 rows in set (0.00 sec)

JSON_CONTAINS

JSON_CONTAINS は各レコードに特性のドキュメントが含まれているか確認することができるクエリです 含まれている場合は「1」含まれていない場合は「0」が返ってきます

2 つ目の引数に検索する値を指定するのですが文字列を指定する場合はシングルクオートで囲った上で中をダブルクオートでも囲う必要があるので注意しましょう

以下は name フィールドに hawksnowlog という値を含む JSON ドキュメントが各レコードで含まれているか確認するクエリです

  • SELECT JSON_CONTAINS(profile, ‘“hawksnowlog”’, ‘$.name’) FROM user;
+---------------------------------------------------+
| JSON_CONTAINS(profile, '"hawksnowlog"', '$.name') |
+---------------------------------------------------+
|                                                 1 |
|                                                 0 |
|                                                 0 |
+---------------------------------------------------+
3 rows in set (0.00 sec)

JSON_CONTAINS も JSON_EXTRACT と組み合わせて検索することができます

  • SELECT JSON_CONTAINS(profile, '"hawksnowlog"', '$.name') FROM user WHERE JSON_EXTRACT(profile, '$.name') LIKE '%hawk%';
+---------------------------------------------------+
| JSON_CONTAINS(profile, '"hawksnowlog"', '$.name') |
+---------------------------------------------------+
|                                                 1 |
|                                                 0 |
+---------------------------------------------------+
2 rows in set (0.00 sec)

JSON_VALUE

指定のキー (パス) の値だけを取り出すことができるクエリです

  • SELECT JSON_VALUE(profile, ‘$.name’) from user;
+-------------------------------+
| JSON_VALUE(profile, '$.name') |
+-------------------------------+
| hawksnowlog                   |
| hawk                          |
| snowlog                       |
+-------------------------------+
3 rows in set (0.01 sec)

JSON_EXTRACT に似ていますがこちらは返り値の型変換が行えたり空 or エラー時のハンドリングができます

  • SELECT JSON_VALUE(profile, ‘$.hoge’ RETURNING CHAR DEFAULT ‘default_value!’ ON EMPTY) from user;
+--------------------------------------------------------------------------------+
| JSON_VALUE(profile, '$.hoge' RETURNING CHAR DEFAULT 'default_value!' ON EMPTY) |
+--------------------------------------------------------------------------------+
| default_value!                                                                 |
| default_value!                                                                 |
| default_value!                                                                 |
+--------------------------------------------------------------------------------+
3 rows in set (0.00 sec)

ちなみに JSON_VALUE も WHERE 句に使えます

  • SELECT * FROM user WHERE JSON_VALUE(profile, ‘$.name’) = ‘hawk’;

その他クエリ

  • JSON_OVERLAPS・・・ 2 つの JSON ドキュメントを比較することができるクエリ
  • MEMBER OF・・・配列内に特定の値が含まれているかチェックするクエリ

Ruby から使ってみる

mysql2 を使って JSON 系のクエリを発行してみました サンプルコードを紹介します

require 'mysql2'
require 'json'

client = Mysql2::Client.new(:host => "localhost", :username => "root", :database => 'test')

query = "select * from user"
records = client.query(query)
records.each do |record|
  puts record["id"]
  profile = JSON.parse(record["profile"])
  puts profile["name"]
end

query = "SELECT JSON_VALUE(profile, '$.name') from user;"
records = client.query(query)
records.each do |record|
  puts record
end

結果は以下の通りです JSON 系のクエリを発行した場合は AS 句を使って結果のカラムを変更してあげると扱いやすいと思います

1
hawksnowlog
2
hawk
3
snowlog
hawksnowlog
hawk
snowlog

最後に

気になるのは性能面かなと思います 全レコードを検索するような場合に更に JSON オブジェクトを展開するとなると検索のオーダが跳ね上がるのでそこは MySQL で頑張るのかコード側で頑張るのか (もしくは別のコンポーネントを使うのか) などを考えたほうが良いかなと思います

参考サイト

0 件のコメント:

コメントを投稿