DATE_FORMATとは、MySQLの日付・日時の値を、指定した書式の文字列に変換する関数です。 DATE_FORMAT(日付, '書式') の形で書き、%Y や %m などの書式指定子を組み合わせて、2026/10/03 や 2026年10月3日(土) といった見た目を自由に作れます。

この記事では、書式指定子を全件「指定子・意味・出力例」の表にまとめ、よく使う形式はコピペ用SQLと実行結果を並べました。 さらに、文字列を日付に戻す STR_TO_DATE で NULL が返る例と警告文、lc_time_names による曜日の日本語化、WHERE句で DATE_FORMAT を使ったときのインデックスの動きを EXPLAIN で確かめています。掲載した出力はすべて MySQL 8.0.46(Ubuntu 24.04、sql_mode は初期値)で実際に実行した結果です。8.4 や 9.x では実行していないため、該当バージョンで使う場合は手元で一度確認してください。

DATE_FORMATの早見表

まずは「これだけ見れば用が足りる」表です。基準の日時は 2026-10-03 09:05:07(土曜日)です。

やりたいこと SQL 結果
YYYY/MM/DD DATE_FORMAT(d, '%Y/%m/%d') 2026/10/03
YYYY-MM-DD DATE_FORMAT(d, '%Y-%m-%d') 2026-10-03
YYYYMMDD(区切りなし) DATE_FORMAT(d, '%Y%m%d') 20261003
年月(YYYY-MM) DATE_FORMAT(d, '%Y-%m') 2026-10
月初の日付 DATE_FORMAT(d, '%Y-%m-01') 2026-10-01
和風(ゼロ埋めあり) DATE_FORMAT(d, '%Y年%m月%d日') 2026年10月03日
和風(ゼロ埋めなし) DATE_FORMAT(d, '%Y年%c月%e日') 2026年10月3日
時:分 DATE_FORMAT(d, '%H:%i') 09:05
日時(秒まで) DATE_FORMAT(d, '%Y/%m/%d %H:%i:%s') 2026/10/03 09:05:07
ISO 8601風 DATE_FORMAT(d, '%Y-%m-%dT%H:%i:%s') 2026-10-03T09:05:07
月/日(ゼロ埋めなし) DATE_FORMAT(d, '%c/%e') 10/3
14桁の連結 DATE_FORMAT(d, '%Y%m%d%H%i%s') 20261003090507
曜日つき(日本語) SET lc_time_names='ja_JP'; のあと '%Y年%c月%e日(%a)' 2026年10月3日(土)
文字列 → 日付(逆変換) STR_TO_DATE('2026/10/03', '%Y/%m/%d') 2026-10-03

覚えておきたいのは「分は %i、月は %m」の1点です。 %m を分のつもりで書くと月が出力されます(後半で実際の結果を載せています)。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「MySQL DATE_FORMATの使い方と書式一覧|文字列⇔日付変換」

DATE_FORMATの基本の書き方

構文と引数

DATE_FORMAT(date, format)
引数 内容 例
date DATE、DATETIME、TIMESTAMP型の列や値。日付として読める文字列・数値も渡せる ordered_at、NOW()、'2026-10-03'
format 書式指定子(% で始まる記号)と、そのまま出力したい文字を並べた文字列 '%Y/%m/%d'

戻り値は文字列です。 日付型ではなくなるので、並べ替えや比較に使うときは注意点があります(「つまずきやすい点」で扱います)。どちらかの引数が NULL のときは NULL が返ります。

いちばん小さな例

SELECT DATE_FORMAT('2026-10-03 09:05:07', '%Y/%m/%d') AS d;
+------------+
| d          |
+------------+
| 2026/10/03 |
+------------+

現在日時を整形するなら NOW() を渡します。

SELECT DATE_FORMAT(NOW(), '%Y/%m/%d %H:%i') AS now_text;

テーブルの列に使う例

以降の例で使うテーブルを用意します。そのまま貼って試せます。

CREATE TABLE orders (
  id INT AUTO_INCREMENT PRIMARY KEY,
  ordered_at DATETIME NOT NULL,
  amount INT NOT NULL,
  KEY idx_ordered_at (ordered_at)
);

-- 2026-01-01 から4分おきに10万件の注文データを作る
SET SESSION cte_max_recursion_depth = 200000;
INSERT INTO orders (ordered_at, amount)
WITH RECURSIVE seq(n) AS (
  SELECT 0 UNION ALL SELECT n + 1 FROM seq WHERE n < 99999
)
SELECT '2026-01-01 00:00:00' + INTERVAL n * 4 MINUTE,
       500 + (n * 37 % 4500)
FROM seq;
ANALYZE TABLE orders;
SET lc_time_names = 'ja_JP';
SELECT id, DATE_FORMAT(ordered_at, '%Y/%m/%d(%a) %H:%i') AS shown
FROM orders
ORDER BY ordered_at DESC
LIMIT 3;
+--------+-----------------------+
| id     | shown                 |
+--------+-----------------------+
| 100000 | 2026/10/05(月) 18:36  |
|  99999 | 2026/10/05(月) 18:32  |
|  99998 | 2026/10/05(月) 18:28  |
+--------+-----------------------+

並べ替えは元の列(ordered_at)で行い、DATE_FORMAT は表示用の列にだけ使うのが基本の形です。

書式指定子の一覧(全32種+その他)

MySQL 8.0 リファレンスマニュアルの DATE_FORMAT の表に載っている指定子を、今回すべて実行して出力を確かめました。基準は 2026-10-03 09:05:07.123456(土曜日)、lc_time_names は初期値の en_US です。

SET @d = '2026-10-03 09:05:07.123456';
SELECT DATE_FORMAT(@d, '%a');  -- 以下、指定子を1つずつ入れ替えて実行

年・月・日

指定子 意味 出力例
%Y 年(4桁) 2026
%y 年(下2桁) 26
%m 月(2桁、00〜12) 10
%c 月(ゼロ埋めなし、0〜12) 10
%M 月の名前(January〜December) October
%b 月の名前の省略形(Jan〜Dec) Oct
%d 日(2桁、00〜31) 03
%e 日(ゼロ埋めなし、0〜31) 3
%D 英語の序数つきの日(1st、2nd、3rd …) 3rd
%j 年の通算日(001〜366) 276

時・分・秒

指定子 意味 出力例
%H 時(24時間、2桁、00〜23) 09
%k 時(24時間、ゼロ埋めなし、0〜23) 9
%h 時(12時間、2桁、01〜12) 09
%I 時(12時間、2桁、01〜12)。%h と同じ 09
%l 時(12時間、ゼロ埋めなし、1〜12)。小文字のエル 9
%i 分(2桁、00〜59) 05
%S 秒(2桁、00〜59) 07
%s 秒(2桁、00〜59)。%S と同じ 07
%f マイクロ秒(6桁、000000〜999999) 123456
%p AM または PM AM
%r 12時間表記の時刻(hh:mm:ss AM/PM) 09:05:07 AM
%T 24時間表記の時刻(hh:mm:ss) 09:05:07

曜日・週

指定子 意味 出力例
%W 曜日の名前(Sunday〜Saturday) Saturday
%a 曜日の名前の省略形(Sun〜Sat) Sat
%w 曜日の番号(0=日曜 〜 6=土曜) 6
%U 週番号(00〜53、日曜始まり、WEEK() のモード0) 39
%u 週番号(00〜53、月曜始まり、WEEK() のモード1) 40
%V 週番号(01〜53、日曜始まり、モード2)。%X と組み合わせる 39
%v 週番号(01〜53、月曜始まり、モード3)。%x と組み合わせる 40
%X 週に対応する年(日曜始まり、4桁)。%V と組み合わせる 2026
%x 週に対応する年(月曜始まり、4桁)。%v と組み合わせる 2026

その他

指定子 意味 出力例
%% パーセント記号そのもの %
% に上記以外の文字を続けたもの その文字がそのまま出力される(例:%z) z

大文字と小文字で意味が変わる点が、この一覧でいちばん大切なところです。迷いやすい組み合わせを抜き出します。

似ている指定子 違い
%m と %M %m は 10、%M は October
%d と %D %d は 03、%D は 3rd
%m と %i %m は月、%i が分
%H と %h %H は24時間、%h は12時間
%Y と %y %Y は4桁、%y は2桁
%m と %c、%d と %e 前者はゼロ埋めあり、後者はゼロ埋めなし

よく使う形式のコピペ用SQLと実行結果

@d を列名や NOW() に置き換えれば、そのまま使えます。

SET @d = '2026-10-03 09:05:07.123456';

SELECT
  DATE_FORMAT(@d, '%Y/%m/%d')            AS a,
  DATE_FORMAT(@d, '%Y-%m-%d')            AS b,
  DATE_FORMAT(@d, '%Y%m%d')              AS c,
  DATE_FORMAT(@d, '%Y-%m')               AS d,
  DATE_FORMAT(@d, '%Y年%m月%d日')        AS e,
  DATE_FORMAT(@d, '%Y年%c月%e日')        AS f,
  DATE_FORMAT(@d, '%H:%i')               AS g,
  DATE_FORMAT(@d, '%Y/%m/%d %H:%i:%s')   AS h,
  DATE_FORMAT(@d, '%Y-%m-%dT%H:%i:%s')   AS i,
  DATE_FORMAT(@d, '%c/%e')               AS j,
  DATE_FORMAT(@d, '%Y%m%d%H%i%s')        AS k,
  DATE_FORMAT(@d, '%Y-%m-01')            AS l,
  DATE_FORMAT(@d, '%p %l:%i')            AS m,
  DATE_FORMAT(@d, '%H:%i:%s.%f')         AS n\G
*************************** 1. row ***************************
a: 2026/10/03
b: 2026-10-03
c: 20261003
d: 2026-10
e: 2026年10月03日
f: 2026年10月3日
g: 09:05
h: 2026/10/03 09:05:07
i: 2026-10-03T09:05:07
j: 10/3
k: 20261003090507
l: 2026-10-01
m: AM 9:05
n: 09:05:07.123456

和暦(令和)で表示する

DATE_FORMAT には和暦の指定子がありません。令和の年は西暦から 2018 を引いて作れます(2019年5月1日以降の日付が対象です)。

SELECT CONCAT('令和', YEAR(@d) - 2018, '年', DATE_FORMAT(@d, '%c月%e日')) AS wareki;
+---------------------+
| wareki              |
+---------------------+
| 令和8年10月3日      |
+---------------------+

月ごとに集計する(GROUP BY)

年月の文字列を作って GROUP BY に使う形は、集計でよく登場します。

SELECT DATE_FORMAT(ordered_at, '%Y-%m') AS ym,
       COUNT(*) AS cnt,
       SUM(amount) AS total
FROM orders
GROUP BY ym
ORDER BY ym
LIMIT 4;
+---------+-------+----------+
| ym      | cnt   | total    |
+---------+-------+----------+
| 2026-01 | 11160 | 30631140 |
| 2026-02 | 10080 | 27704520 |
| 2026-03 | 11160 | 30682440 |
| 2026-04 | 10800 | 29716200 |
+---------+-------+----------+

MySQL では SELECT で付けた別名(ym)を GROUP BY に書けます。GROUP BY DATE_FORMAT(ordered_at, '%Y-%m') と式をそのまま書いても同じ結果になることを確かめました。

時刻だけの値にはTIME_FORMAT

TIME型の値や '09:05:07' のような時刻だけの文字列には、TIME_FORMAT を使います。使えるのは時・分・秒・マイクロ秒の指定子です。

SELECT TIME_FORMAT('09:05:07', '%H時%i分') AS t;
+------------+
| t          |
+------------+
| 09時05分   |
+------------+

DATE_FORMAT に '09:05:07' を渡すと、時刻としては読まれず 00:00 が返りました(後述の表を参照)。

GET_FORMATで定型の書式を取り出す

GET_FORMAT は、地域ごとの定型の書式文字列を返す関数です。DATE_FORMAT や STR_TO_DATE の第2引数に渡せます。

SELECT GET_FORMAT(DATE, 'ISO')     AS g1,
       GET_FORMAT(DATETIME, 'JIS') AS g2,
       GET_FORMAT(DATE, 'USA')     AS g3,
       GET_FORMAT(DATE, 'EUR')     AS g4,
       DATE_FORMAT('2026-10-03', GET_FORMAT(DATE, 'USA')) AS u;
+----------+-------------------+----------+----------+------------+
| g1       | g2                | g3       | g4       | u          |
+----------+-------------------+----------+----------+------------+
| %Y-%m-%d | %Y-%m-%d %H:%i:%s | %m.%d.%Y | %d.%m.%Y | 10.03.2026 |
+----------+-------------------+----------+----------+------------+

タイムゾーンを変換してから整形する

UTC で保存した日時を日本時間で表示するときは、CONVERT_TZ で変換してから DATE_FORMAT に渡します。オフセット(+09:00)で指定する形なら、タイムゾーンのテーブルを読み込んでいない環境でも動きます。

SELECT DATE_FORMAT(
         CONVERT_TZ('2026-10-03 00:05:07', '+00:00', '+09:00'),
         '%Y/%m/%d %H:%i') AS jst;
+------------------+
| jst              |
+------------------+
| 2026/10/03 09:05 |
+------------------+

曜日を日本語にする(lc_time_names)

%a や %W は、初期設定では英語(Sat、Saturday)で出力されます。システム変数 lc_time_names を ja_JP にすると、曜日と月の名前が日本語になります。

設定前(en_US)

SELECT @@lc_time_names;
SELECT DATE_FORMAT(@d, '%Y年%c月%e日(%a)') AS a,
       DATE_FORMAT(@d, '%W %M') AS b;
+-----------------+
| @@lc_time_names |
+-----------------+
| en_US           |
+-----------------+
+-----------------------+------------------+
| a                     | b                |
+-----------------------+------------------+
| 2026年10月3日(Sat)    | Saturday October |
+-----------------------+------------------+

設定後(ja_JP)

SET lc_time_names = 'ja_JP';
SELECT DATE_FORMAT(@d, '%Y年%c月%e日(%a)') AS a,
       DATE_FORMAT(@d, '%W / %M / %b / %p / %D') AS b,
       DAYNAME(@d) AS c,
       MONTHNAME(@d) AS e;
+-----------------------+--------------------------------------+-----------+-------+
| a                     | b                                    | c         | e     |
+-----------------------+--------------------------------------+-----------+-------+
| 2026年10月3日(土)     | 土曜日 / 10月 / 10月 / AM / 3rd      | 土曜日    | 10月  |
+-----------------------+--------------------------------------+-----------+-------+

1週間分を並べると次のとおりです(2026-09-28 が月曜日)。

指定子 月 火 水 木 金 土 日
%a 月 火 水 木 金 土 日
%W 月曜日 火曜日 水曜日 木曜日 金曜日 土曜日 日曜日

この結果からわかることを整理します。

項目 ja_JP での動き
%a 「土」のように1文字になる
%W、DAYNAME() 「土曜日」になる
%M、%b、MONTHNAME() 「10月」になる
%p AM / PM のまま(午前・午後にはならない)
%D 3rd のまま(英語の序数)

設定の範囲を選ぶ

書き方 効く範囲
SET lc_time_names = 'ja_JP'; いまの接続(セッション)だけ
SET GLOBAL lc_time_names = 'ja_JP'; これ以降の新しい接続すべて(サーバー再起動で元に戻る。権限が必要)
設定ファイルの [mysqld] に lc_time_names=ja_JP サーバー起動時から有効

今回実行して確かめたのはセッション単位の SET です。GLOBAL と設定ファイルは未実行で、一般に知られた仕様として載せています。アプリケーションから使う場合は、接続直後にセッション単位で SET するのが影響範囲が小さく扱いやすい方法です。

設定を変えずに曜日を日本語にする

サーバーや接続の設定に左右されない書き方もあります。WEEKDAY() は月曜を0とする番号を返すので、ELT() で文字に置き換えます。

SET lc_time_names = 'en_US';
SELECT CONCAT(
         DATE_FORMAT(@d, '%Y年%c月%e日('),
         ELT(WEEKDAY(@d) + 1, '月', '火', '水', '木', '金', '土', '日'),
         ')') AS a;
+-----------------------+
| a                     |
+-----------------------+
| 2026年10月3日(土)     |
+-----------------------+

午前・午後を日本語にしたいときも、同じ考え方で書けます。CONCAT(IF(HOUR(d) < 12, '午前', '午後'), DATE_FORMAT(d, '%l時%i分')) に 13時5分の値を渡すと、「午後1時05分」が返りました。

文字列を日付に変換する(STR_TO_DATE)

STR_TO_DATE は DATE_FORMAT の逆で、書式に沿って文字列を読み取り、日付型の値を返します。 使う指定子は DATE_FORMAT と同じです。

STR_TO_DATE(str, format)

コピペ用SQLと実行結果

SELECT
  STR_TO_DATE('2026/10/03', '%Y/%m/%d')                    AS a,
  STR_TO_DATE('20261003', '%Y%m%d')                        AS b,
  STR_TO_DATE('2026年10月3日', '%Y年%c月%e日')             AS c,
  STR_TO_DATE('2026-10-03 09:05:07', '%Y-%m-%d %H:%i:%s')  AS d,
  STR_TO_DATE('09:05', '%H:%i')                            AS e,
  STR_TO_DATE('03/10/2026', '%d/%m/%Y')                    AS f,
  STR_TO_DATE('Oct 3, 2026', '%b %e, %Y')                  AS g\G
*************************** 1. row ***************************
a: 2026-10-03
b: 2026-10-03
c: 2026-10-03
d: 2026-10-03 09:05:07
e: 09:05:00
f: 2026-10-03
g: 2026-10-03

戻り値の型は、書式に含まれる部品で決まります。

書式に含まれるもの 戻り値の型 例
日付の部品だけ DATE 2026-10-03
時刻の部品だけ TIME 09:05:00
日付と時刻の両方 DATETIME 2026-10-03 09:05:07

書式が合わないとNULLが返る(警告つきで再現)

STR_TO_DATE でいちばん多いつまずきは、文字列と書式の区切り文字が合っていないケースです。 エラーで止まらず、NULL と警告が返ります。

SELECT STR_TO_DATE('2026-10-03', '%Y/%m/%d') AS a;
SHOW WARNINGS;
+------+
| a    |
+------+
| NULL |
+------+
+---------+------+-----------------------------------------------------------------+
| Level   | Code | Message                                                         |
+---------+------+-----------------------------------------------------------------+
| Warning | 1411 | Incorrect datetime value: '2026-10-03' for function str_to_date |
+---------+------+-----------------------------------------------------------------+

文字列はハイフン区切りなのに、書式はスラッシュ区切りになっています。書式を '%Y-%m-%d' に合わせると 2026-10-03 が返ります。NULL が返ったら、まず SHOW WARNINGS を実行して Code 1411 が出ていないか確認するのが近道です。

ほかのパターンも実行して確かめました。

入力 書式 結果 警告
'2026-10-03' '%Y/%m/%d' NULL 1411 Incorrect datetime value
'2026/13/03'(13月) '%Y/%m/%d' NULL 1411
'2026/02/30'(存在しない日) '%Y/%m/%d' NULL 1411
''(空文字) '%Y/%m/%d' NULL 1411
NULL '%Y/%m/%d' NULL なし
'2026-10'(日がない) '%Y-%m' NULL 1411
'2026/10/03 extra'(後ろに余分な文字) '%Y/%m/%d' 2026-10-03 1292 Truncated incorrect date value
'2026/10/03'(時刻がない) '%Y/%m/%d %H:%i' 2026-10-03 00:00:00 なし

ポイントは3つです。

  • 区切り文字や順番が違う、範囲外の値、存在しない日付は NULL になります。
  • 後ろに余分な文字があっても、前半が書式に合えば変換されます(警告 1292 が出ます)。
  • 年月だけの文字列は、初期設定では NULL になります。日が 0 の日付を認めない NO_ZERO_IN_DATE が sql_mode に入っているためです。sql_mode = '' に切り替えて実行すると 2026-10-00 が返りました。年月から日付を作りたいときは、STR_TO_DATE(CONCAT('2026-10', '-01'), '%Y-%m-%d') のように日を補うと扱いやすくなります。この式は初期設定の sql_mode で 2026-10-01 を返しました。

なお、MySQL 8.0 リファレンスマニュアルには「8.0.35 以降の STR_TO_DATE は範囲チェックを完全に行う」という記載があります。今回の 8.0.46 でも '2026/02/30' は NULL になりました。

INSERT・UPDATEではエラーで止まる

SELECT では NULL と警告で済んだ変換も、厳密モード(STRICT_TRANS_TABLES。初期値で有効)のもとで INSERT や UPDATE に使うと、エラーになって文が止まります。

CREATE TABLE imp (id INT PRIMARY KEY, s VARCHAR(20), d DATE);
INSERT INTO imp (id, s) VALUES (1, '2026/10/03'), (2, '2026-10-04');

UPDATE imp SET d = STR_TO_DATE(s, '%Y/%m/%d') WHERE id = 1;  -- 成功
UPDATE imp SET d = STR_TO_DATE(s, '%Y/%m/%d') WHERE id = 2;  -- 書式が合わない
ERROR 1411 (HY000): Incorrect datetime value: '2026-10-04' for function str_to_date
SELECT * FROM imp;
+----+------------+------------+
| id | s          | d          |
+----+------------+------------+
|  1 | 2026/10/03 | 2026-10-03 |
|  2 | 2026-10-04 | NULL       |
+----+------------+------------+

id=2 は更新されず、d は NULL のまま残りました。sql_mode = '' に切り替えて同じ UPDATE を実行すると、エラーではなく警告 1411 になり、d には NULL が入りました。取り込み処理では、エラーで止まってくれる厳密モードのほうが、気づかないまま NULL が増えるのを防げます。

複数の書式が混ざった列を取り込む

スラッシュ区切りとハイフン区切りが混ざった列は、COALESCE で順に試すと1つの式で変換できます。COALESCE は、NULL でない最初の値を返す関数です。

SELECT id, s,
       COALESCE(STR_TO_DATE(s, '%Y/%m/%d'),
                STR_TO_DATE(s, '%Y-%m-%d')) AS d2
FROM imp;
+----+------------+------------+
| id | s          | d2         |
+----+------------+------------+
|  1 | 2026/10/03 | 2026-10-03 |
|  2 | 2026-10-04 | 2026-10-04 |
+----+------------+------------+

1つ目の書式に合わなかった行については警告 1411 が出ますが、結果は意図どおりです。

CASTやDATE()との使い分け

文字列を日付にする方法は STR_TO_DATE だけではありません。

SELECT CAST('2026/10/03' AS DATE)   AS a,
       DATE('2026-10-03 09:05:07') AS b,
       CAST('20261003' AS DATE)    AS c,
       CAST('10/03/2026' AS DATE)  AS d;
SHOW WARNINGS;
+------------+------------+------------+------+
| a          | b          | c          | d    |
+------------+------------+------------+------+
| 2026-10-03 | 2026-10-03 | 2026-10-03 | NULL |
+------------+------------+------------+------+
+---------+------+-------------------------------------------------------------------------------------------------------------+
| Level   | Code | Message                                                                                                     |
+---------+------+-------------------------------------------------------------------------------------------------------------+
| Warning | 4095 | Delimiter '/' in position 4 in datetime value '2026/10/03' at row 1 is deprecated. Prefer the standard '-'. |
| Warning | 1292 | Incorrect datetime value: '10/03/2026'                                                                      |
+---------+------+-------------------------------------------------------------------------------------------------------------+
方法 向いている場面 今回の結果からわかること
STR_TO_DATE(s, 書式) 年月日以外の並び、和風表記、英語の月名など 書式を自分で決められる
CAST(s AS DATE) 年→月→日の順に並んだ文字列 スラッシュ区切りは変換できるが、非推奨の警告 4095 が出る
DATE(s) 日時から日付の部分だけ取り出す '2026-10-03 09:05:07' → 2026-10-03

年月日の順ではない文字列(月/日/年 など)は CAST では NULL になるため、STR_TO_DATE で書式を指定します。

WHERE句で使うとインデックスが効かなくなる(EXPLAINで確認)

DATE_FORMAT は表示用の整形には便利ですが、WHERE句で列を DATE_FORMAT に通すと、その列のインデックスを使った範囲検索ができなくなります。 冒頭で作った orders テーブル(10万件、ordered_at にインデックスあり)で確かめます。

DATE_FORMATで絞り込む書き方

EXPLAIN SELECT id, amount FROM orders
WHERE DATE_FORMAT(ordered_at, '%Y-%m-%d') = '2026-03-15';
+----+-------------+--------+------+---------------+------+---------+------+--------+----------+-------------+
| id | select_type | table  | type | possible_keys | key  | key_len | ref  | rows   | filtered | Extra       |
+----+-------------+--------+------+---------------+------+---------+------+--------+----------+-------------+
|  1 | SIMPLE      | orders | ALL  | NULL          | NULL | NULL    | NULL | 100256 |   100.00 | Using where |
+----+-------------+--------+------+---------------+------+---------+------+--------+----------+-------------+

(partitions 列は幅の都合で省いています。)type が ALL、key が NULL です。テーブル全体を読んで、1行ずつ DATE_FORMAT を計算してから比較しています。

範囲で絞り込む書き方

EXPLAIN SELECT id, amount FROM orders
WHERE ordered_at >= '2026-03-15' AND ordered_at < '2026-03-16';
+----+-------------+--------+-------+----------------+----------------+---------+------+------+----------+-----------------------+
| id | select_type | table  | type  | possible_keys  | key            | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+--------+-------+----------------+----------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | orders | range | idx_ordered_at | idx_ordered_at | 5       | NULL |  360 |   100.00 | Using index condition |
+----+-------------+--------+-------+----------------+----------------+---------+------+------+----------+-----------------------+

type が range、key が idx_ordered_at になり、読む行数の見積もりが 100256 から 360 に減りました。

EXPLAIN ANALYZEで実際の時間を比べる

-- DATE_FORMAT で絞り込み
-> Filter: (date_format(orders.ordered_at,'%Y-%m-%d') = '2026-03-15')  (cost=10082 rows=100256) (actual time=15.3..54.5 rows=360 loops=1)
    -> Table scan on orders  (cost=10082 rows=100256) (actual time=0.964..24.6 rows=100000 loops=1)

-- 範囲で絞り込み
-> Index range scan on orders using idx_ordered_at over ('2026-03-15 00:00:00' <= ordered_at < '2026-03-16 00:00:00'), with index condition: (...)  (cost=162 rows=360) (actual time=0.388..0.462 rows=360 loops=1)
書き方 読んだ行数 実際の時間(今回の環境)
DATE_FORMAT(ordered_at, …) = '2026-03-15' 100,000行 約54.5ミリ秒
ordered_at >= … AND ordered_at < … 360行 約0.46ミリ秒

どちらも返る行は同じ360行です。時間は環境やデータ量で変わりますが、読む行数が約280分の1になる点は変わりません。

月単位でも同じ

EXPLAIN SELECT COUNT(*) FROM orders
WHERE DATE_FORMAT(ordered_at, '%Y-%m') = '2026-03';

この場合、type は index(インデックス全体を端から端まで読む)、rows は 100256 でした。インデックスの名前は表示されますが、絞り込みには使われていません。範囲に書き換えると type は range、rows は 20084 になり、COUNT(*) の結果はどちらも 11160 で一致しました。

書き換えの対応表

やりたいこと インデックスを使いにくい書き方 インデックスを使える書き方
ある1日 DATE_FORMAT(col, '%Y-%m-%d') = '2026-03-15' col >= '2026-03-15' AND col < '2026-03-16'
ある月 DATE_FORMAT(col, '%Y-%m') = '2026-03' col >= '2026-03-01' AND col < '2026-04-01'
ある年 DATE_FORMAT(col, '%Y') = '2026' col >= '2026-01-01' AND col < '2027-01-01'
今日 DATE_FORMAT(col, '%Y-%m-%d') = CURDATE() col >= CURDATE() AND col < CURDATE() + INTERVAL 1 DAY

終わり側を「翌日(翌月)の0時より前」と < で書くのがコツです。 BETWEEN '2026-03-01' AND '2026-03-31' と書くと、DATETIME列では3月31日の0時0分0秒より後の行が含まれなくなります。

どうしても式で検索したいとき

「年月の文字列で検索する画面が多い」といった場合は、式の結果そのものにインデックスを付ける方法があります。今回は2つを実行して確かめました。

-- 方法1:関数インデックス(MySQL 8.0.13以降)
ALTER TABLE orders ADD INDEX idx_d ((CAST(ordered_at AS DATE)));
EXPLAIN SELECT id, amount FROM orders WHERE CAST(ordered_at AS DATE) = '2026-03-15';
-- → type: ref, key: idx_d, rows: 360

-- 方法2:生成列+インデックス
ALTER TABLE orders
  ADD COLUMN ordered_ym CHAR(7) AS (DATE_FORMAT(ordered_at, '%Y-%m')) STORED,
  ADD INDEX idx_ym2 (ordered_ym);
EXPLAIN SELECT COUNT(*) FROM orders WHERE ordered_ym = '2026-03';
-- → type: ref, key: idx_ym2

方法2を入れたあとは、WHERE DATE_FORMAT(ordered_at, '%Y-%m') = '2026-03' と式のまま書いたクエリでも key: idx_ym2 が選ばれました。式と一致する生成列のインデックスを、オプティマイザが見つけてくれるためです。ただし、インデックスが増えるぶん書き込みは少し重くなります。まずは範囲条件への書き換えを検討し、それで足りないときに選ぶ、という順番がおすすめです。

つまずきやすい点と直し方

実際に実行して、意図と違う結果になった例をまとめます。

SELECT DATE_FORMAT(NULL, '%Y')                      AS a,
       DATE_FORMAT('2026-10-03', '%H:%i:%s')        AS b,
       DATE_FORMAT('2026-02-30', '%Y/%m/%d')        AS c,
       DATE_FORMAT('09:05:07', '%H:%i')             AS d,
       DATE_FORMAT(20261003, '%Y/%m/%d')            AS e,
       DATE_FORMAT(@d, '%Y/%M/%D')                  AS f,
       DATE_FORMAT(@d, '%Y/%m/%d %h:%m')            AS g\G
*************************** 1. row ***************************
a: NULL
b: 00:00:00
c: NULL
d: 00:00
e: 2026/10/03
f: 2026/October/3rd
g: 2026/10/03 09:10
列 何が起きたか こうするとよい
a 引数が NULL なので NULL COALESCE(DATE_FORMAT(col, …), '未設定') や IFNULL で表示を決める
b 日付だけの値に時刻の指定子を使うと 00:00:00 時刻が必要な列は DATETIME型にする
c 存在しない日付(2月30日)は NULL。警告 1292 が出る 入力時点で日付として正しいか確かめる
d 時刻だけの文字列は DATE_FORMAT では読まれず 00:00。警告 4095 が出る TIME_FORMAT を使う
e 数値の 20261003 も日付として読まれる そのまま使える
f %M と %D を大文字にすると October、3rd 数字にしたいときは小文字の %m、%d
g 分のつもりで %m と書くと月(10)が出る。正しくは 09:05 分は %i

文字列になったあとは日付として比較されない

DATE_FORMAT の結果は文字列なので、大小比較は文字の順で行われます。

SELECT DATE_FORMAT('2026-10-03', '%Y/%m/%d') > '2026/9/30' AS cmp_str;
+---------+
| cmp_str |
+---------+
|       0 |
+---------+

10月3日は9月30日より後ですが、文字として比べると '1' は '9' より前に並ぶため、結果は 0(偽)になりました。比較と並べ替えは元の日付列で行い、DATE_FORMAT は SELECT の表示列だけに使うと、この問題は起きません。

同じ理由で、'%m/%d' のように年を含まない書式で ORDER BY すると、年をまたぐデータの順番が入れ替わります。2026-12-31 と 2027-01-01 をこの書式で並べ替えると、01/01、12/31 の順になりました。

戻り値の文字コード

SELECT CHARSET(DATE_FORMAT(NOW(), '%Y')) AS cs;
-- → utf8mb4

DATE_FORMAT の戻り値は、接続の文字コード(character_set_connection)になります。和風表記の「年」「月」「日」が文字化けする場合は、接続の文字コードが utf8mb4 になっているかを確認します。mysql コマンドなら --default-character-set=utf8mb4 を付けて接続します。

年・月・日を数値で取り出したいとき

整形ではなく数値が欲しいときは、専用の関数のほうが短く書けます。

SELECT YEAR('2026-10-03') AS y, MONTH('2026-10-03') AS m,
       DAY('2026-10-03') AS dd, LAST_DAY('2026-10-03') AS ld;
+------+------+------+------------+
| y    | m    | dd   | ld         |
+------+------+------+------------+
| 2026 |   10 |    3 | 2026-10-31 |
+------+------+------+------------+

どの関数を使うかの判断表

目的 使う関数 補足
日付を好きな見た目の文字列にする DATE_FORMAT この記事の中心
時刻だけを整形する TIME_FORMAT 時・分・秒の指定子だけ使える
文字列を日付にする(書式を指定) STR_TO_DATE 書式が合わないと NULL+警告 1411
年→月→日の文字列を日付にする CAST(… AS DATE) ハイフン区切りが標準
日時から日付だけ取り出す DATE() 戻り値は DATE型
年・月・日を数値で取り出す YEAR()、MONTH()、DAY() 計算や条件に向く
月末の日付を得る LAST_DAY() 2026-10-31 など
期間で絞り込む 範囲条件(>= と <) インデックスを使える
月ごとに集計する GROUP BY DATE_FORMAT(col, '%Y-%m') 絞り込みは範囲条件と組み合わせる

確認用チェックリスト

  • [ ] 分は %i、月は %m になっているか
  • [ ] 24時間表記は %H、12時間表記は %h と %p の組み合わせになっているか
  • [ ] ゼロ埋めの有無(%m と %c、%d と %e)は意図どおりか
  • [ ] 曜日を日本語にするなら、接続ごとに SET lc_time_names = 'ja_JP' を実行しているか
  • [ ] WHERE句で列を DATE_FORMAT に通していないか(範囲条件に書き換えられないか)
  • [ ] ORDER BY と比較は元の日付列で行っているか
  • [ ] STR_TO_DATE の書式は、区切り文字まで入力と一致しているか
  • [ ] NULL が返ったら SHOW WARNINGS を確認したか

よくある質問

Q. DATE_FORMATで「分」を出すには、どの指定子を使いますか? A. %i です。%m は月を表すため、'%H:%m' と書くと「時:月」になります。今回の実行でも、9時5分の値に '%h:%m' を指定すると 09:10(10は月)が返りました。

Q. 曜日を日本語で出すにはどうすればよいですか? A. SET lc_time_names = 'ja_JP'; を実行してから %a(土)や %W(土曜日)を使います。設定を変えたくない場合は、ELT(WEEKDAY(d) + 1, '月', '火', '水', '木', '金', '土', '日') で同じ表示を作れます。どちらも MySQL 8.0.46 で実行して確かめました。

Q. STR_TO_DATEがNULLを返します。原因は何ですか? A. 文字列と書式が合っていないケースがほとんどです。区切り文字(/ と -)、年月日の順番、存在しない日付(2月30日など)を見直してください。直後に SHOW WARNINGS; を実行すると、Code 1411 の「Incorrect datetime value: … for function str_to_date」という警告で、どの値が読めなかったかがわかります。

Q. WHERE句でDATE_FORMATを使ってもよいですか? A. 動作はしますが、列を関数に通すとインデックスによる範囲検索が使われなくなります。今回の10万件のテーブルでは、読む行数が 100,000行と360行という差になりました。col >= '2026-03-15' AND col < '2026-03-16' のような範囲条件に書き換えるのがおすすめです。

まとめ

  • DATE_FORMAT(日付, '書式') は、日付を好きな見た目の文字列に変換する関数。戻り値は文字列
  • よく使う指定子は %Y(年)%m(月)%d(日)%H(時)%i(分)%s(秒)。分は %i
  • ゼロ埋めなしは %c(月)と %e(日)。和風表記は '%Y年%c月%e日'
  • 曜日の日本語化は SET lc_time_names = 'ja_JP'。%a が「土」、%W が「土曜日」になる
  • 逆変換は STR_TO_DATE。書式が合わないと NULL と警告 1411、INSERT・UPDATE では厳密モードでエラーになる
  • WHERE句では DATE_FORMAT を使わず範囲条件で書くと、インデックスが使われる(EXPLAIN の type が ALL から range に変わる)
  • 比較と並べ替えは元の日付列で行い、DATE_FORMAT は表示用の列だけに使う

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「MySQL DATE_FORMATの使い方と書式一覧|文字列⇔日付変換」

株式会社RJCでは、MySQLの日付の扱い方やSQLの書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?