MySQL DATE_ADD 関数で簡単に日付計算!具体的な使い方

MySQL DATE_ADD 関数で簡単に日付計算!具体的な使い方徹底解説

はじめに:なぜデータベースで日付計算が必要なのか?

データベース、特にリレーショナルデータベースであるMySQLでは、日付や時刻のデータを扱う機会は非常に多いです。ECサイトの注文日、ユーザーの登録日、ブログ記事の公開日、タスクの完了期限など、様々な情報が日付・時刻データとして格納されます。

これらの日付データを扱う際、単に表示するだけでなく、「N日後の日付を計算したい」「特定のイベントからMヶ月前の日付を知りたい」「指定した期間内のデータを抽出したい」といった、日付や時刻に関する計算が必要になることが頻繁に発生します。

例えば、以下のような計算を考えた場合、どのように実現するでしょうか?

  • 注文日から7日後の発送予定日を計算したい。
  • 契約開始日から1年後の契約更新日を計算したい。
  • 特定のキャンペーン期間(開始日から30日間)に購入した顧客リストを作成したい。
  • 今日のデータだけでなく、過去90日間の集計を行いたい。
  • 毎月末に発生するイベントの次回の発生日を知りたい。

これらの計算を手動で行うのは現実的ではありませんし、アプリケーション側で計算しようとすると、プログラミング言語によって日付計算の方法が異なったり、タイムゾーンや夏時間、うるう年といった考慮事項が出てきて複雑になりがちです。

そこで活躍するのが、データベースシステムが提供する日付・時刻関数です。MySQLには、日付や時刻に関する様々な計算や操作を行うための関数が豊富に用意されています。その中でも、特定の日付や時刻に一定期間を加算する際に最もよく利用される関数の一つが DATE_ADD 関数です。

DATE_ADD 関数を使うことで、MySQLのクエリ内で直接、直感的かつ正確に日付・時刻の計算を行うことができます。本記事では、この DATE_ADD 関数に焦点を当て、その基本的な使い方から、様々な単位での加算方法、関連関数との連携、そして実務で役立つ応用例や注意点まで、約5000語にわたって徹底的に解説します。これを読めば、MySQLでの日付計算が格段に楽になるはずです。

DATE_ADD 関数の基本

DATE_ADD 関数は、指定した日付または日時に対して、指定した期間(インターバル)を加算するための関数です。その基本的な構文は以下の通りです。

sql
DATE_ADD(date, INTERVAL expr unit)

この構文は、以下の3つの要素から構成されています。

  1. date: 計算の基準となる日付または日時の値です。カラム名、リテラル(文字列)、あるいは日付/時刻関数(例: NOW(), CURDATE())を指定できます。
  2. INTERVAL: これはキーワードであり、必ず記述します。続く exprunit を組み合わせて、加算する「期間」を指定することを示します。
  3. expr unit: ここで、加算する期間の量 (expr) と単位 (unit) を指定します。

    • expr: 加算する期間の量を数値で指定します。整数だけでなく、浮動小数点数も単位によっては指定可能です。正の値であれば将来の日付、負の値であれば過去の日付を計算できます。符号は expr に含める必要があります。
    • unit: 加算する期間の単位を指定します。様々な単位が利用可能であり、後述で詳しく説明します。単一の単位だけでなく、複数の単位を組み合わせた複合単位も指定できます。

DATE_ADD 関数は、加算後の日付または日時の値を返します。元の date が日付 (DATE) 型の場合、結果も日付型になります(ただし、時刻単位を加算した場合は結果が DATETIME 型になることがあります)。元の date が日時 (DATETIME or TIMESTAMP) 型の場合、結果は通常日時型になります。

引数の詳細と具体例

DATE_ADD 関数の各引数について、さらに詳しく見ていきましょう。

date 引数

date 引数は、計算の起点となる日付または日時です。以下の形式で指定できます。

  • 日付リテラル(文字列): 'YYYY-MM-DD' 形式で指定します。
    sql
    SELECT DATE_ADD('2023-10-26', INTERVAL 7 DAY);
    -- 結果: '2023-11-02'
  • 日時リテラル(文字列): 'YYYY-MM-DD HH:MI:SS' 形式で指定します。
    sql
    SELECT DATE_ADD('2023-10-26 10:00:00', INTERVAL 2 HOUR);
    -- 結果: '2023-10-26 12:00:00'
  • DATE または DATETIME 型のカラム名: テーブルのカラムに格納された日付/日時データを使用します。
    sql
    -- 仮に orders テーブルに order_date (DATE型) カラムがあるとして
    SELECT order_date, DATE_ADD(order_date, INTERVAL 7 DAY) AS shipping_date
    FROM orders
    WHERE order_id = 101;
  • 日付/時刻関数: 現在の日付や日時を取得する関数などを使用します。
    “`sql
    — 今日の日付から3ヶ月後
    SELECT DATE_ADD(CURDATE(), INTERVAL 3 MONTH);

    — 現在の日時から1時間30分後
    SELECT DATE_ADD(NOW(), INTERVAL ‘1:30’ HOUR_MINUTE);
    “`

無効な日付値を date に指定した場合、DATE_ADDNULL を返すことがあります。

INTERVAL expr unit 引数

この部分が DATE_ADD 関数の心臓部です。加算する期間を「量 (expr)」と「単位 (unit)」の組み合わせで指定します。

unit の種類と具体的な使い方

unit は、加算する期間の単位を指定するキーワードです。MySQLは非常に多様な単位をサポートしています。ここでは主要な単位とその使い方を詳しく見ていきます。

単一単位:

加算する量が単一の単位で表せる場合に使用します。expr は通常、整数のリテラルまたは整数に評価される式を指定します。

  1. SECOND: 秒単位での加算。
    “`sql
    SELECT DATE_ADD(‘2023-10-26 10:30:00’, INTERVAL 30 SECOND);
    — 結果: ‘2023-10-26 10:30:30’

    SELECT DATE_ADD(NOW(), INTERVAL -15 SECOND); — 15秒前
    `expr` に浮動小数点数を指定しても、小数点以下は切り捨てられます。sql
    SELECT DATE_ADD(‘2023-10-26 10:30:00’, INTERVAL 30.5 SECOND);
    — 結果: ‘2023-10-26 10:30:30’ (0.5秒は無視される)
    “`

  2. MINUTE: 分単位での加算。
    “`sql
    SELECT DATE_ADD(‘2023-10-26 10:30:00’, INTERVAL 15 MINUTE);
    — 結果: ‘2023-10-26 10:45:00’

    SELECT DATE_ADD(‘2023-10-26 10:30:00’, INTERVAL -60 MINUTE); — 1時間前
    — 結果: ‘2023-10-26 09:30:00’
    “`

  3. HOUR: 時間単位での加算。
    “`sql
    SELECT DATE_ADD(‘2023-10-26 10:30:00’, INTERVAL 8 HOUR);
    — 結果: ‘2023-10-26 18:30:00’

    SELECT DATE_ADD(CURDATE(), INTERVAL 10 HOUR); — 今日の10時00分00秒 (CURDATE()は時間を00:00:00と扱う)
    — 結果例: ‘2023-10-26 10:00:00’
    “`

  4. DAY: 日単位での加算。日付計算で最も頻繁に利用されます。
    “`sql
    SELECT DATE_ADD(‘2023-10-26’, INTERVAL 1 DAY);
    — 結果: ‘2023-10-27’

    SELECT DATE_ADD(‘2023-10-26’, INTERVAL 30 DAY);
    — 結果: ‘2023-11-25’ (月の繰り上がりも正しく計算される)

    SELECT DATE_ADD(CURDATE(), INTERVAL -7 DAY); — 7日前
    “`

  5. WEEK: 週単位での加算(7日単位での加算)。
    “`sql
    SELECT DATE_ADD(‘2023-10-26’, INTERVAL 1 WEEK); — 7日後
    — 結果: ‘2023-11-02’

    SELECT DATE_ADD(‘2023-10-26’, INTERVAL 4 WEEK); — 28日後
    — 結果: ‘2023-11-23’
    “`

  6. MONTH: 月単位での加算。expr は通常整数です。
    “`sql
    SELECT DATE_ADD(‘2023-10-26’, INTERVAL 1 MONTH);
    — 結果: ‘2023-11-26’

    SELECT DATE_ADD(‘2023-10-26’, INTERVAL 6 MONTH);
    — 結果: ‘2024-04-26’

    SELECT DATE_ADD(‘2023-10-26’, INTERVAL -3 MONTH); — 3ヶ月前
    — 結果: ‘2023-07-26’
    “`
    注意点: 月単位の加算では、元の日の日付が加算後の月に存在しない場合、その月の最終日に調整されます。これは重要な挙動であり、後述の「注意点・落とし穴」で詳しく説明します。

  7. QUARTER: 四半期単位での加算(3ヶ月単位)。
    “`sql
    SELECT DATE_ADD(‘2023-10-26’, INTERVAL 1 QUARTER); — 3ヶ月後
    — 結果: ‘2024-01-26’

    SELECT DATE_ADD(‘2023-10-26’, INTERVAL -2 QUARTER); — 6ヶ月前
    — 結果: ‘2023-04-26’
    ``
    これも
    MONTH` と同様に月末処理の注意点があります。

  8. YEAR: 年単位での加算。
    “`sql
    SELECT DATE_ADD(‘2023-10-26’, INTERVAL 1 YEAR);
    — 結果: ‘2024-10-26’

    SELECT DATE_ADD(‘2023-02-29’, INTERVAL 1 YEAR); — うるう年の2月29日に1年加算
    — 結果: ‘2024-02-29’ (翌年がうるう年なら2月29日)

    SELECT DATE_ADD(‘2023-02-28’, INTERVAL 1 YEAR);
    — 結果: ‘2024-02-28’

    SELECT DATE_ADD(‘2024-02-29’, INTERVAL 1 YEAR); — うるう年の2月29日に1年加算 (翌年がうるう年でない)
    — 結果: ‘2025-02-28’ (翌年の最終日に調整される)
    “`
    これも月末処理(特に2月末)の注意点があります。

  9. MICROSECOND: マイクロ秒単位での加算。日時型で使用します。MySQLのバージョンや設定によっては、マイクロ秒の精度が制限される場合があります。
    sql
    SELECT DATE_ADD('2023-10-26 10:30:00.123456', INTERVAL 100 MICROSECOND);
    -- 結果: '2023-10-26 10:30:00.123556'

複合単位:

加算する量が複数の単位の組み合わせで表せる場合に使用します。expr は、単位を結合した特定の形式の文字列で指定します。expr をシングルクォートで囲む必要があります。

形式: 'DETERMINANT UNIT_COMBINATION'

DETERMINANT は、時間、日付、またはその両方を含む数値部分です。UNIT_COMBINATION は、使用する単位の組み合わせを指定します。

代表的な複合単位と expr の形式:

  1. MINUTE_SECOND: 分と秒。expr 形式は 'MM:SS' または 'MM:SS.fraction'
    “`sql
    SELECT DATE_ADD(‘2023-10-26 10:30:00’, INTERVAL ‘5:30’ MINUTE_SECOND); — 5分30秒後
    — 結果: ‘2023-10-26 10:35:30’

    SELECT DATE_ADD(‘2023-10-26 10:30:00.000’, INTERVAL ‘0:0.123’ MINUTE_SECOND); — 123ミリ秒後 (0.123秒)
    — 結果: ‘2023-10-26 10:30:00.123’
    “`

  2. HOUR_MINUTE: 時間と分。expr 形式は 'HH:MM'
    sql
    SELECT DATE_ADD('2023-10-26 10:30:00', INTERVAL '2:45' HOUR_MINUTE); -- 2時間45分後
    -- 結果: '2023-10-26 13:15:00'

  3. HOUR_SECOND: 時間、分、秒。expr 形式は 'HH:MM:SS'
    sql
    SELECT DATE_ADD('2023-10-26 10:30:00', INTERVAL '1:30:45' HOUR_SECOND); -- 1時間30分45秒後
    -- 結果: '2023-10-26 12:00:45'

  4. DAY_HOUR: 日と時間。expr 形式は 'D HH'
    sql
    SELECT DATE_ADD('2023-10-26 10:00:00', INTERVAL '3 12' DAY_HOUR); -- 3日と12時間後
    -- 結果: '2023-10-29 22:00:00'

  5. DAY_MINUTE: 日、時間、分。expr 形式は 'D HH:MM'
    sql
    SELECT DATE_ADD('2023-10-26 10:00:00', INTERVAL '5 8:30' DAY_MINUTE); -- 5日8時間30分後
    -- 結果: '2023-10-31 18:30:00'

  6. DAY_SECOND: 日、時間、分、秒。expr 形式は 'D HH:MM:SS'
    sql
    SELECT DATE_ADD('2023-10-26 10:00:00', INTERVAL '7 2:05:10' DAY_SECOND); -- 7日2時間5分10秒後
    -- 結果: '2023-11-02 12:05:10'

  7. YEAR_MONTH: 年と月。expr 形式は 'YYYY-MM'
    “`sql
    SELECT DATE_ADD(‘2023-10-26’, INTERVAL ‘1-3’ YEAR_MONTH); — 1年3ヶ月後
    — 結果: ‘2025-01-26’ (2023年10月 + 1年 = 2024年10月, 2024年10月 + 3ヶ月 = 2025年1月)

    SELECT DATE_ADD(‘2023-10-26’, INTERVAL ‘-2-6’ YEAR_MONTH); — 2年6ヶ月前
    — 結果: ‘2021-04-26’
    “`
    これも月末処理の注意点があります。

  8. SECOND_MICROSECOND: 秒とマイクロ秒。expr 形式は 'SS.fraction'
    sql
    SELECT DATE_ADD('2023-10-26 10:30:00.000000', INTERVAL '10.500' SECOND_MICROSECOND); -- 10.5秒後
    -- 結果: '2023-10-26 10:30:10.500000'

  9. MINUTE_MICROSECOND: 分、秒、マイクロ秒。expr 形式は 'MM:SS.fraction'
    sql
    SELECT DATE_ADD('2023-10-26 10:30:00.000000', INTERVAL '5:10.123456' MINUTE_MICROSECOND); -- 5分10.123456秒後
    -- 結果: '2023-10-26 10:35:10.123456'

  10. HOUR_MICROSECOND: 時間、分、秒、マイクロ秒。expr 形式は 'HH:MM:SS.fraction'
    sql
    SELECT DATE_ADD('2023-10-26 10:30:00.000000', INTERVAL '1:30:45.987654' HOUR_MICROSECOND); -- 1時間30分45.987654秒後
    -- 結果: '2023-10-26 12:00:45.987654'

  11. DAY_MICROSECOND: 日、時間、分、秒、マイクロ秒。expr 形式は 'D HH:MM:SS.fraction'
    sql
    SELECT DATE_ADD('2023-10-26 10:30:00.000000', INTERVAL '2 12:34:56.123456' DAY_MICROSECOND); -- 2日12時間34分56.123456秒後
    -- 結果: '2023-10-29 00:04:56.123456'

  12. YEAR_MICROSECOND: 年、月、日、時間、分、秒、マイクロ秒。これは通常使用されません。YEAR_MONTH, DAY_SECOND などを組み合わせて計算します。

expr の符号:

expr に負の値を指定することで、特定期間「前」の日付・時刻を計算できます。これは DATE_SUB 関数を使うのと同等です。

“`sql
— 10日前
SELECT DATE_ADD(CURDATE(), INTERVAL -10 DAY);

— 3時間前
SELECT DATE_ADD(NOW(), INTERVAL -3 HOUR);

— 2年6ヶ月前
SELECT DATE_ADD(‘2023-10-26’, INTERVAL ‘-2-6’ YEAR_MONTH);
“`

expr は数値または文字列で指定しますが、文字列の場合(特に複合単位)、シングルクォートで囲む必要があります。数値の場合でも囲むことは可能ですが、必須ではありません(例: INTERVAL 7 DAY または INTERVAL '7' DAY)。しかし、複合単位の場合は 'D HH:MM:SS' のような固定形式で、かつ全体をシングルクォートで囲むことが必須です。

DATE_ADD と ADDDATE、DATE_SUB と SUBDATE

MySQLには、DATE_ADD 関数と非常に似た関数として ADDDATE が存在します。実は、DATE_ADD(date, INTERVAL expr unit)ADDDATE(date, INTERVAL expr unit) のシノニム(同義語)です。つまり、同じ機能を持つ関数であり、どちらを使っても構いません。

“`sql
SELECT DATE_ADD(‘2023-10-26’, INTERVAL 10 DAY);
— 結果: ‘2023-11-05’

SELECT ADDDATE(‘2023-10-26’, INTERVAL 10 DAY);
— 結果: ‘2023-11-05’
“`

さらに、ADDDATE 関数にはもう一つの構文があります。

sql
ADDDATE(date, days)

この構文では、指定した datedays で指定した日数を加算します。days は単なる整数値です。これは DATE_ADD(date, INTERVAL days DAY) と完全に同等です。

“`sql
SELECT ADDDATE(‘2023-10-26’, 10); — 10日後
— 結果: ‘2023-11-05’

SELECT DATE_ADD(‘2023-10-26’, INTERVAL 10 DAY); — 同じ結果
— 結果: ‘2023-11-05’

SELECT ADDDATE(‘2023-10-26’, -10); — 10日前
— 結果: ‘2023-10-16’
“`

こちらは日単位での加算・減算に特化したシンプルな構文と言えます。他の単位(月、年、時間など)を加算したい場合は、INTERVAL 構文を使う必要があります。

同様に、特定期間「前」の日付・時刻を計算するための関数として DATE_SUBSUBDATE があります。

sql
DATE_SUB(date, INTERVAL expr unit)
SUBDATE(date, INTERVAL expr unit)

これらは DATE_ADD / ADDDATE とは逆に、指定した期間を「減算」します。構文は加算の場合と全く同じです。

“`sql
— 10日前
SELECT DATE_SUB(‘2023-10-26’, INTERVAL 10 DAY);
— 結果: ‘2023-10-16’

— 3ヶ月前
SELECT DATE_SUB(‘2023-10-26’, INTERVAL 3 MONTH);
— 結果: ‘2023-07-26’
“`

そして SUBDATE にも、日単位減算に特化したもう一つの構文があります。

sql
SUBDATE(date, days)

これは DATE_SUB(date, INTERVAL days DAY) と同等です。

“`sql
SELECT SUBDATE(‘2023-10-26’, 10); — 10日前
— 結果: ‘2023-10-16’

SELECT DATE_SUB(‘2023-10-26’, INTERVAL 10 DAY); — 同じ結果
— 結果: ‘2023-10-16’
“`

結論として、日付・時刻の加算には DATE_ADD または ADDDATE を、減算には DATE_SUB または SUBDATE を使います。INTERVAL 構文を使えば、加算・減算どちらも DATE_ADD に負の expr を渡すことで実現できますが、可読性を考えると加算には DATE_ADD (または ADDDATE)、減算には DATE_SUB (または SUBDATE) を使い分けるのが一般的です。特に日単位の加算・減算であれば ADDDATE(date, days)SUBDATE(date, days) のシンプルな構文も便利です。

実践的な使い方例

DATE_ADD 関数は、実際のデータベース操作において様々な場面で活用できます。いくつか具体的な例を見てみましょう。

例 1:注文日からの発送予定日計算

ECサイトの注文テーブルがあるとします。注文日から3営業日後に発送すると仮定して、発送予定日を計算します(ここでは簡単のため、土日祝日は考慮せず単に3日後とします)。

sql
-- orders テーブルに order_id (INT), order_date (DATE) カラムがあるとする
SELECT
order_id,
order_date,
DATE_ADD(order_date, INTERVAL 3 DAY) AS estimated_shipping_date
FROM
orders
WHERE
order_id = 101;

結果例:
| order_id | order_date | estimated_shipping_date |
| ——– | ———- | ———————– |
| 101 | 2023-10-26 | 2023-10-29 |

例 2:サブスクリプションの次回更新日計算

会員テーブルに最終支払い日 last_paid_date (DATE) と契約期間 contract_months (INT) カラムがあるとします。次回の契約更新日を計算します。

sql
-- members テーブルに member_id (INT), last_paid_date (DATE), contract_months (INT) カラムがあるとする
SELECT
member_id,
last_paid_date,
contract_months,
DATE_ADD(last_paid_date, INTERVAL contract_months MONTH) AS next_renewal_date
FROM
members
WHERE
member_id = 5;

結果例(member_id = 5 の last_paid_date = ‘2023-07-15’, contract_months = 6 の場合):
| member_id | last_paid_date | contract_months | next_renewal_date |
| ——— | ————– | ————— | —————– |
| 5 | 2023-07-15 | 6 | 2024-01-15 |

月末処理の注意点も考慮する必要があります。例えば、last_paid_date = '2023-07-31', contract_months = 6 の場合、結果は '2024-01-31' となりますが、last_paid_date = '2023-08-31', contract_months = 6 の場合、結果は '2024-02-29' (2024年がうるう年なら) または '2024-02-28' (2024年がうるう年でなければ) となります。

例 3:特定の期間内のデータ抽出

「過去30日間の注文データ」を抽出したいとします。

sql
-- orders テーブルに order_date (DATE) カラムがあるとする
SELECT
order_id,
order_date,
amount
FROM
orders
WHERE
order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY);
-- または WHERE order_date >= ADDDATE(CURDATE(), -30);
-- または WHERE order_date BETWEEN DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND CURDATE();

WHERE 句で DATE_ADD (または DATE_SUB) を使用する場合、計算結果は特定の日付値になるため、その値とカラムを比較することでフィルタリングできます。ただし、パフォーマンスのセクションで詳しく述べますが、この書き方だとカラムにインデックスがあっても効率が悪くなる可能性があります。インデックスを有効に利用するためには、計算を検索対象のカラムではなく比較対象の値に対して行うように記述するのが推奨されます。

例えば、上記クエリは以下のように書き換える方が一般的にパフォーマンスが良いとされています。

sql
SELECT
order_id,
order_date,
amount
FROM
orders
WHERE
order_date BETWEEN DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND CURDATE();

この場合、DATE_SUB(CURDATE(), INTERVAL 30 DAY)CURDATE() はクエリ実行時に一度だけ計算され、定数として扱われるため、order_date カラムに対するインデックスが効率的に利用されます。

例 4:月末のイベントの次回日付

毎月末に定期的に実行されるタスクがあり、今日の日付から直近の月末以降の次々回の月末を知りたいとします。まず、今日の月末を計算し、そこからさらに2ヶ月加算することで、次々回の月末に近い日付を得ます。月末処理の挙動を利用します。

sql
-- 今日の日付を基準に、次々回の月末を計算する
SELECT
CURDATE() AS today,
LAST_DAY(CURDATE()) AS end_of_this_month, -- 今月の最終日
DATE_ADD(LAST_DAY(CURDATE()), INTERVAL 1 MONTH) AS end_of_next_month_potential, -- 来月末になる可能性のある日付
LAST_DAY(DATE_ADD(LAST_DAY(CURDATE()), INTERVAL 1 MONTH)) AS end_of_next_month_actual, -- 来月の最終日
DATE_ADD(LAST_DAY(CURDATE()), INTERVAL 2 MONTH) AS end_of_month_after_next_potential, -- 次々月の月末になる可能性のある日付
LAST_DAY(DATE_ADD(LAST_DAY(CURDATE()), INTERVAL 2 MONTH)) AS end_of_month_after_next_actual; -- 次々月の最終日

この例では、LAST_DAY 関数も組み合わせています。LAST_DAY(date) は、指定した date が属する月の最終日を返します。
月末処理の挙動により、DATE_ADD(date, INTERVAL N MONTH)date が月末日である場合、加算後の日付もその月の最終日になることが期待されます。例えば、10月31日に2ヶ月加えると、12月31日になります。しかし、常に月末日を基準に計算したい場合は、一度 LAST_DAY で月末にしてから DATE_ADD するのが安全な方法です。

よりシンプルに「来月末」や「次々回の月末」を知るには、現在の月初や月末から月を加えて、その月の最終日を LAST_DAY で計算するのが確実です。

“`sql
— 今日の日付から、来月の月末を計算する
SELECT
CURDATE() AS today,
LAST_DAY(DATE_ADD(CURDATE(), INTERVAL 1 MONTH)) AS end_of_next_month;

— 今日の日付から、次々月の月末を計算する
SELECT
CURDATE() AS today,
LAST_DAY(DATE_ADD(CURDATE(), INTERVAL 2 MONTH)) AS end_of_month_after_next;
“`
この方が意図が明確になり、計算も正確です。

例 5:特定の時間間隔での集計

例えば、過去24時間以内のユーザーアクティビティを集計したい場合。

sql
-- user_activity テーブルに activity_time (DATETIME) カラムがあるとする
SELECT
user_id,
COUNT(*) AS recent_activity_count
FROM
user_activity
WHERE
activity_time >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
-- または activity_time >= NOW() - INTERVAL 24 HOUR -- INTERVALは算術演算子としても使用可能
GROUP BY
user_id;

この例では、NOW() 関数(現在の日時を返す)から24時間前を計算し、それ以降のアクティビティをフィルタリングしています。

注意点・落とし穴

DATE_ADD 関数は非常に便利ですが、いくつか注意すべき点があります。

  1. 月末処理 (Month-End Handling):
    MONTH, QUARTER, YEAR, YEAR_MONTH といった月を含む単位を加算する場合、元の日の日付が加算後の月に存在しない場合、MySQLはその月の最終日に日付を調整します。
    例えば、1月31日に1ヶ月を加算すると、通常は2月31日という日は存在しないため、2月の最終日(うるう年なら29日、それ以外なら28日)になります。
    “`sql
    SELECT DATE_ADD(‘2023-01-31’, INTERVAL 1 MONTH); — 2023年はうるう年でない
    — 結果: ‘2023-02-28’

    SELECT DATE_ADD(‘2024-01-31’, INTERVAL 1 MONTH); — 2024年はうるう年
    — 結果: ‘2024-02-29’

    SELECT DATE_ADD(‘2023-03-31’, INTERVAL 1 MONTH);
    — 結果: ‘2023-04-30’

    SELECT DATE_ADD(‘2023-03-31’, INTERVAL 2 MONTH);
    — 結果: ‘2023-05-31’ (5月31日は存在する)
    “`
    この挙動は仕様であり、ほとんどの場合は意図した結果になりますが、予期しない結果にならないよう注意が必要です。特に、毎月同じ日付(例えば15日)にイベントがある場合でも、計算によっては月末日になってしまう可能性があることを理解しておきましょう。

    例えば、契約開始日が毎月月末で、そこから1ヶ月後の更新日を計算したい場合、DATE_ADD だけで計算するとうまくいくように見えますが、開始日が月の途中で、加算期間が月末処理を引き起こすような期間(例えば1ヶ月)でない場合、期待と異なる日付になる可能性があります。このような場合は、前述の LAST_DAY(DATE_ADD(start_date, INTERVAL N MONTH)) のように、計算結果の月の最終日を取得する方が正確な場合があります。

  2. タイムゾーンと夏時間 (Time Zones and DST):
    DATE_ADD 関数自体は、基本的にデータベースサーバーのシステムタイムゾーン設定に基づいて日付/時刻計算を行います。しかし、夏時間(DST)の開始や終了によって、特定の時間帯が存在しなかったり、重複したりする場合があります。DATE_ADD は、このようなDSTによる時間のずれを自動的に補正しないことがあります。特に HOUR, MINUTE, SECOND のような時間単位で計算する場合、DST期間をまたぐ計算には注意が必要です。より厳密なタイムゾーン処理が必要な場合は、CONVERT_TZ 関数など、タイムゾーンを意識した関数と組み合わせて使用するか、アプリケーション側でタイムゾーン情報を適切に処理する必要があります。

  3. うるう年 (Leap Year):
    YEARYEAR_MONTH 単位での加算・減算で、2月29日を含む日付を扱う場合、うるう年の扱いに注意が必要です。
    “`sql
    SELECT DATE_ADD(‘2024-02-29’, INTERVAL 1 YEAR); — 2024年はうるう年、2025年は違う
    — 結果: ‘2025-02-28’ (翌年の最終日に調整)

    SELECT DATE_ADD(‘2024-02-29’, INTERVAL 4 YEAR); — 2028年はうるう年
    — 結果: ‘2028-02-29’
    “`
    これも月末処理の一種として、加算後の年に2月29日が存在しない場合は2月28日に調整されることを覚えておきましょう。

  4. 無効な日付・日時値:
    DATE_ADDdate 引数に無効な日付(例: '2023-02-30', '2023-13-01')を指定した場合、結果は NULL になることがあります。また、厳密なSQLモードが有効になっている場合、エラーが発生する可能性もあります。クエリを実行する前に、入力データが有効な日付・日時であることを確認することが重要です。

関連する便利な関数

DATE_ADD 関数と組み合わせて使うと便利な、MySQLの日付・時刻関連関数がいくつかあります。

  • NOW(): 現在の日時を 'YYYY-MM-DD HH:MM:SS' または 'YYYY-MM-DD HH:MM:SS.fraction' 形式で返します。DATE_ADD(NOW(), ...) のように、現在時刻を基準にした計算によく使われます。
  • CURDATE(): 現在の日付を 'YYYY-MM-DD' 形式で返します。DATE_ADD(CURDATE(), ...) のように、今日を基準にした日付計算によく使われます。
  • CURTIME(): 現在の時刻を 'HH:MM:SS' または 'HH:MM:SS.fraction' 形式で返します。時間単位の計算と組み合わせて使われることがあります。
  • UNIX_TIMESTAMP(date): 指定した日時をUnixタイムスタンプ(1970年1月1日 00:00:00 UTCからの経過秒数)に変換します。
  • FROM_UNIXTIME(unix_timestamp): Unixタイムスタンプを日時形式に変換します。
    Unixタイムスタンプは単純な数値なので、数値演算で期間を加算・減算し、後で日時形式に戻すというアプローチも考えられます。例えば、「現在時刻から1時間後」は FROM_UNIXTIME(UNIX_TIMESTAMP() + 3600) と計算することもできます。しかし、DATE_ADD(NOW(), INTERVAL 1 HOUR) の方が直感的で、MySQLの最適化も効きやすいため、通常は DATE_ADD を使う方が推奨されます。

  • DATE_FORMAT(date, format): 日付/日時を指定した形式の文字列に整形します。DATE_ADD で計算した結果を見やすい形式で表示したい場合に役立ちます。
    sql
    SELECT DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y年%m月%d日');
    -- 結果例: '2023年11月26日'

  • DATEDIFF(date1, date2): 2つの日付間の日数を返します (date1 - date2)。DATE_ADD とは異なり、日数を計算する関数です。

  • TIMESTAMPDIFF(unit, datetime_expr1, datetime_expr2): 2つの日時/日付間の期間を、指定した単位で計算します。DATE_ADD の逆の操作に近い関数です。例えば、「2つの日付間に何ヶ月あるか」などを計算できます。
    sql
    SELECT TIMESTAMPDIFF(MONTH, '2023-07-15', '2024-01-15');
    -- 結果: 6 (2024-01-15 は 2023-07-15 の6ヶ月後)

    DATE_ADD で特定の期間を加算した結果が正しいかを確認する際に、TIMESTAMPDIFF が役立つことがあります。

  • INTERVAL expr unit: これは関数ではなく、期間を指定するためのリテラル構造です。DATE_ADD, DATE_SUB, ADDDATE, SUBDATE などの関数で期間を指定する際に必須となります。

パフォーマンスに関する考慮事項

DATE_ADD 関数は、SQLクエリの SELECT リストや WHERE 句、ORDER BY 句などで使用できます。しかし、パフォーマンスの観点から、特に WHERE 句での使用には注意が必要です。

例として、「過去7日間の注文」を検索するクエリを考えます。

“`sql
— クエリA: DATE_ADD/SUB を WHERE 句のカラム側に適用
SELECT order_id, order_date FROM orders WHERE DATE_SUB(order_date, INTERVAL 7 DAY) >= CURDATE(); — order_date から7日引いて今日と比較

— クエリB: DATE_ADD/SUB を WHERE 句の定数側に適用
SELECT order_id, order_date FROM orders WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY); — 今日から7日引いた日付と order_date を比較
“`

もし orders テーブルの order_date カラムにインデックスが作成されている場合、クエリAのようにインデックスが適用されるカラム(order_date)に対して関数 (DATE_SUB) を適用してしまうと、MySQLはインデックスを利用できず、テーブル全体をスキャンする可能性が高くなります(フルテーブルスキャン)。これはデータ量が多い場合にパフォーマンスが著しく低下する原因となります。

一方、クエリBのように、関数 (DATE_SUB) を比較対象となる値 (CURDATE()) に適用し、インデックスが適用されているカラム(order_date)はそのままの形で比較演算子(>=)の片側に置く場合、MySQLは DATE_SUB(CURDATE(), INTERVAL 7 DAY) の計算結果を定数として扱い、order_date >= '計算結果の日付' という形式の比較を行います。この形式であれば、order_date カラムのインデックスを効率的に利用することができ、検索速度が向上します。

したがって、WHERE 句で日付計算を行う場合は、計算結果が定数となるように、計算対象をカラムではなくリテラルや関数(例: CURDATE(), NOW())側に寄せるように記述するのが、インデックスを最大限に活用するためのベストプラクティスです。

より具体的には、WHERE column_name operator DATE_ADD/SUB(constant, INTERVAL expr unit) または WHERE column_name BETWEEN date1 AND date2 の形式を目指します。date1date2DATE_ADD/SUB などで事前に計算した定数値です。

例:
“`sql
— 過去30日間のデータ
SELECT … FROM your_table WHERE date_column BETWEEN DATE_SUB(CURDATE(), INTERVAL 30 DAY) AND CURDATE();

— 未来3日間のデータ
SELECT … FROM your_table WHERE date_column BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 3 DAY);
“`

ただし、SELECT リストや ORDER BY 句で DATE_ADD を使用しても、通常は大きなパフォーマンス問題になりにくいです。なぜなら、これらの句は検索結果に対して処理を行うため、インデックスの利用には直接影響しない場合が多いからです(ただし、結果セットが大きい場合は計算コストが無視できないこともあります)。

まとめ

本記事では、MySQLの日付・時刻計算において非常に強力で便利な DATE_ADD 関数について、その基本的な使い方から応用、そして注意点まで詳細に解説しました。

  • DATE_ADD(date, INTERVAL expr unit) の構文で、指定した日付/時刻に期間を加算できます。
  • expr に負の値を指定することで、過去の日付/時刻を計算できます。
  • unit には SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR, MICROSECOND などの単一単位、および 'D HH:MM:SS' のような形式で指定する MINUTE_SECOND, HOUR_MINUTE, DAY_HOUR, YEAR_MONTH などの複合単位が豊富に用意されています。
  • ADDDATE(date, INTERVAL expr unit)DATE_ADD のシノニムであり、ADDDATE(date, days) は日単位の加算に特化したシンプルな構文です。
  • DATE_SUB(date, INTERVAL expr unit) および SUBDATE(date, INTERVAL expr unit) / SUBDATE(date, days) は、指定した期間を減算するための関数です。
  • 月を含む単位(MONTH, YEAR_MONTH など)で加算する場合、加算後の月に元の日の日付が存在しない場合は、その月の最終日に調整される「月末処理」の挙動があります。
  • WHERE 句で DATE_ADD などの関数を使用する場合、インデックスを有効に利用するためには、計算対象をカラムではなく比較対象の値側に記述する(例: WHERE date_column >= calculated_date)ことが重要です。
  • CURDATE(), NOW(), LAST_DAY, DATE_FORMAT, TIMESTAMPDIFF などの関連関数と組み合わせることで、より高度な日付・時刻処理が可能になります。

DATE_ADD 関数をマスターすることで、MySQLデータベース内での日付計算が非常に簡単かつ効率的に行えるようになります。日々の業務やアプリケーション開発で日付データを扱う際に、ぜひ積極的に活用してみてください。月末処理やパフォーマンスに関する注意点を理解しておけば、予期せぬ問題を防ぎ、データベースの性能を最大限に引き出すことができるでしょう。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール