MySQL道普請便り

第278回MySQL 9.7で変わったDATE型の挙動を確認してみる

MySQL 9.7.0のリリースノートを見ていると、DATE型に関する修正がいくつかまとめて入っていることに気付きます。TIMEDIFF()、FROM_DAYS()、DAYNAME()、ADDDATE()など、普段から使うことのある関数も含まれています。

これらは WL#16895: Refactor DATE handling in server によるDATE型の扱いの見直しに関連する修正です。Worklogを見ると、サーバー内部でDATE値を扱う際に利用していたMYSQL_TIMEを、DATE専用のDate_valへ置き換えるリファクタリングのようです。

内部実装の変更と聞くと、普段利用するSQLへの影響はあまりなさそうにも見えます。しかし、リリースノートには実際の関数の修正も複数挙がっています。ということで今回は、MySQL 8.4.11とMySQL 9.7.2を用意して、どのような挙動差があるのかを確認してみました。

今回のsql_modeは、MySQL 9.4.11とMySQL 9.7.2で同一です。

ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,
ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION

設定の差ではなく、バージョン差として結果を比較します。

0年の日付演算

まずは、リリースノートにも記載されている0年の日付演算です。今回は、日付演算の境界値として0000-01-01を用います。0000-00-00は、MySQLで特別な意味を持つゼロ日付です。普段のアプリケーションで0年の日付を明示的に使う機会は多くないと思いますが、境界値に対する日付演算の結果は気になるところです。0年1月1日と1月31日に1日を加算してみます。

mysql8.4.11> SELECT
    ->   ADDDATE('0000-01-01', INTERVAL 1 DAY) AS jan_1,
    ->   ADDDATE('0000-01-31', INTERVAL 1 DAY) AS jan_31;
+------------+------------+
| jan_1      | jan_31     |
+------------+------------+
| 0000-00-00 | 0000-00-00 |
+------------+------------+

mysql9.7.2> SELECT
    ->   ADDDATE('0000-01-01', INTERVAL 1 DAY) AS jan_1,
    ->   ADDDATE('0000-01-31', INTERVAL 1 DAY) AS jan_31;
+------------+------------+
| jan_1      | jan_31     |
+------------+------------+
| 0000-01-02 | 0000-02-01 |
+------------+------------+

8.4.11では、どちらも0000-00-00になりました。一方、9.7.2では0000-01-02および0000-02-01となり、月またぎを含めて日付として計算した結果が返っています。

次に、TO_DAYS()とFROM_DAYS()の組み合わせも確認します。TO_DAYS()は日付を通算日へ変換する関数で、FROM_DAYS()は通算日を日付へ戻す関数です。

mysql8.4.11> SELECT TO_DAYS('0000-01-01'), FROM_DAYS(1);
+------------------------+--------------+
| TO_DAYS('0000-01-01')  | FROM_DAYS(1) |
+------------------------+--------------+
|                      1 | 0000-00-00   |
+------------------------+--------------+

mysql9.7.2> SELECT TO_DAYS('0000-01-01'), FROM_DAYS(1);
+------------------------+--------------+
| TO_DAYS('0000-01-01')  | FROM_DAYS(1) |
+------------------------+--------------+
|                      1 | 0000-01-01   |
+------------------------+--------------+

TO_DAYS('0000-01-01')の結果は、両方とも1です。しかし、8.4.11ではFROM_DAYS(1)が0000-00-00となり、この値では変換の往復が成立していません。9.7.2ではFROM_DAYS(1)も0000-01-01を返すため、往復できるようになっています。

DATEとDATETIMEを混ぜたTIMEDIFF()

次に、TIMEDIFF()の引数にDATETIMEとDATEを混ぜた場合です。

mysql8.4.11> SELECT
    ->   TIMEDIFF('2026-08-24 12:00:00', '2026-08-23') AS diff1,
    ->   TIMEDIFF('2026-08-23', '2026-08-24 12:00:00') AS diff2;
+-------+-------+
| diff1 | diff2 |
+-------+-------+
| NULL  | NULL  |
+-------+-------+

mysql9.7.2> SELECT
    ->   TIMEDIFF('2026-08-24 12:00:00', '2026-08-23') AS diff1,
    ->   TIMEDIFF('2026-08-23', '2026-08-24 12:00:00') AS diff2;
+-----------+------------+
| diff1     | diff2      |
+-----------+------------+
| 36:00:00  | -36:00:00  |
+-----------+------------+

8.4.11では、どちらの式もNULLになりました。一方、9.7.2では36:00:00-36:00:00が返ります。この結果から、今回のTIMEDIFF()ではDATE側がその日の00:00:00として扱われ、DATETIMEとの差分が計算されていることがわかります。

8.4系でTIMEDIFF()の結果がNULLかどうかを分岐に使っているSQLがあれば注意が必要です。今回のように、8.4ではNULLだった式が、9.7では時間差を返すようになります。

DATEをBIGINTへコピーしてみる

続いて、DATE列の値をBIGINT列へINSERT ... SELECTした場合を確認します。

CREATE TABLE src (d DATE);
CREATE TABLE dst (n BIGINT);

INSERT INTO src VALUES ('2026-08-23');
INSERT INTO dst SELECT d FROM src;

SELECT n FROM dst;

結果は以下のようになりました。

mysql8.4.11> SELECT n FROM dst;
+----------------+
| n              |
+----------------+
| 20260823000000 |
+----------------+

mysql9.7.2> SELECT n FROM dst;
+----------+
| n        |
+----------+
| 20260823 |
+----------+

8.4.11では20260823000000、9.7.2では20260823です。8.4.11では、DATE値をINTEGER列へコピーする際にDATETIMEへ拡張され、時刻部分の00:00:00が付加されてから数値化されていました。9.7.2では、このINSERT ... SELECTでDATE値がYYYYMMDD形式の数値として代入されます。日付を数値形式で外部システムへ連携している場合、これは比較的影響が出やすそうです。特に14桁の値を前提にしているCSV出力やETL処理がある場合は、アップグレード前に確認しておいた方がよいでしょう。

必要な形式が決まっているなら、暗黙変換に任せない方が安全です。8桁のYYYYMMDDが必要な場合はCAST(d AS UNSIGNED)、14桁のYYYYMMDD000000が必要な場合はCAST(DATE_FORMAT(d, '%Y%m%d000000') AS UNSIGNED)のように明示できます。

補足⁠DAYNAME()を数値演算に入れた場合

DAYNAME()を数値演算に入れた場合も確認してみます。これはあまり実用的な式ではありませんが、9.7で修正された挙動の一つです。

mysql8.4.11> SELECT
    ->   DAYNAME('2026-08-23') AS day_name,
    ->   DAYNAME('2026-08-23') + 0 AS dayname_plus_zero,
    ->   WEEKDAY('2026-08-23') AS weekday;
+----------+-------------------+---------+
| day_name | dayname_plus_zero | weekday |
+----------+-------------------+---------+
| Sunday   |                 6 |       6 |
+----------+-------------------+---------+

mysql9.7.2> SELECT
    ->   DAYNAME('2026-08-23') AS day_name,
    ->   DAYNAME('2026-08-23') + 0 AS dayname_plus_zero,
    ->   WEEKDAY('2026-08-23') AS weekday;
+----------+-------------------+---------+
| day_name | dayname_plus_zero | weekday |
+----------+-------------------+---------+
| Sunday   |                 0 |       6 |
+----------+-------------------+---------+

なお、今回の検証環境ではlc_time_namesがen_USのため、DAYNAME('2026-08-23')はSundayを返します。DAYNAME()単体は、どちらのバージョンでも曜日名の Sunday を返しています。しかし8.4.11では、DAYNAME()を数値演算に含めるとWEEKDAY()相当の値として評価され、日曜日を表す6が返ります。9.7.2では曜日名の文字列を数値へ変換するため、0が返ります。

曜日番号が必要な場合は、DAYNAME()を数値へ変換するのではなく、最初からWEEKDAY()またはDAYOFWEEK()を利用すべきでしょう。なお、WEEKDAY()は月曜日を0、DAYOFWEEK()は日曜日を1として数えます。

まとめ

今回は、MySQL 8.4.11と9.7.2でDATE型に関する挙動を比較してみました。

0年の日付演算やFROM_DAYS()、DATETIMEとDATEを混ぜたTIMEDIFF()は、9.7でより自然な結果を返すようになっています。一方で、DATEからBIGINTへの暗黙変換は、既存の連携処理に影響する可能性があります。WL#16895は内部実装のリファクタリングですが、SQLから見える結果にもきちんと差がありました。バージョンアップ時には、新機能だけでなく、今回のような細かな型変換や関数の結果も確認しておきたいところです。

参考資料

おすすめ記事

記事・ニュース一覧