注目しているMySQLの新機能について調べてみた

はじめに

ダニーです、お久しぶりです。

今日は最近不具合調査などで、データの調査・修正など何かとDBを触る機会が多かったんですが、最近DB関連のキャッチアップしてないなと思いまして、MySQL 8.0 から色々新機能やら増えてるみたいだし、何か役に立ちそうな機能が増えてないか調べた内容をブログにしようとなった次第です。

それでは行ってみましょう。

注目した新機能

今回は公式リファレンスをベースに

  • 調査する時に便利そう
  • 気になった(へーそういうのできるようになったんだ!的なもの)
  • なにこれ?

なものをピックアップしました。
対象バージョンは8.0・8.4・9.0 辺りを中心にビックアップしました。
※公式に8.1〜8.3のリファレンスのリンクがなかったので

アトミックデータ定義ステートメント (アトミック DDL) サポート(MySQL 8.0 ~)

MySQL 8.0 から、DDL 操作(CREATE、ALTER、DROP など)がアトミックに実行されるようになり、クラッシュ時のデータ一貫性が向上しました。

これのおかげで、5.7などの旧バージョンでDDL操作中にクラッシュなどが発生した時に中途半端なテーブルなどが作成されてしまう可能性がありましたが、8.0以降は

  • 完全に作成するか
  • 失敗か

どちらかしか発生しないようになります。

-- 例 : MySQL 5.7の場合

mysql> CREATE TABLE t1 (c1 INT); -- t1テーブル作成:成功
Query OK, 0 rows affected (0.04 sec)

mysql> show tables; -- テーブルの存在確認
+----------------+
| Tables_in_test |
+----------------+
| t1             |
+----------------+
1 row in set (0.00 sec)

mysql> DROP TABLE t1, t2; -- t1・t2(存在しない)テーブルの削除
ERROR 1051 (42S02): Unknown table 'test.t2' -- t2テーブル無いとエラー
mysql> show tables;
Empty set (0.00 sec) -- 失敗したけど、t1テーブルは削除されている。


-- ■■■■■■■■■■■■■■■■■■■■■■■■■■


-- 例:MySQL8.0の場合

mysql> CREATE TABLE t1 (c1 INT); -- t1テーブル作成:成功
Query OK, 0 rows affected (0.02 sec)

mysql> show tables; -- テーブルの存在確認
+----------------+
| Tables_in_test |
+----------------+
| t1             |
+----------------+
1 row in set (0.01 sec)

mysql> DROP TABLE t1, t2; -- t1・t2(存在しない)テーブルの削除
ERROR 1051 (42S02): Unknown table 'test.t2' -- t2テーブル無いとエラー
mysql> show tables; 
+----------------+
| Tables_in_test |
+----------------+
| t1             |
+----------------+
1 row in set (0.00 sec) -- ※失敗したので、DROP 操作が正常にロールバックされた。

JSON の拡張機能(MySQL 8.0 〜)

MySQL 8.0 では JSON 関数が強化され、JSON_TABLE などの新機能により、JSON データの操作がより柔軟になったようです。

実務では、DBにJSONで保存されたカラムなどを操作することが少ないので、そういうこともできるんですねーという学びになりました。

■ JSON 集計関数 JSON_ARRAYAGG() および JSON_OBJECTAGG() が追加

-- JSON_ARRAYAGG() 
-- 結果を単一のJSON配列で返す
mysql> SELECT o_id, JSON_ARRAYAGG(attribute) AS attributes
    -> FROM (
    ->     SELECT 2 AS o_id, 'color' AS attribute, 'red' AS value
    ->     UNION ALL
    ->     SELECT 2, 'fabric', 'silk'
    ->     UNION ALL
    ->     SELECT 3, 'color', 'green'
    ->     UNION ALL
    ->     SELECT 3, 'shape', 'square'
    -> ) AS tmp
    -> GROUP BY o_id;

+------+---------------------+
| o_id | attributes          |
+------+---------------------+
|    2 | ["color", "fabric"] |
|    3 | ["color", "shape"]  |
+------+---------------------+
2 rows in set (0.01 sec) 


-- ■■■■■■■■■■■■■■■■■■■■■■■■■■


-- JSON_OBJECTAGG()
-- 結果をキーと値のペアを含む JSON オブジェクトを返す
mysql> SELECT o_id, JSON_OBJECTAGG(attribute, value)
    -> FROM (
    ->     SELECT 2 AS o_id, 'color' AS attribute, 'red' AS value
    ->     UNION ALL
    ->     SELECT 2, 'fabric', 'silk'
    ->     UNION ALL
    ->     SELECT 3, 'color', 'green'
    ->     UNION ALL
    ->     SELECT 3, 'shape', 'square'
    -> ) AS tmp
    -> GROUP BY o_id;

+------+---------------------------------------+
| o_id | JSON_OBJECTAGG(attribute, value)      |
+------+---------------------------------------+
|    2 | {"color": "red", "fabric": "silk"}    |
|    3 | {"color": "green", "shape": "square"} |
+------+---------------------------------------+
2 rows in set (0.01 sec)

■ JSON_TABLE() 関数追加

mysql> SELECT *
    -> FROM
    ->     JSON_TABLE(
    ->         '[{"a":3,"b":"0"},{"a":"3","b":"1"},{"a":2,"b":1},{"a":0},{"b":[1,2]}]',
    ->         "$[*]" COLUMNS(
    ->             rowid FOR ORDINALITY,
    ->             xa INT EXISTS PATH "$.a",
    ->             xb INT EXISTS PATH "$.b",
    ->             sa VARCHAR(100) PATH "$.a",
    ->             sb VARCHAR(100) PATH "$.b",
    ->             ja JSON PATH "$.a",
    ->             jb JSON PATH "$.b"
    ->         )
    ->     ) AS  jt1;
+-------+------+------+------+------+------+--------+
| rowid | xa   | xb   | sa   | sb   | ja   | jb     |
+-------+------+------+------+------+------+--------+
|     1 |    1 |    1 | 3    | 0    | 3    | "0"    |
|     2 |    1 |    1 | 3    | 1    | "3"  | "1"    |
|     3 |    1 |    1 | 2    | 1    | 2    | 1      |
|     4 |    1 |    0 | 0    | NULL | 0    | NULL   |
|     5 |    0 |    1 | NULL | NULL | NULL | [1, 2] |
+-------+------+------+------+------+------+--------+
5 rows in set (0.00 sec)

感想

JSON関係の機能が色々増えてるので、どういう機能か調べてみたものの・・・どういう時に使うのか?というのが思い浮かばなかった。

仮にJSON型で保存されたデータを扱う場合は、大体何かしらのフレームワークのORMで解決できたりするのでは?と思いましたが、ORMで取得する前にJSONを解析することでプログラム側で複雑な処理をする必要がなくなるというのもメリットの一つと言える・・・が、結局ORMなのにSQLを書かなくてはいけないため、一長一短という感想でした。

WITH (共通テーブル式) がサポートされました(MySQL 8.0 〜)

CTE(Common Table Expressions)の導入により、一時テーブルを使わずに複雑なクエリが簡潔に書けるようになりました。

-- 例として、今現在の有料のプラン契約の企業を検索したいとなった時に
-- 企業テーブル(company)
-- 契約テーブル(plan_entry)

-- MySQL 5.7の場合
mysql> SELECT
    ->     company.id,
    ->     company.name,
    ->     plan_entry.start_date,
    ->     plan_entry.end_date
    -> FROM
    ->     plan_entry
    -> LEFT JOIN
    ->     company ON plan_entry.company_id = company.id
    -> WHERE
    ->     company.deleted_at is null
    ->     AND plan_entry.plan_id <> 1
    -> ;
+------+--------------+------------+------------+
| id   | name         | start_date | end_date   |
+------+--------------+------------+------------+
|    1 | さんぷる1    | 2025-01-10 | 2025-07-09 |
+------+--------------+------------+------------+
|    2 | さんぷる2    | 2025-02-17 | 2025-08-16 |
+------+--------------+------------+------------+
2 row in set (0.01 sec)


-- ■■■■■■■■■■■■■■■■■■■■■■■■■■


-- MySQL 8.0 から
mysql> WITH
    ->     current_company as (
    ->       SELECT * FROM company ORDER BY id
    ->     ),
    ->     current_plan_entry as (
    ->       SELECT * FROM plan_entry ORDER BY id
    ->     )
    ->
    -> SELECT
    ->     current_company.id,
    ->     current_company.name,
    ->     current_plan_entry.start_date,
    ->     current_plan_entry.end_date
    -> FROM
    ->     current_company
    -> LEFT JOIN
    ->     current_plan_entry ON current_company.id = current_plan_entry.company_id
    -> WHERE
    ->     current_plan_entry.plan_id <> 1
    -> ;
+------+--------------+------------+------------+
| id   | name         | start_date | end_date   |
+------+--------------+------------+------------+
|    1 | さんぷる1    | 2025-01-10 | 2025-07-09 |
+------+--------------+------------+------------+
|    2 | さんぷる2    | 2025-02-17 | 2025-08-16 |
+------+--------------+------------+------------+
2 row in set (0.00 sec)

※再帰的な場合は RECURSIVE キーワードを含めることで表現することも可能のようです。

mysql> WITH RECURSIVE cte (n) AS
    -> (
    ->   SELECT 1
    ->   UNION ALL
    ->   SELECT n + 1 FROM cte WHERE n < 5
    -> )
    -> SELECT * FROM cte;
+------+
| n    |
+------+
|    1 |
|    2 |
|    3 |
|    4 |
|    5 |
+------+
5 rows in set (0.00 sec)

感想

例のような簡単な場合はそこまで恩恵は分かりづらいかもしれませんが、何百行に及ぶSQLなどになった場合に条件が複雑になるケースよくあると思います。
WITHで意味のある分類毎に分離しそれを定義すると共通して使えることで、最大の恩恵は結果がメモリにキャッシュされることです。
何度も再利用可能のため、全体を通してレスポンスが大幅に改善が期待出来ます。
またSQLの簡素化も期待出来るので、調査・分析などに便利だと感じました。

ウィンドウ関数がサポートされました(MySQL 8.0 〜)

ランキングや累積計算などの集計処理が、SQL だけで柔軟に実装できるようになりました。

ほとんどの集計関数(「AVG」や「SUM」など)はウインドウ関数として利用ができるようになっているので、便利になりました。

例:公開している求人の給与の平均と合計が知りたい

mysql> SELECT
    ->     id,
    ->     company_id
    ->     title,
    ->     salary_min,
    ->     AVG(salary) OVER (partition by company_id) AS avg_salary, -- company_id 毎の平均値
    ->     SUM(salary) OVER (partition by company_id) AS sum_salary  -- company_id 毎の集計
    -> FROM adv;
+----+------------+-------+--------+------------+------------+
| id | company_id | title | salary | avg_salary | sum_salary |
+----+------------+-------+--------+------------+------------+
|  1 |          1 | testA |   1500 |  3000.0000 |       9000 |
|  2 |          1 | testB |   2000 |  3000.0000 |       9000 |
|  3 |          1 | testC |   5500 |  3000.0000 |       9000 |
+----+------------+-------+--------+------------+------------+
3 rows in set (0.00 sec)

他にも様々なウインドウ関数が追加されています。

名前説明
CUME_DIST()累積分布値
DENSE_RANK()パーティション内の現在の行のランク (ギャップなし)
FIRST_VALUE()ウィンドウフレームの最初の行からの引数の値
LAG()パーティション内の現在行より遅れている行の引数の値
LAST_VALUE()ウィンドウフレームの最後の行からの引数の値
LEAD()パーティション内の現在の行の先頭行からの引数の値
NTH_VALUE()ウィンドウフレームの N 番目の行からの引数の値
NTILE()パーティション内の現在の行のバケット番号。
PERCENT_RANK()パーセントランク値
RANK()パーティション内の現在の行のランク (ギャップあり)
ROW_NUMBER()パーティション内の現在の行数
引用:12.21.1 Window 関数の説明
https://dev.mysql.com/doc/refman/8.0/ja/window-function-descriptions.html

感想

個人的に便利そうと感じたのは、LAG 関数でした。

例えば

-- 例:現在契約中のプランIDの「一つ前」のプランIDが知りたい

mysql> SELECT
    ->     id,
    ->     company_id,
    ->     plan_id,
    ->     LAG(plan_id) OVER w as `LAG`
    -> FROM plan_entry
    -> WINDOW w as (PARTITION BY company_id ORDER BY id);
+----+------------+---------+------+
| id | company_id | plan_id | LAG  |
+----+------------+---------+------+
|  1 |          1 |       1 | NULL |
|  2 |          1 |       2 |    1 | 
+----+------------+---------+------+
2 rows in set (0.00 sec)

というように、「一つ前」の情報が知りたいというケースを知りたいと思うのは私だけではないはず・・・!!

サブクエリなどでも表現は出来ますが、よりシンプルに取得できるので参考になりました。

ラテラル導出テーブル が出来るようになりました(MySQL 8.0 〜)

LATERAL キーワードのサポートによってサブクエリがより活用出来るようになりました。

例えば、

  • 企業毎に公開中の求人数が知りたい
  • 企業毎に応募の合計数が知りたい

となった時に通常サブクエリなどで求めることは可能です。

mysql> SELECT
    -> company.name,
    ->     (
    ->         SELECT count(*) AS count
    ->         FROM adv
    ->         WHERE adv.company_id = company.id
    ->     ) AS adv_count,
    ->     (
    ->         SELECT count(*) AS count
    ->         FROM entry
    ->         LEFT JOIN adv ON adv.id = entry.adv_id
    ->         WHERE adv.company_id = company.id
    ->     ) AS entry_count
    -> FROM
    -> company;
+--------------+-----------+-------------+
| name         | adv_count | entry_count |
+--------------+-----------+-------------+
| sample_A     |         3 |           5 |
+--------------+-----------+-------------+
| sample_B     |        10 |          20 |
+--------------+-----------+-------------+
2 row in set (0.00 sec)

ですが、この場合企業毎に2回ずつサブクエリで計算されるので、効率は良くないですよね。

更にここから

  • 応募の合計数が〇〇以上のものだけがほしい や
  • 公開求人数が〇〇以上のものだけに絞りたい など

複数の条件で更に絞りたい場合 SELECT 句 でのサブクエリでは都合が悪い場合があります。(HAVING句 利用などが必要になってくる。)

今回のLATERAL キーワードを使うことで FROM 句 で定義することが出来るようになるため、上記の条件指定などより効率的に書くことが可能になります。

mysql> SELECT
    ->     company.name,
    ->     adv_count.count AS adv_count,
    ->     entry_count.count AS entry_count
    -> FROM
    ->     company,
    ->     LATERAL
    ->     (
    ->         SELECT count(*) AS count
    ->         FROM adv
    ->         WHERE adv.company_id = com.id
    ->     ) AS adv_count,
    ->     LATERAL
    ->     (
    ->         SELECT count(*) AS count
    ->         FROM entry
    ->         LEFT JOIN adv ON adv.id = entry.adv_id
    ->         WHERE adv.company_id = com.id
    ->     ) AS entry_count
    -> WHERE
    ->     entry_count.count > 5
+--------------+-----------+-------------+
| name         | adv_count | entry_count |
+--------------+-----------+-------------+
| sample_B     |        10 |          20 |
+--------------+-----------+-------------+
1 row in set (0.00 sec)

EXPLAIN ANALYZE ステートメントが追加されました(MySQL 8.0 〜)

実行ステートメント順にツリー形式上で情報が表示され

  • 推定実行コスト
  • 戻された行の推定数
  • 最初の行を返す時間
  • すべての行 (実際のコスト) を返す時間 (ミリ秒)
  • イテレータによって返された行数
  • ループ数

などの情報が提供されるため、より詳細な調査が可能となりました。

-- 例
mysql> EXPLAIN ANALYZE SELECT * FROM company JOIN adv ON adv.company_id = company.id;
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN                                                                                                                                                                                                                                                                                                           |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Inner hash join (adv.company_id = company.id)  (cost=0.802 rows=1) (actual time=0.193..0.242 rows=3 loops=1)
|   -> Table scan on adv  (cost=0.352 rows=2) (actual time=0.0208..0.0257 rows=3 loops=1)
|   -> Hash
|       -> Table scan on company  (cost=0.35 rows=1) (actual time=0.0424..0.0467 rows=1 loops=1)
|
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

JavaScript ストアド プログラム(MySQL 9.0 〜)

MySQL 9 よりJavaScript プログラム をSQL 上で利用することが可能となったようです。

※利用するにはいくつかの条件・制限があるようです。

下記公式よりサンプルを抜粋させて頂きました。

mysql> CREATE FUNCTION js_pow(arg1 INT, arg2 INT) 
    -> RETURNS INT LANGUAGE JAVASCRIPT 
    -> AS 
    ->   $$
    $>     let x = Math.pow(arg1, arg2);
    $>     return x;
    $>   $$
    -> ;
Query OK, 0 rows affected (0.02 sec)

mysql> SELECT js_pow(2,3);
+-------------+
| js_pow(2,3) |
+-------------+
|           8 |
+-------------+
1 row in set (0.00 sec)

27.3.1 JavaScript Stored Program Creation and Management

https://dev.mysql.com/doc/refman/9.0/en/srjs-management.html

正直な感想ですが、どうやって活用するのかがわからないという感想です。(私には使いこなせる自信がないです・・・)

ただリファレンスの見ていくと関数定義以外にスキーマ操作なども可能なので、javascript に精通してる場合はメリットがありそうですが、今の時点では「そういうことも出来るようになってすごいなー」という一機能としての感想に留まりました。

まとめ

いかがでしたでしょうか?
今回調べててやはり バージョン8.0 から 非常に多くの機能の追加や仕様の更新が目立ちました。
バージョン8.4 は8.0 からの各機能のアップデートが多く、より安定したバージョンとなってる印象です。
バージョン9.0 からは 更に JavaScript プログラムがサポートされ新たな機能が MySQL に加わりました。

しばらく漠然とMySQL を利用してましたが、いろんな機能や変更があり業務で活用出来るものがあったので良かったです。

なにか刺さった内容ありましたか?

今回は個人的にピックアップした内容となります、他にもまだまだ追加・更新された機能がたくさんありますので、気になった方は是非公式のリファレンスをご参照ください。

それでは本日はこの辺で、お読み頂きありがとうございました!

参考資料