2018年9月30日日曜日

RDBMS(Oracle、SQL Server、MySQL)でJSONを扱ってみた

1. はじめに

複雑な構造のデータやデータ定義が頻繁に変わるデータを容易に格納できるという点で、NoSQLの中でもMongoDBやCouchbase、MarklogicなどのJSON形式でデータを保持できるドキュメント指向DBの利用は広がりつつあります。

ただし、JSON形式のデータを保持できるというだけであれば、昨今のメジャーなRDBMSにおいても機能的には可能になってきています。また、SQLにおけるJSONの扱いはSQL:2016という標準で規定されているため、各製品で利用できる関数や記法などはある程度似ています。ただ、実際には標準への準拠状況や製品独自の表記/オプションにおいて少なくない差異がある状況になっています。

そこで今回(?)は、メジャーなRDBMSの内でもOracle、SQL Server、MySQLの3DBにおける基本的なJSONの取り扱い方を簡単に見ていきたいと思います。

# PostgreSQLも見ようと思ったのですが、力尽きた…

マニュアルなどは当たっていますが、隅々まで読み込んだわけではないので、間違いやより良い方法があるかもしれません。その点はご容赦ください。また、最初に調べ始めたのがOracleのため、若干Oracle基準の説明になっている部分がありますが、ご承知おきください。

2. 動作確認環境

  • Oracle Database 18c (18.3.0) for Linux x86-64 (Enterprise Edition)
  • SQL Server 2017 for Linux (Evaluation Edition)
  • MySQL 8.0.12 for Linux x86-64 (Community Edition)

3. 使用データ

今回の動作確認では以下のデータを使っていきます。
 {
    "book_id" : 1,
    "title" : "キャズム",
    "publisher" : "翔泳社",
    "pages" : 360,
    "publish_dt" : "2014-10-04",
    "author" : [{"name" : "ジェフリー・ムーア", "role" : "著"},
                {"name" : "川又 政治", "role" : "翻訳"}]
}
{
    "book_id" : 2,
    "title" : "ビジョナリー・カンパニー",
    "publisher" : "日経BP社",
    "pages" : 475,
    "publish_dt" : "1995-09-26",
    "author" : [{"name" : "ジム・コリンズ", "role" : "著"},
                {"name" : "山岡 洋一", "role" : "翻訳"}]
}
{
    "book_id" : 3,
    "title" : "世界の経営学者はいま何を考えているのか",
    "publisher" : "英治出版",
    "pages" : 352,
    "publish_dt" : "2012-11-13",
    "author" : [{"name" : "入山 章栄", "role" : "著"}]
}
{
    "book_id" : 4,
    "title" : "イノベーションへの解",
    "publisher" : "翔泳社",
    "pages" : 373,
    "publish_dt" : "2003-12-13",
    "author" : [{"name" : "クレイトン・クリステンセン", "role" : "著"},
                {"name" : "マイケル・レイナー", "role" : "著"},
                {"name" : "玉田 俊平太", "role" : "監修"},
                {"name" : "山岡 洋一", "role" : "翻訳"}]
}
{
    "book_id" : 5,
    "title" : "機械との競争",
    "publisher" : "日経BP社",
    "pages" : 176,
    "publish_dt" : "2013-02-07",
    "author" : [{"name" : "エリック・ブリニョルフソン", "role" : "著"},
                {"name" : "アンドリュー・マカフィー", "role" : "著"},
                {"name" : "村井 章子", "role" : "翻訳"}]

4. 動作確認

4.1. データ挿入

JSONデータを格納するテーブルを作成し、データを挿入します。

* Oracle

CREATE TABLE book (
    book_id   NUMBER(3),
    book_json VARCHAR2(4000),
    CONSTRAINT pk_book PRIMARY KEY (book_id),
    CONSTRAINT c_book_01 CHECK (book_json IS JSON)
);
INSERT INTO book VALUES (
1,
'{
    "book_id" : 1,
    "title" : "キャズム",
    "publisher" : "翔泳社",
    "pages" : 360,
    "publish_dt" : "2014-10-04",
    "author" : [{"name" : "ジェフリー・ムーア", "role" : "著"},
                {"name" : "川又 政治", "role" : "翻訳"}]
}');
OracleではVARCHAR2、CLOB、またはBLOBのデータ型でJSONを保持します。対象の列に正しいJSONデータが入っていることを確認するために、「IS JSON」制約を利用します。

* SQL Server

CREATE TABLE [book] (
    [book_id] INT,
    [book_json] nvarchar(4000),
    CONSTRAINT [pk_book] PRIMARY KEY ([book_id]),
    CONSTRAINT [c_book_01] CHECK (ISJSON([book_json]) = 1)
);
INSERT INTO [book] VALUES (
1,
'{
    "book_id" : 1,
    "title" : "キャズム",
    "publisher" : "翔泳社",
    "pages" : 360,
    "publish_dt" : "2014-10-04",
    "author" : [{"name" : "ジェフリー・ムーア", "role" : "著"},
                {"name" : "川又 政治", "role" : "翻訳"}]
}');
SQL Serverではnvarcahrまたはnvarchar(max)のデータ型でJSONを保持します。対象の列に正しいJSONデータが入っていることを確認するために、正しいJSONデータを受け取ったときに1を返す「ISJSON」関数を利用します。

* MySQL

 CREATE TABLE book (
    book_id int,
    book_json json,
    CONSTRAINT pk_book PRIMARY KEY (book_id)
);
 INSERT INTO book VALUES (
1,
'{
    "book_id" : 1,
    "title" : "キャズム",
    "publisher" : "翔泳社",
    "pages" : 360,
    "publish_dt" : "2014-10-04",
    "author" : [{"name" : "ジェフリー・ムーア", "role" : "著"},
                {"name" : "川又 政治", "role" : "翻訳"}]
}');
MySQLではJSONデータ型が準備されているので、JSONデータの格納にはそれを用います。個人的には一番分かり易いかなと思います。

4.2. フィールドの値(スカラー値)の取得

各JSONデータからbook_idとtitleというフィールドの値を取得します。

* Oracle 

// ドット表記法 (Oracle)
SELECT b.book_json.book_id, b.book_json.title FROM book b;
Oracleにはドット表記法という記法があり、(JSONデータを含む列名).(JSONフィールド名)で該当の値を取得することができます。とても簡単ですね。ただ、取得できる値は一律に文字列型になってしまいます。上の結果でBOOK_IDの出力結果が左寄せになっていることからも分かると思います。
ちなみに、テーブル別名を付けないとORA-00904エラーになります。
// JSON_VALUE関数の利用 (Oracle)
SELECT
    json_value(book_json, '$.book_id' RETURNING NUMBER) book_id,
    json_value(book_json, '$.title') title
FROM
    book
SQL:2016ではJSONデータからスカラー値を取得するJSON_VALUEという関数があります。Oracleの場合はRETURNING句を付けることで返るデータ型も指定することができます。出力結果でBOOK_IDが右寄せになっていることが確認できます。
標準とはいえ、正直面倒くさい表記法ですね。
// JSON_TABLE関数の利用 (Oracle)
SELECT
    x.*
FROM
    book b,
    json_table(b.book_json, '$'
        COLUMNS (book_id NUMBER PATH '$.book_id',
                 title   VARCHAR2(80 CHAR) PATH '$.title')) x
先のJSON_VALUEのSQL文では、JSON_VALUE関数が2回呼ばれていますが、実際にJSONデータ1つに対して解析も2回行われるそうです。無駄ですね。この非効率を回避するために、JSON_TABLEという関数を使うことができます。これもSQL:2016の一部です。上のSQL文の結果は先のクエリと同じです。
ただ、ぶっちゃけ分かり易いSQL文かというと…

* SQL Server

// JSON_VALUE関数の利用 (SQL Server)
SELECT
    json_value([book_json], '$.book_id') [book_id],
    json_value([book_json], '$.title') [title]
FROM
    [book];
SQL Serverでは、Oracleのドット表記法のような記法はなく、基本SQL:2016で定義された関数を用いてJSONデータから値を取得します。
ちなみに、SQL ServerのJSON_VALUE関数には、OracleのRETURNINGのようなデータ型を指定するオプションはありません。
// 結果をJSON形式で返す (SQL Server)
SELECT
    json_value([book_json], '$.book_id') [book_id],
    json_value([book_json], '$.title') [title]
FROM
    [book]
FOR JSON PATH, WITHOUT_ARRAY_WRAPPER;
SQL Serverでは「FOR JSON」オプションを指定することで、結果をJSON形式で返すことができます。
ただし、アプリ側がJSON形式でデータを欲しいことは多々ありますが、複数のJSONデータをカンマでつなぎ1行という形式が使いやすいかというと、どうなんでしょうね。
// OPENJSON関数の利用 (SQL Server)
SELECT
    [b].[book_id],
    [x].[key],
    [x].[key],
    [x].[value]
FROM
    [book] [b]
CROSS APPLY
    openjson([b].[book_json]) [x]
WHERE
    [x].[key] IN ('title', 'pages');
SQL ServerにはJSON_TABLE関数はありません。JSONデータを表データに変換するには代わりにOPENJSON関数を利用します。ただ、結果はいわゆる縦持ちモデルで返ってくるので注意が必要です。

* MySQL

 // -> 記法 (MySQL)
SELECT
    b.book_json->'$.book_id',
    b.book_json->'$.title'
FROM
    book b
MySQLでは(JSONデータが格納された列)->(JSONパス式)という形でデータを取得することができます。Oracleのドット表記法と似ていますが、「->」の後ろがJSONパス式であることと、出力結果にダブルクォートが付くことに違いがいあります。ダブルクォートを外す関数UNQUOTEも準備されていますが少し試してみて想定通り動かなかったです(調査が不足しているのでしょう)。
// JSON_EXTRACT関数の利用 (MySQL)
SELECT
    json_extract(b.book_json, '$.book_id') book_id,
    json_extract(b.book_json, '$.title') title
FROM
    book b
MySQLにはJSON_VALUE関数はありませんが、代わりにJSON_EXTRACTという関数が準備されています(SQL:2016標準には含まれていません)。使い方はほぼ一緒ですね。
// JSON_EXTRACTは複数項目を一度に処理でき (MySQL)
SELECT
    json_extract(b.book_json, '$.book_id', '$.title')
FROM
    book b
MySQLではJSON_EXTRACT関数の1回の呼び出しで、複数のフィールドの値を取得することができます。ただし、本当に処理が効率的に行われているかは分かりません。また、結果が配列で返ってくるのは仕方ないとは言え、あまり便利ではないですね。
今回は触れませんが、MySQLにもJSON_TABLE関数は準備されています。

4.3.  フィールドの値(オブジェクト)の取得

各JSONデータからauthor(複数の著者に関しての情報を配列で保持)の1番目の値を取得します。

* Oracle

// JSON_QUERY関数の利用 (Oracle)
SELECT json_query(book_json, '$.author[0]') from book
スカラー値(数字や文字列、日付など)ではなく、JSONオブジェクトを取得する場合は、JSON_QUERY関数を使います。

* SQL Server

// JSON_QUERY関数の利用 (SQL Server)
SELECT
    json_query([book_json], '$.author[0]')
FROM
    [book];
JSON_QUERYの使い方は、Oracleとほぼ一緒です。

* MySQL

 // JSON_EXTRACTによるJSONオブジェクトの取得(MySQL)
SELECT
    b.book_json->'$.author[0]'
FROM
    book b
Oracle/SQLServerの場合、取得対象がスカラー値かJSONオブジェクト/配列かでJSON_VALUEとJSON_QUERYを使い分ける必要がありますが、MySQLの場合はどちらでもJSON_EXTRACTもしくは「->」表記を使えます。メリデメあって良し悪しは一概には言えませんが。 

4.4. 配列からのデータ取得

著者名のリストを取得します。

* Oracle

 // 配列の取得 (Oracle)
SELECT json_query(book_json, '$.author.name' WITH WRAPPER) from book
JSON_QUERYはJSONオブジェクトだけではなく、配列も返すことができます。

* SQL Server

// 配列の取得(SQL Server) - うまくいかない
SELECT
    json_query([book_json], '$.author.name')
FROM
    [book];
これをSQL Serverでやる方法は分かりませんでした。JSONデータは所詮文字列なので頑張ればできるのですが、JSON関数をうまく使ってできないのでしょうか。

* MySQL

// 配列の取得 (MySQL)
SELECT
    b.book_json->'$.author[*].name'
FROM
    book b
MySQLでは[*]という表記をパス式で配列のすべての要素にアクセスできます。

4.5. 絞り込み

出版社(publisher)が「翔泳社」である書籍のタイトル(title)を取得します。

* Oracle

// JSON_EXISTS関数の利用 (Oracle)
SELECT b.book_json.title FROM book b
WHERE json_exists(b.book_json, '$?(@.publisher == "翔泳社")');
単純な絞り込みであればWHERE句でJSON_VALUEを使えばいい気もしますが(もちろん使えます)、OracleにはJSON_EXISTSという関数も用意されています。

* SQL Server  

// JSON_VALUE関数による絞り込み (SQL Server)
SELECT
    json_value([book_json], '$.book_id') [book_id],
    json_value([book_json], '$.title') [title]
FROM
    [book]
WHERE
    json_value([book_json], '$.publisher') = '翔泳社';
SQL ServerにはJSON_EXISTS関数はないので、JSON_VALUE関数を使って絞り込みを実装します。
単純なケースであれば、個人的にはこっちの方が分かり易い気もします。

* MySQL

// JSON_CONTAINSによる絞り込み (MySQL)
SELECT
    b.book_json->'$.book_id',
    b.book_json->'$.title'
FROM
    book b
WHERE
    json_contains(b.book_json, '"翔泳社"', '$.publisher')
MySQLでもJSON_EXTRACTもしくは「->」表記による絞り込みはできますが、JSON_CONTAINSという関数を使った絞り込みも可能です。ちなみにこの「CONTAINS」の意味ですが、「翔泳社という文字をpublisherに含む」という意味ではなく、「(複数存在するかもしれない)puslisherの中に、翔泳社に一致するフィールドが最低1つ含まれる」という意味です(今回は各JSONデータの中に1回しかpublisherが出現しないので違いが分かり辛いですが)。

4.5. 集約(配列の作成)

出版社(publisher)ごとに書籍のタイトルを集約し、配列で返します。

* Oracle

 // JSON_OJECT関数とJSON_ARRAYAGG関数の利用 (Oracle)
SELECT
    json_object(
        'publisher'  VALUE x.publisher,
        'titles'     VALUE json_arrayagg(x.title))
FROM
    book b,
    json_table(b.book_json, '$'
        COLUMNS (publisher VARCHAR2(80 CHAR) PATH '$.publisher',
                 title   VARCHAR2(80 CHAR) PATH '$.title')) x
GROUP BY
    x.publisher;
このSQL文では、データを集約し配列を生成するためのJSON_ARRAYAGG関数と、結果をJSON形式で返すために表データ⇒JSONデータの変換を行うJSON_OBJECT関数を利用しています。

*SQL Server

SQL Serverでこれを実現する方法は分かりませんでした。もちろん文字列処理をすれば可能なのは分かりますが。

* MySQL

// JSON_OBJECTとJSON_ARRAYAGGを利用 (MySQL)
SELECT
    json_object('publisher', x.publisher, 'titles', json_arrayagg(x.title))
FROM
    (
    SELECT
        b.book_json->'$.publisher' publisher,
        b.book_json->'$.title' title
    FROM
        book b
    ) x
GROUP BY
    x.publisher
MySQLには若干表記が異なりますが、Oracleと同じJSON_OBJECTとJSON_ARRAYAGG関数が用意されています。
本当は副問い合わせなしで書きたかったのですが、どうもGROUP BYの中では「->」表記はGROUP BYの中では使えないようですね(JSON_EXTRACTも)。なぜだろう…

5. まとめ

今回、Oracle、SQL Server、MySQLのJSONの扱いを比較してみましたが、扱った範囲だと、以下のような感じでしょうか。
  • OracleとMySQLは大体やりたいことができそう。あとは好みかな。
  • SQL Serverは対応しているJSON関数やJSONパス式の範囲が狭く、まだこれから。
あと、今回は索引が利用できるかなどの性能面は全く見ていないので、実際に利用するにはもう少し調査が必要でしょう。
あと、今回は基本、JSONデータから表形式データを取得するケースがメインでしたが、それは準備されている関数がJSONデータ⇒SQL値の変換関数が多いことから来ています。JSONデータをそのまま処理してJSONデータとして返すにはSQL:2016の関数群(JSON_*)を使うのはちょっと違うのでしょうね。

そういうことをするには、MySQLなどではMySQL Shell for Javascript/Pythonなど使えるのでしょうか。

0 件のコメント:

コメントを投稿