JSON_TABLE()を使ってMySQLのJSONをテーブルに変換する

MySQL 8.0のJSONをテーブルに変換するにはJSON_TABLE()を使う。

例えば次のように、人が複数のスキルを持っていて、そのスキルを複数の評価者が点数付けすることを表すのに、正規化しないでJSONで定義しているとする。

personevaluatorskill_evaluations
1 999
{
  "skills": [
    {
      "name": "Python",
      "level": 3
    },
    {
      "name": "Java",
      "level": 4
    }
  ]
}
1 888
{
  "skills": [
    {
      "name": "Python",
      "level": 4
    },
    {
      "name": "Java",
      "level": 5
    }
  ]
}
2 999
{
  "skills": [
    {
      "name": "Python",
      "level": 1
    },
    {
      "name": "Shell",
      "level": 5
    }
  ]
}

各人のスキルレベルの評価平均を取得したい時、正規化していないがゆえにGROUP BYが簡単にできなくなっている。

そのような時にJSON_TABLE()を使ってJSONをテーブルに変換する。(書き方がCROSS JOINのようにも見えるが、実際にはLATERAL JOIN的な動きになっている。)

SELECT
    skill_name,       -- JSON_TABLE内で定義した名前
    AVG(skill_level)  -- JSON_TABLE内で定義した名前
FROM table1,
    JSON_TABLE(skill_evaluations, '$.skills[*]' COLUMNS (  -- 参照元のテーブルの直後にJSON_TABLEを配置
        skill_name  TEXT PATH '$.name',  -- 定義名 型 PATH JSONパス
        skill_level INT  PATH '$.level')
    ) x
WHERE person = 1
GROUP BY skill_name

結果は以下のようになる

skill_name avg(skill_level)
Python 3.5000
Java 4.5000

型定義にはCOLLATE句をつけた方がいい

GROUP BYする時に、大文字小文字/ひらがなカタカナ等が同一視されたりされなかったり、MySQLのデフォルト照合順序の設定に依存してしまう。

意図通りの結果を得るために、TEXT型やVARCHAR型として定義した場合は、COLLATE句をつけた方がいい。

COLLATE句なし

COLLATE句なしだと以下のようにPythonとPYTHONが同一視されるかもしれない。

WITH table_1 AS (
  SELECT
      1 AS person,
      999 AS evaluator,
      '{"skills": [{"name": "Python", "level": 3}]}' AS skill_evaluations
  UNION ALL
  SELECT
      1 AS person,
      999 AS evaluator,
      '{"skills": [{"name": "PYTHON", "level": 4}]}' AS skill_evaluations
)
SELECT
    skill_name,
    AVG(skill_level)
FROM table1,
    JSON_TABLE(skill_evaluations, '$.skills[*]' COLUMNS (
        skill_name  TEXT PATH '$.name',
        skill_level INT  PATH '$.level')
    ) x
WHERE person = 1
GROUP BY skill_name
skill_name avg(skill_level)
Python 3.5000

COLLATE句あり

skill_name TEXT PATH '$.name'

skill_name TEXT COLLATE utf8mb4_bin PATH '$.name'

とすれば、常に以下の結果が得られる。

skill_name avg(skill_level)
Python 3.0000
PYTHON 4.0000