Ccmmutty logo
Commutty IT
0 pv10 min read

【SQL・PG】月末日関連の関数・処理テンプレメモ

https://cdn.magicode.io/media/notebox/d90ee7dd-b712-4a27-a70f-3545ad7212e7.jpeg
大体この辺の時期になると隠れてたバグが見つかるやつ・・・
ということで今回は
各データベースでの計算方法と自分がよく使う言語での月末判定を整理しました。

月の演算(Nか月後・Nか月前)と月末調整

対象データベース: ORACLE, SQLSERVER, POSTGRES, MYSQL, MariaDB, SQLite, IBM DB2
考慮点:
  • Nか月後・Nか月前の計算
  • 指定日が月末の場合は算出結果も月末にする
  • うるう年対応
  • 11/30の3か月後 → 2/28, 2/28の3か月後 → 5/28 の月末にしないパターンも一応できるやつは考える

1. ORACLE

ORACLEではADD_MONTHS関数を使用できる。
月末調整も自動で行われるため、基本的にADD_MONTHSを使えばOK。
-- Nか月後(指定日が月末なら結果も月末)
SELECT ADD_MONTHS(DATE '2025-02-28', 3) FROM dual; -- 2025-05-31

-- Nか月前(指定日が月末なら結果も月末)
SELECT ADD_MONTHS(DATE '2025-02-28', -3) FROM dual; -- 2024-11-30
特記事項
  • ORACLEのADD_MONTHSは、月末日の場合は結果も月末になる仕様(2025/2/28 → 2025/5/31)。
  • うるう年も正しく処理(2024/2/28 → 2024/5/28)。
  • 11/30の3か月後は2/28 になる(2024/11/30 → 2025/2/28)。

2. SQLSERVER

SQLSERVERではDATEADD関数を使用するが、月末処理は自動で行わなれない
そのため、月末判定を行い、月末の場合はEOMONTHを使う必要あり。
-- Nか月後(指定日が月末なら結果も月末)
SELECT 
    CASE 
        WHEN DAY(@date) = DAY(EOMONTH(@date)) 
        THEN EOMONTH(DATEADD(MONTH, 3, @date))
        ELSE DATEADD(MONTH, 3, @date)
    END AS result
特記事項
  • EOMONTHを使い、月末なら結果も月末にする。
  • 2/28 → 5/28 の調整は別途処理が必要(DATEADDの標準仕様では 2/28 → 5/28 にならない)。

3. POSTGRES

PostgreSQLではINTERVAL 'N months'を使いますが、月末処理を考慮する必要あり
-- Nか月後(指定日が月末なら結果も月末)
SELECT 
    CASE 
        WHEN date_part('day', my_date) = date_part('day', my_date + INTERVAL '1 month' - INTERVAL '1 day') 
        THEN my_date + INTERVAL '3 months' - INTERVAL '1 day'
        ELSE my_date + INTERVAL '3 months'
    END AS result
FROM (SELECT DATE '2025-02-28' AS my_date) t;
特記事項
  • INTERVALを使うと、基本的には2/28 → 5/28になる。
  • 月末調整が必要+ INTERVAL '1 month' - INTERVAL '1 day'で判定)。

4. MYSQL / MariaDB

DATE_ADD関数を使いますが、月末処理を考慮する必要あり
-- Nか月後(指定日が月末なら結果も月末)
SELECT 
    CASE 
        WHEN DAY(@date) = DAY(LAST_DAY(@date)) 
        THEN LAST_DAY(DATE_ADD(@date, INTERVAL 3 MONTH))
        ELSE DATE_ADD(@date, INTERVAL 3 MONTH)
    END AS result;
特記事項
  • LAST_DAYを使い、元の月が月末なら結果も月末にする
  • うるう年や11/30 → 2/28 の処理は問題なし

5. SQLite

SQLiteではstrftimeを使いますが、月末処理を考慮する必要あり
-- Nか月後(指定日が月末なら結果も月末)
SELECT 
    CASE 
        WHEN strftime('%d', date) = strftime('%d', date, 'start of month', '+1 month', '-1 day')
        THEN strftime('%Y-%m-%d', date, '+3 months', 'start of month', '+1 month', '-1 day')
        ELSE strftime('%Y-%m-%d', date, '+3 months')
    END AS result
FROM (SELECT '2025-02-28' AS date);
特記事項
  • SQLiteにはEOMONTHのような関数がないため、start of month, +1 month, -1 day で月末調整。
  • うるう年の処理も適切に行う。

6. IBM DB2

DB2ではADD_MONTHSが使えるらしい。一度しか見たことないので知識浅め
-- Nか月後(指定日が月末なら結果も月末)
SELECT ADD_MONTHS(DATE('2025-02-28'), 3) FROM SYSIBM.SYSDUMMY1;
特記事項
  • ORACLEと同様、DB2のADD_MONTHSは月末を維持する
  • うるう年の調整も問題なし。

まとめ(DBごとの対応表)

DB標準関数月末保持うるう年対応
ORACLEADD_MONTHS
SQLSERVERDATEADD + EOMONTH要調整❌ (2/28 → 5/28にする場合は追加処理)
POSTGRESINTERVAL要調整
MYSQLDATE_ADD + LAST_DAY要調整
MariaDBDATE_ADD + LAST_DAY要調整
SQLitestrftime要調整
IBM DB2ADD_MONTHS

DB系まとめ
  • ORACLE, DB2は ADD_MONTHS で簡単に処理できる。
  • SQLSERVER, POSTGRES, MYSQL, SQLite は月末調整のロジックが必要
  • うるう年対応は INTERVAL (Postgres) では問題ないが、SQLSERVERは調整が必要。

◆言語での月末日判定まとめ

これらを利用して上記のDB周りの関数ではどうにもならないのを補助。もしくは言語でどうにかする時に使う。

C#

DateTime.DaysInMonthを使用する
using System;

public class DateUtils
{
    public static bool IsEndOfMonth(DateTime date)
    {
        return date.Day == DateTime.DaysInMonth(date.Year, date.Month);
    }
}

// 使用例
Console.WriteLine(DateUtils.IsEndOfMonth(new DateTime(2025, 2, 28))); // True
Console.WriteLine(DateUtils.IsEndOfMonth(new DateTime(2025, 2, 27))); // False

VB.NET

DateTime.DaysInMonthを使用する
Imports System

Module DateUtils
    Function IsEndOfMonth(ByVal dateValue As DateTime) As Boolean
        Return dateValue.Day = DateTime.DaysInMonth(dateValue.Year, dateValue.Month)
    End Function

    Sub Main()
        Console.WriteLine(IsEndOfMonth(New DateTime(2025, 2, 28))) ' True
        Console.WriteLine(IsEndOfMonth(New DateTime(2025, 2, 27))) ' False
    End Sub
End Module

Java

lengthOfMonth()を使用する
lengthOfMonth ⇒ 月の長さ(月末日) getDayOfMonth ⇒ 月の何日目か(2025/02/28⇒28)
import java.time.LocalDate;

public class DateUtils {
    public static boolean isEndOfMonth(LocalDate date) {
        return date.getDayOfMonth() == date.lengthOfMonth();
    }

    public static void main(String[] args) {
        System.out.println(isEndOfMonth(LocalDate.of(2025, 2, 28))); // True
        System.out.println(isEndOfMonth(LocalDate.of(2025, 2, 27))); // False
    }
}

Python(ライブラリなし)

28日を基準に計算する。
やってることは他とあまり変わらない。
ライブラリをみたらPythonは calendar モジュールや relativedelta を提供する dateutil ライブラリが使用できる。
使うとよりシンプルに月末日を判定できる。※下記にライブラリを使ってるのを記載するので興味なければ飛ばしてください

from datetime import date

def is_end_of_month(d: date) -> bool:
    next_day = d.replace(day=28) + (date(d.year, d.month % 12 + 1, 1) - date(d.year, d.month, 28))
    return d == next_day - (next_day - d).days

# 使用例
print(is_end_of_month(date(2025, 2, 28)))  # True
print(is_end_of_month(date(2025, 2, 27)))  # False

一応、仕組み。間違ってたらすみません。
  1. d.replace(day=28)
    • 指定日の日付を28日に固定します。
    • 理由: 全ての月に28日は存在するため、エラーを防ぐために28日を基準にしている。
  2. date(d.year, d.month % 12 + 1, 1)
    • 次の月の1日を作成。
    • d.month % 12 + 1 により、12月の場合は翌年の1月となる。
  3. date(d.year, d.month, 28)
    • 今月の28日を取得。
  4. next_day = d.replace(day=28) + (date(d.year, d.month % 12 + 1, 1) - date(d.year, d.month, 28))
    • 次の月の1日との差分を利用し、月末の日付を取得
    • (date(d.year, d.month % 12 + 1, 1) - date(d.year, d.month, 28))
      → これにより 月末までの日数 が求まる。
    • それを28日に加算することで、月末の日付を導き出す。
  5. d == next_day - (next_day - d).days
    • next_day - (next_day - d).days によって元の日付が月末かどうかを判定。
    • 月末の場合は d == next_day となる。

コードの挙動例

入力日付next_day判定 (d == next_day)
2025/02/282025/02/28True
2025/02/272025/02/28False
2024/11/302024/11/30True
2025/01/312025/01/31True

calendar モジュールを使う

calendar.monthrange を使うと、指定した月の最終日が簡単に取得できる。
import calendar
from datetime import date

def is_end_of_month(d: date) -> bool:
    last_day = calendar.monthrange(d.year, d.month)[1]
    return d.day == last_day

# 使用例
print(is_end_of_month(date(2025, 2, 28)))  # True
print(is_end_of_month(date(2025, 2, 27)))  # False
解説
  • calendar.monthrange(year, month)(曜日, 月の最終日) を返します。
  • d.day == last_day で月末かどうかを判定する。

dateutil.relativedelta を使う

dateutil ライブラリの relativedelta を使うと、簡単に月末日を求めることができる。
from datetime import date
from dateutil.relativedelta import relativedelta

def is_end_of_month(d: date) -> bool:
    return (d + relativedelta(days=1)).day == 1

# 使用例
print(is_end_of_month(date(2025, 2, 28)))  # True
print(is_end_of_month(date(2025, 2, 27)))  # False
解説
  • relativedelta(days=1) を使って翌日に移動。
  • 翌日が1日なら、元の日付は月末です。

どちらを選ぶべきか?

  • 標準ライブラリのみを使用したいcalendar モジュール
  • 外部ライブラリ (dateutil) を使ってもよいrelativedelta を使った方法
シンプルに書きたい場合や、dateutil を既に使っている場合は relativedelta が便利。
どちらもパフォーマンス的に大きな差はないっぽいので、用途に合わせて選んでください。
思い出しながら書いた部分もあったり言語バージョンが古かったりするかもなので
上手く動かないときは適宜対応ということでお願いします。

Discussion

コメントにはログインが必要です。