SQLの日付関数とは、現在日時の取得、日付の加算・減算、差分、書式変換などを行う関数で、名前と挙動がデータベースごとに異なります。 同じ「1か月後」でも、MySQLはDATE_ADD、PostgreSQLは+ INTERVAL、OracleはADD_MONTHS、SQL ServerはDATEADDと書き方が違い、さらに「1月31日の1か月後」「2月28日の1か月後」の結果までDBによって変わります。

この記事では、よく使う日付操作を「やりたいこと」別に4DBで横並びにした早見表と、DBごとのコピペ用SQLをまとめました。PostgreSQL 16.13とMySQL 8.0.46は実際に実行し、出力をそのまま掲載しています。OracleとSQL Serverは公式ドキュメント(Oracle SQL Language Reference 26、Microsoft Learn)に沿って記載し、表の見出しで「実行確認済み」「ドキュメント準拠」を区別しています。月末をまたぐ計算、BETWEENで最終日が抜ける現象、関数を列にかけるとインデックスが使われない点も、実行計画つきで確認します。

SQL日付関数の早見表(4DB比較)

dは日付の列または値、tsは日時です。

やりたいこと MySQL 8.0(実行確認済み) PostgreSQL 16(実行確認済み) Oracle(ドキュメント準拠) SQL Server(ドキュメント準拠)
現在日時 NOW() now() / CURRENT_TIMESTAMP SYSDATE / SYSTIMESTAMP GETDATE() / SYSDATETIME()
今日の日付だけ CURDATE() CURRENT_DATE TRUNC(SYSDATE) CAST(GETDATE() AS date)
n日後 d + INTERVAL 7 DAY d + 7 d + 7 DATEADD(day, 7, d)
nか月後 DATE_ADD(d, INTERVAL 1 MONTH) d + INTERVAL '1 month' ADD_MONTHS(d, 1) DATEADD(month, 1, d)
n時間後 ts + INTERVAL 3 HOUR ts + INTERVAL '3 hours' ts + INTERVAL '3' HOUR DATEADD(hour, 3, ts)
日数の差 DATEDIFF(d2, d1) d2 - d1 d2 - d1 DATEDIFF(day, d1, d2)
月数の差 TIMESTAMPDIFF(MONTH, d1, d2) age(d2, d1)から計算 MONTHS_BETWEEN(d2, d1) DATEDIFF(month, d1, d2)※境界の数
年齢 TIMESTAMPDIFF(YEAR, 誕生日, 基準日) EXTRACT(YEAR FROM age(基準日, 誕生日)) TRUNC(MONTHS_BETWEEN(基準日, 誕生日) / 12) 補正式が必要(後述)
月初 DATE_FORMAT(d, '%Y-%m-01') date_trunc('month', d) TRUNC(d, 'MM') DATETRUNC(month, d)※2022以降
月末 LAST_DAY(d) date_trunc('month', d) + INTERVAL '1 month - 1 day' LAST_DAY(d) EOMONTH(d)
週初(月曜) d - INTERVAL WEEKDAY(d) DAY date_trunc('week', d) TRUNC(d, 'IW') DATETRUNC(iso_week, d)※2022以降
切り捨て DATE_FORMATで代用 date_trunc('hour', ts) TRUNC(ts, 'HH') DATETRUNC(hour, ts)
年・月・日 YEAR(d) MONTH(d) DAY(d) EXTRACT(YEAR FROM d) EXTRACT(YEAR FROM d) YEAR(d) MONTH(d) DAY(d)
曜日(番号) DAYOFWEEK(d)(日=1)/ WEEKDAY(d)(月=0) EXTRACT(DOW FROM d)(日=0)/ ISODOW(月=1) TO_CHAR(d, 'D')※NLS設定に依存 DATEPART(weekday, d)※DATEFIRSTに依存
日付→文字列 DATE_FORMAT(d, '%Y/%m/%d') to_char(d, 'YYYY/MM/DD') TO_CHAR(d, 'YYYY/MM/DD') FORMAT(d, 'yyyy/MM/dd') / CONVERT(varchar(10), d, 111)
文字列→日付 STR_TO_DATE(s, '%Y/%m/%d') to_date(s, 'YYYY/MM/DD') TO_DATE(s, 'YYYY/MM/DD') CONVERT(date, s, 111) / TRY_CONVERT
日時→UNIX時間 UNIX_TIMESTAMP(ts) EXTRACT(EPOCH FROM ts) (日時 - DATE '1970-01-01') * 86400 DATEDIFF_BIG(second, '1970-01-01', ts)
UNIX時間→日時 FROM_UNIXTIME(n) to_timestamp(n) DATE '1970-01-01' + n / 86400 DATEADD(second, n, '1970-01-01')
タイムゾーン変換 CONVERT_TZ(ts, 'UTC', 'Asia/Tokyo') ts AT TIME ZONE 'Asia/Tokyo' FROM_TZ(ts, 'UTC') AT TIME ZONE 'Asia/Tokyo' ts AT TIME ZONE 'UTC' AT TIME ZONE 'Tokyo Standard Time'
期間で絞り込む col >= '2026-09-01' AND col < '2026-10-01' 同左 col >= DATE '2026-09-01' AND col < DATE '2026-10-01' 同左('20260901'形式が確実)

迷ったら「月末の扱い」「曜日番号の起点」「差分の数え方」の3点がDBで違うと覚えておくと、移行やレビューで役立ちます。

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

(記載例)出典:株式会社RJC「SQL 日付関数 一覧|4DB比較表とコピペSQL・実行結果つき」

検証環境と前提

項目 内容
PostgreSQL 16.13(SET timezone = 'Asia/Tokyo')
MySQL 8.0.46(SET time_zone = 'Asia/Tokyo'、タイムゾーン表はmysql_tzinfo_to_sqlで読み込み済み)
Oracle SQL Language Reference(Release 26)に準拠。未実行
SQL Server Microsoft Learn(SQL Server 2022/2025)に準拠。未実行

実行結果として載せているのはPostgreSQLとMySQLだけです。OracleとSQL Serverの結果は、ドキュメントに書かれた仕様から導いた値として「(ドキュメント準拠)」と書き分けています。

現在日時と今日の日付を取得する

「日時が欲しいのか、日付だけ欲しいのか」で関数を選びます。 日付だけなら時刻部分を落とした型(date)にしておくと、比較やグループ化が素直になります。

-- PostgreSQL 16
SET timezone = 'Asia/Tokyo';
SELECT now() AS now_ts, current_timestamp AS cur_ts, current_date AS today,
       localtimestamp AS local_ts, clock_timestamp() AS clock;
SELECT now()::date AS d1, CAST(now() AS date) AS d2;
            now_ts             |            cur_ts             |   today    |          local_ts          |             clock
-------------------------------+-------------------------------+------------+----------------------------+-------------------------------
 2026-09-29 12:56:47.701616+09 | 2026-09-29 12:56:47.701616+09 | 2026-09-29 | 2026-09-29 12:56:47.701616 | 2026-09-29 12:56:47.702066+09

     d1     |     d2
------------+------------
 2026-09-29 | 2026-09-29

PostgreSQLのnow()はトランザクション開始時刻で、同じトランザクション内では同じ値を返します。実行した瞬間の時刻が必要ならclock_timestamp()を使います(上の出力でも末尾の値がわずかに進んでいます)。

-- MySQL 8.0
SET time_zone = 'Asia/Tokyo';
SELECT NOW() AS now_ts, CURRENT_TIMESTAMP AS cur_ts, CURDATE() AS today,
       SYSDATE() AS sys, UTC_TIMESTAMP() AS utc;
SELECT DATE(NOW()) AS d1, CAST(NOW() AS DATE) AS d2;
+---------------------+---------------------+------------+---------------------+---------------------+
| now_ts              | cur_ts              | today      | sys                 | utc                 |
+---------------------+---------------------+------------+---------------------+---------------------+
| 2026-09-29 12:57:10 | 2026-09-29 12:57:10 | 2026-09-29 | 2026-09-29 12:57:10 | 2026-09-29 03:57:10 |
+---------------------+---------------------+------------+---------------------+---------------------+

MySQLのNOW()は文の開始時刻、SYSDATE()は関数を評価した時刻です。

-- Oracle(ドキュメント準拠)
SELECT SYSDATE, SYSTIMESTAMP, CURRENT_DATE, TRUNC(SYSDATE) AS today FROM dual;

-- SQL Server(ドキュメント準拠)
SELECT GETDATE() AS now_dt, SYSDATETIME() AS now_dt2, SYSDATETIMEOFFSET() AS now_off,
       GETUTCDATE() AS utc_dt, CAST(GETDATE() AS date) AS today;
DB 注意点
Oracle SYSDATEはDBサーバーのOSの時刻、CURRENT_DATEはセッションのタイムゾーンの時刻です。DATE型も時刻を持つため、日付だけにするにはTRUNCを使います
SQL Server GETDATE()はdatetime(精度は約3ミリ秒)、SYSDATETIME()はdatetime2(7)です。CURRENT_DATEはSQL Server 2025とAzure SQLで使えます

日付を加算・減算する(n日後・nか月後・n時間後)

日単位は足し算、月・年単位は専用の関数かINTERVALで書くのが4DB共通の考え方です。

-- PostgreSQL 16
SELECT DATE '2026-09-28' + 7                        AS plus7days,
       DATE '2026-09-28' - INTERVAL '1 month'       AS minus1month,
       TIMESTAMP '2026-09-28 10:00' + INTERVAL '90 minutes' AS plus90min,
       DATE '2026-09-28' + make_interval(years => 1) AS plus1year;
 plus7days  |     minus1month     |      plus90min      |      plus1year
------------+---------------------+---------------------+---------------------
 2026-10-05 | 2026-08-28 00:00:00 | 2026-09-28 11:30:00 | 2027-09-28 00:00:00

PostgreSQLではdate + integerはdateのまま、date + intervalはtimestampになります。日付として扱いたいときは::dateを付けます。

-- MySQL 8.0
SELECT DATE_ADD('2026-09-28', INTERVAL 7 DAY)      AS plus7days,
       DATE_SUB('2026-09-28', INTERVAL 1 MONTH)    AS minus1month,
       '2026-09-28 10:00:00' + INTERVAL 90 MINUTE  AS plus90min,
       DATE_ADD('2026-09-28', INTERVAL 1 YEAR)     AS plus1year;
+------------+-------------+---------------------+------------+
| plus7days  | minus1month | plus90min           | plus1year  |
+------------+-------------+---------------------+------------+
| 2026-10-05 | 2026-08-28  | 2026-09-28 11:30:00 | 2027-09-28 |
+------------+-------------+---------------------+------------+
-- Oracle(ドキュメント準拠)
SELECT DATE '2026-09-28' + 7 AS plus7days,
       ADD_MONTHS(DATE '2026-09-28', -1) AS minus1month,
       TIMESTAMP '2026-09-28 10:00:00' + INTERVAL '90' MINUTE AS plus90min,
       ADD_MONTHS(DATE '2026-09-28', 12) AS plus1year
FROM dual;

-- SQL Server(ドキュメント準拠)
SELECT DATEADD(day, 7, '2026-09-28')    AS plus7days,
       DATEADD(month, -1, '2026-09-28') AS minus1month,
       DATEADD(minute, 90, '2026-09-28 10:00') AS plus90min,
       DATEADD(year, 1, '2026-09-28')   AS plus1year;

OracleはDATE同士の演算が「日」単位の数値なので、d + 1/24で1時間後にもなります。

月末をまたぐ加算はDBで結果が変わる

「1月31日の1か月後」は4DBとも2月末になりますが、「2月28日(月末)の1か月後」はOracleのADD_MONTHSだけ3月31日になります。 請求の締め日や契約の更新日を計算するときに差が出るポイントです。

-- PostgreSQL 16
SELECT DATE '2026-01-31' + INTERVAL '1 month' AS jan31_plus1m,
       DATE '2024-01-31' + INTERVAL '1 month' AS leap_jan31_plus1m,
       DATE '2026-03-31' - INTERVAL '1 month' AS mar31_minus1m,
       DATE '2024-02-29' + INTERVAL '1 year'  AS feb29_plus1y;
SELECT DATE '2026-02-28' + INTERVAL '1 month' AS feb28_plus1m;
    jan31_plus1m     |  leap_jan31_plus1m  |    mar31_minus1m    |    feb29_plus1y
---------------------+---------------------+---------------------+---------------------
 2026-02-28 00:00:00 | 2024-02-29 00:00:00 | 2026-02-28 00:00:00 | 2025-02-28 00:00:00

    feb28_plus1m
---------------------
 2026-03-28 00:00:00

MySQL 8.0でも同じSQLをDATE_ADD/DATE_SUBで実行し、同じ日付(2026-02-28、2024-02-29、2026-02-28、2025-02-28、2026-03-28)になりました。

計算 MySQL(実行確認済み) PostgreSQL(実行確認済み) Oracle ADD_MONTHS(ドキュメント準拠) Oracle + INTERVAL(ドキュメント準拠) SQL Server DATEADD(ドキュメント準拠)
2026-01-31 + 1か月 2026-02-28 2026-02-28 2026-02-28 エラー ORA-01839 2026-02-28
2026-03-31 − 1か月 2026-02-28 2026-02-28 2026-02-28 エラー ORA-01839 2026-02-28
2026-02-28 + 1か月 2026-03-28 2026-03-28 2026-03-31 2026-03-28 2026-03-28

OracleのADD_MONTHSは、ドキュメントに「元の日付が月の最終日であるか、結果の月の日数が元の日より少ない場合、結果はその月の最終日」と定義されています。そのため月末同士の対応を保ちます。一方、+ INTERVAL '1' MONTHは存在しない日付(2月31日)を作ろうとしてORA-01839: date not valid for month specifiedになります。

月の加算は「まとめて足す」と「1か月ずつ足す」で結果が変わる点も押さえておきます。

-- PostgreSQL 16
SELECT (DATE '2026-01-31' + INTERVAL '1 month' + INTERVAL '1 month')::date AS twice,
       (DATE '2026-01-31' + INTERVAL '2 month')::date AS once2;
   twice    |   once2
------------+------------
 2026-03-28 | 2026-03-31

MySQLもtwice = 2026-03-28、once2 = 2026-03-31でした。毎月の予定日を並べるときは、前回の結果に足し続けるのではなく、起点の日付に「nか月」を足すと月末がずれません。

日付の差分を求める(日数・月数・時間)

差分は「何を数えるか」がDBで違います。 特にSQL ServerのDATEDIFFは「期間の長さ」ではなく「またいだ境界の数」を返します。

-- PostgreSQL 16
SELECT DATE '2026-09-28' - DATE '2026-01-01' AS days,
       age(DATE '2026-09-28', DATE '2026-01-31') AS age_interval,
       EXTRACT(YEAR FROM age(DATE '2026-09-28', DATE '2026-01-31')) * 12
         + EXTRACT(MONTH FROM age(DATE '2026-09-28', DATE '2026-01-31')) AS months,
       EXTRACT(EPOCH FROM TIMESTAMP '2026-09-28 18:30' - TIMESTAMP '2026-09-28 09:00') / 3600 AS hours;
 days |  age_interval  | months |       hours
------+----------------+--------+--------------------
  270 | 7 mons 28 days |      7 | 9.5000000000000000
-- MySQL 8.0
SELECT DATEDIFF('2026-09-28', '2026-01-01') AS days,
       TIMESTAMPDIFF(MONTH, '2026-01-31', '2026-09-28') AS months,
       TIMESTAMPDIFF(MINUTE, '2026-09-28 09:00', '2026-09-28 18:30') / 60 AS hours,
       TIMESTAMPDIFF(DAY, '2026-09-28 23:00', '2026-09-29 01:00') AS tsdiff_day,
       DATEDIFF('2026-09-29 01:00', '2026-09-28 23:00') AS datediff_day;
+------+--------+--------+------------+--------------+
| days | months | hours  | tsdiff_day | datediff_day |
+------+--------+--------+------------+--------------+
|  270 |      7 | 9.5000 |          0 |            1 |
+------+--------+--------+------------+--------------+

MySQLのDATEDIFFは時刻を無視して日付部分の差(1)、TIMESTAMPDIFF(DAY, ...)は経過した丸1日の数(2時間なので0)を返します。引数の順番も逆(DATEDIFF(終了, 開始)、TIMESTAMPDIFF(単位, 開始, 終了))なので注意します。

-- Oracle(ドキュメント準拠)
SELECT DATE '2026-09-28' - DATE '2026-01-01' AS days,           -- 270
       MONTHS_BETWEEN(DATE '2026-09-28', DATE '2026-01-31') AS months, -- 小数を含む
       (TIMESTAMP '2026-09-28 18:30:00' - TIMESTAMP '2026-09-28 09:00:00') AS diff -- INTERVAL DAY TO SECOND
FROM dual;

-- SQL Server(ドキュメント準拠)
SELECT DATEDIFF(day, '2026-01-01', '2026-09-28') AS days,        -- 270
       DATEDIFF(month, '2026-01-31', '2026-02-01') AS months,     -- 1(境界を1回またぐ)
       DATEDIFF(year, '2025-12-31', '2026-01-01') AS years,       -- 1
       DATEDIFF(minute, '2026-09-28 09:00', '2026-09-28 18:30') / 60.0 AS hours;
計算 MySQL(実行確認済み) PostgreSQL(実行確認済み) SQL Server(ドキュメント準拠)
1/31 → 2/1 の月数 TIMESTAMPDIFF(MONTH) = 0 age = 1 day DATEDIFF(month) = 1
12/31 → 1/1 の年数 TIMESTAMPDIFF(YEAR) = 0 ageの年 = 0 DATEDIFF(year) = 1

「満何か月」「満何年」が欲しいときにSQL ServerのDATEDIFFをそのまま使うと、1日違いでも1と数えます。 次の年齢計算の補正式を使います。

年齢を計算する(誕生日の前日・当日)

年齢は「誕生日の当日に1つ増える」ので、前日と当日で確認します。

-- PostgreSQL 16
SELECT EXTRACT(YEAR FROM age(DATE '2026-09-28', DATE '1990-09-29')) AS age_day_before_bday,
       EXTRACT(YEAR FROM age(DATE '2026-09-29', DATE '1990-09-29')) AS age_on_bday;

-- MySQL 8.0
SELECT TIMESTAMPDIFF(YEAR, '1990-09-29', '2026-09-28') AS age_day_before_bday,
       TIMESTAMPDIFF(YEAR, '1990-09-29', '2026-09-29') AS age_on_bday;
 age_day_before_bday | age_on_bday
---------------------+-------------
                  35 |          36

MySQLも同じく35と36でした。

-- Oracle(ドキュメント準拠)
SELECT TRUNC(MONTHS_BETWEEN(DATE '2026-09-29', DATE '1990-09-29') / 12) AS age FROM dual;

-- SQL Server(ドキュメント準拠):DATEDIFF(year)を誕生日前なら1引く
DECLARE @birth date = '1990-09-29', @base date = '2026-09-28';
SELECT DATEDIFF(year, @birth, @base)
       - CASE WHEN DATEADD(year, DATEDIFF(year, @birth, @base), @birth) > @base THEN 1 ELSE 0 END AS age;

どのDBでも使える方法として「yyyymmddの数値の差を10000で割って切り捨てる」計算があります。PostgreSQLとMySQLで実行し、前日35・当日36を確認しました。

-- PostgreSQL 16
SELECT (to_char(DATE '2026-09-28', 'YYYYMMDD')::int - to_char(DATE '1990-09-29', 'YYYYMMDD')::int) / 10000 AS age;
-- MySQL 8.0
SELECT FLOOR((DATE_FORMAT('2026-09-28', '%Y%m%d') - DATE_FORMAT('1990-09-29', '%Y%m%d')) / 10000) AS age;

月初・月末・週の始まりを求める

月末は「翌月1日の前日」と考えると、うるう年も含めて正確です。専用関数(LAST_DAY、EOMONTH)があるDBはそれを使います。

-- PostgreSQL 16
SELECT date_trunc('month', DATE '2026-09-28')::date AS month_start,
       (date_trunc('month', DATE '2026-09-28') + INTERVAL '1 month - 1 day')::date AS month_end,
       (date_trunc('month', DATE '2026-02-10') + INTERVAL '1 month - 1 day')::date AS feb_end,
       date_trunc('week', DATE '2026-09-30')::date AS week_start_monday,
       (DATE '2026-09-30' - EXTRACT(DOW FROM DATE '2026-09-30')::int) AS week_start_sunday;
 month_start | month_end  |  feb_end   | week_start_monday | week_start_sunday
-------------+------------+------------+-------------------+-------------------
 2026-09-01  | 2026-09-30 | 2026-02-28 | 2026-09-28        | 2026-09-27
-- MySQL 8.0
SELECT DATE_FORMAT('2026-09-28', '%Y-%m-01') AS month_start_str,
       LAST_DAY('2026-09-28') - INTERVAL DAY(LAST_DAY('2026-09-28')) - 1 DAY AS month_start,
       LAST_DAY('2026-09-28') AS month_end,
       LAST_DAY('2026-02-10') AS feb_end,
       DATE_SUB('2026-09-30', INTERVAL WEEKDAY('2026-09-30') DAY) AS week_start_monday,
       DATE_SUB('2026-09-30', INTERVAL DAYOFWEEK('2026-09-30') - 1 DAY) AS week_start_sunday;

MySQL 8.0の実行結果は、PostgreSQLと同じく2026-09-01、2026-09-01、2026-09-30、2026-02-28、2026-09-28、2026-09-27でした。

DATE_FORMATで作った月初は文字列です。日付型が必要な場合は2列目の書き方か、CAST(... AS DATE)を使います。

-- Oracle(ドキュメント準拠)
SELECT TRUNC(DATE '2026-09-28', 'MM') AS month_start,
       LAST_DAY(DATE '2026-09-28')    AS month_end,
       TRUNC(DATE '2026-09-30', 'IW') AS week_start_monday
FROM dual;

-- SQL Server(ドキュメント準拠)
DECLARE @d date = '2026-09-30';
SELECT DATEFROMPARTS(YEAR(@d), MONTH(@d), 1) AS month_start,   -- 全バージョン
       EOMONTH(@d)                            AS month_end,     -- EOMONTH(@d, 1) で翌月末
       DATETRUNC(iso_week, @d)                AS week_start_monday, -- SQL Server 2022以降
       DATEADD(day, -((DATEPART(weekday, @d) + @@DATEFIRST - 2) % 7), @d) AS week_start_monday_any;

SQL ServerのDATETRUNC(week, d)はSET DATEFIRSTの設定(米国英語の既定は日曜)に従うため、月曜始まりに固定したいときはiso_weekを使います。最後の式はDATEFIRSTの設定に関係なく月曜を返す書き方です。

日時を切り捨てる(年・四半期・月・時間単位)

集計の単位にそろえる処理が「切り捨て」です。PostgreSQLはdate_trunc、OracleはTRUNC、SQL Server 2022以降はDATETRUNC、MySQLは専用関数がないためDATE_FORMATで組み立てます。

-- PostgreSQL 16
SELECT date_trunc('year',    TIMESTAMP '2026-09-28 14:35:50') AS y,
       date_trunc('month',   TIMESTAMP '2026-09-28 14:35:50') AS m,
       date_trunc('hour',    TIMESTAMP '2026-09-28 14:35:50') AS h,
       date_trunc('quarter', TIMESTAMP '2026-09-28 14:35:50') AS q;
          y          |          m          |          h          |          q
---------------------+---------------------+---------------------+---------------------
 2026-01-01 00:00:00 | 2026-09-01 00:00:00 | 2026-09-28 14:00:00 | 2026-07-01 00:00:00
-- MySQL 8.0
SELECT DATE_FORMAT('2026-09-28 14:35:50', '%Y-01-01 00:00:00') AS y,
       DATE_FORMAT('2026-09-28 14:35:50', '%Y-%m-01 00:00:00') AS m,
       DATE_FORMAT('2026-09-28 14:35:50', '%Y-%m-%d %H:00:00') AS h,
       MAKEDATE(YEAR('2026-09-28'), 1) + INTERVAL QUARTER('2026-09-28') - 1 QUARTER AS q;

MySQL 8.0の実行結果は、y=2026-01-01 00:00:00、m=2026-09-01 00:00:00、h=2026-09-28 14:00:00、q=2026-07-01で、PostgreSQLと同じ値です(MySQL側は文字列または日付型で返ります)。

単位 Oracle(ドキュメント準拠) SQL Server 2022以降(ドキュメント準拠)
年 TRUNC(ts, 'YYYY') DATETRUNC(year, ts)
四半期 TRUNC(ts, 'Q') DATETRUNC(quarter, ts)
月 TRUNC(ts, 'MM') DATETRUNC(month, ts)
日 TRUNC(ts) DATETRUNC(day, ts)
時 TRUNC(ts, 'HH') DATETRUNC(hour, ts)

年・月・日・曜日を取り出す

曜日の番号はDBごとに起点が違います。 2026-09-28(月曜日)で比べます。

-- PostgreSQL 16
SELECT EXTRACT(YEAR FROM DATE '2026-09-28') AS y, EXTRACT(MONTH FROM DATE '2026-09-28') AS m,
       EXTRACT(DAY FROM DATE '2026-09-28') AS d, EXTRACT(DOW FROM DATE '2026-09-28') AS dow,
       EXTRACT(ISODOW FROM DATE '2026-09-28') AS isodow, EXTRACT(QUARTER FROM DATE '2026-09-28') AS q,
       EXTRACT(WEEK FROM DATE '2026-09-28') AS isoweek, date_part('doy', DATE '2026-09-28') AS doy,
       to_char(DATE '2026-09-28', 'Dy') AS dy;
  y   | m | d  | dow | isodow | q | isoweek | doy | dy
------+---+----+-----+--------+---+---------+-----+-----
 2026 | 9 | 28 |   1 |      1 | 3 |      40 | 271 | Mon
-- MySQL 8.0
SELECT YEAR('2026-09-28') AS y, MONTH('2026-09-28') AS m, DAY('2026-09-28') AS d,
       DAYOFWEEK('2026-09-28') AS dayofweek, WEEKDAY('2026-09-28') AS weekday,
       EXTRACT(QUARTER FROM '2026-09-28') AS q, WEEK('2026-09-28', 3) AS isoweek,
       DAYOFYEAR('2026-09-28') AS doy, DAYNAME('2026-09-28') AS dname;
+------+------+------+-----------+---------+------+---------+------+--------+
| y    | m    | d    | dayofweek | weekday | q    | isoweek | doy  | dname  |
+------+------+------+-----------+---------+------+---------+------+--------+
| 2026 |    9 |   28 |         2 |       0 |    3 |      40 |  271 | Monday |
+------+------+------+-----------+---------+------+---------+------+--------+
関数 月曜の値 日曜の値 起点
PostgreSQL EXTRACT(DOW) 1 0 日曜=0
PostgreSQL EXTRACT(ISODOW) 1 7 月曜=1(ISO)
MySQL DAYOFWEEK() 2 1 日曜=1
MySQL WEEKDAY() 0 6 月曜=0
Oracle TO_CHAR(d, 'D') 設定次第 設定次第 NLS_TERRITORYに依存
SQL Server DATEPART(weekday, d) 既定2 既定1 SET DATEFIRSTに依存

OracleとSQL Serverはセッション設定で番号が変わるため、曜日で条件を書くときはOracleならTO_CHAR(d, 'DY', 'NLS_DATE_LANGUAGE=ENGLISH')、SQL Serverなら上記の@@DATEFIRSTで補正する式のように、設定に左右されない書き方にしておくと安心です。

日付と文字列を変換する(書式指定)

書式記号はDBごとに別物です。特に「分」はMySQLが%i、PostgreSQL・OracleがMI、SQL ServerのFORMATがmm(月はMM)です。

-- PostgreSQL 16
SELECT to_char(TIMESTAMP '2026-09-28 14:05:09', 'YYYY/MM/DD HH24:MI:SS') AS fmt1,
       to_char(DATE '2026-09-28', 'YYYY"年"FMMM"月"FMDD"日"') AS fmt_ja,
       to_char(DATE '2026-09-28', 'YYYYMM') AS yyyymm,
       to_date('2026/09/28', 'YYYY/MM/DD') AS parsed_date,
       to_timestamp('2026-09-28 14:05', 'YYYY-MM-DD HH24:MI') AS parsed_ts;
        fmt1         |    fmt_ja     | yyyymm | parsed_date |       parsed_ts
---------------------+---------------+--------+-------------+------------------------
 2026/09/28 14:05:09 | 2026年9月28日 | 202609 | 2026-09-28  | 2026-09-28 14:05:00+09
-- MySQL 8.0
SELECT DATE_FORMAT('2026-09-28 14:05:09', '%Y/%m/%d %H:%i:%s') AS fmt1,
       DATE_FORMAT('2026-09-28', '%Y年%c月%e日') AS fmt_ja,
       DATE_FORMAT('2026-09-28', '%Y%m') AS yyyymm,
       STR_TO_DATE('2026/09/28', '%Y/%m/%d') AS parsed_date,
       STR_TO_DATE('2026-09-28 14:05', '%Y-%m-%d %H:%i') AS parsed_ts;

MySQL 8.0の実行結果も、2026/09/28 14:05:09、2026年9月28日、202609、2026-09-28、2026-09-28 14:05:00でした。

-- Oracle(ドキュメント準拠)
SELECT TO_CHAR(SYSDATE, 'YYYY/MM/DD HH24:MI:SS') AS fmt1,
       TO_DATE('2026/09/28', 'YYYY/MM/DD') AS parsed_date
FROM dual;

-- SQL Server(ドキュメント準拠)
SELECT FORMAT(GETDATE(), 'yyyy/MM/dd HH:mm:ss') AS fmt1,
       CONVERT(varchar(10), GETDATE(), 111) AS style111,   -- yyyy/mm/dd
       CONVERT(varchar(8),  GETDATE(), 112) AS style112,   -- yyyymmdd
       CONVERT(date, '2026/09/28', 111) AS parsed_date,
       TRY_CONVERT(date, '2026/02/30', 111) AS invalid_is_null;
要素 MySQL PostgreSQL / Oracle SQL Server FORMAT
年4桁 %Y YYYY yyyy
月2桁 %m MM MM
日2桁 %d DD dd
時(24時間) %H HH24 HH
分 %i MI mm
秒 %s SS ss

よくある失敗は「分」に月の記号を書いてしまうことです(PostgreSQLでHH24:MMと書くと月が出ます)。SQL ServerのFORMATは柔軟な反面、大量の行ではCONVERTのスタイル番号のほうが軽く動きます。

UNIX時間と相互変換する

UNIX時間は「1970-01-01 00:00:00 UTCからの秒数」なので、タイムゾーンの扱いをそろえるのがポイントです。

-- PostgreSQL 16
SELECT EXTRACT(EPOCH FROM TIMESTAMPTZ '2026-09-28 00:00:00+09')::bigint AS to_unix,
       to_timestamp(1790521200) AS from_unix;
  to_unix   |       from_unix
------------+------------------------
 1790521200 | 2026-09-28 00:00:00+09
-- MySQL 8.0(セッションのtime_zoneがAsia/Tokyo)
SELECT UNIX_TIMESTAMP('2026-09-28 00:00:00') AS to_unix, FROM_UNIXTIME(1790521200) AS from_unix;
+------------+---------------------+
| to_unix    | from_unix           |
+------------+---------------------+
| 1790521200 | 2026-09-28 00:00:00 |
+------------+---------------------+

MySQLのUNIX_TIMESTAMPとFROM_UNIXTIMEはセッションのtime_zoneで解釈されます。サーバーの設定が違うと値がずれるため、UTCで保存・比較するならSET time_zone = '+00:00'をそろえます。

-- Oracle(ドキュメント準拠):UTCの日時として計算
SELECT ROUND((CAST(SYS_EXTRACT_UTC(SYSTIMESTAMP) AS DATE) - DATE '1970-01-01') * 86400) AS to_unix,
       DATE '1970-01-01' + 1790521200 / 86400 AS from_unix_utc
FROM dual;

-- SQL Server(ドキュメント準拠)
SELECT DATEDIFF_BIG(second, '1970-01-01', GETUTCDATE()) AS to_unix,
       DATEADD(second, 1790521200, '1970-01-01') AS from_unix_utc;

タイムゾーンを変換する(UTC ⇔ 日本時間)

「UTCで保存し、表示するときに日本時間へ変換」が扱いやすい基本形です。

-- PostgreSQL 16
SELECT TIMESTAMPTZ '2026-09-28 09:00:00+00' AS utc_input_shown_in_session_tz,
       TIMESTAMPTZ '2026-09-28 09:00:00+00' AT TIME ZONE 'Asia/Tokyo' AS as_tokyo_local,
       TIMESTAMP '2026-09-28 18:00:00' AT TIME ZONE 'Asia/Tokyo' AT TIME ZONE 'UTC' AS tokyo_to_utc;
 utc_input_shown_in_session_tz |   as_tokyo_local    |    tokyo_to_utc
-------------------------------+---------------------+---------------------
 2026-09-28 18:00:00+09        | 2026-09-28 18:00:00 | 2026-09-28 09:00:00

PostgreSQLのAT TIME ZONEは入力の型で意味が反転します。timestamptz AT TIME ZONE 'Asia/Tokyo'は「その地域の現地時刻(タイムゾーンなし)に変換」、timestamp AT TIME ZONE 'Asia/Tokyo'は「この時刻を東京の時刻とみなしてtimestamptzにする」です。

-- MySQL 8.0
SELECT CONVERT_TZ('2026-09-28 09:00:00', 'UTC', 'Asia/Tokyo') AS utc_to_tokyo,
       CONVERT_TZ('2026-09-28 18:00:00', 'Asia/Tokyo', 'UTC') AS tokyo_to_utc,
       CONVERT_TZ('2026-09-28 09:00:00', '+00:00', '+09:00') AS offset_style;
+---------------------+---------------------+---------------------+
| utc_to_tokyo        | tokyo_to_utc        | offset_style        |
+---------------------+---------------------+---------------------+
| 2026-09-28 18:00:00 | 2026-09-28 09:00:00 | 2026-09-28 18:00:00 |
+---------------------+---------------------+---------------------+

MySQLで'Asia/Tokyo'のような地域名を使うには、タイムゾーン表の読み込み(mysql_tzinfo_to_sql)が必要です。読み込んでいない環境ではCONVERT_TZはNULLを返すので、その場合は'+09:00'形式で書きます。

-- Oracle(ドキュメント準拠)
SELECT FROM_TZ(TIMESTAMP '2026-09-28 09:00:00', 'UTC') AT TIME ZONE 'Asia/Tokyo' AS utc_to_tokyo FROM dual;

-- SQL Server(ドキュメント準拠、SQL Server 2016以降):Windowsのタイムゾーン名を使う
SELECT CAST('2026-09-28 09:00:00' AS datetime2) AT TIME ZONE 'UTC' AT TIME ZONE 'Tokyo Standard Time' AS utc_to_tokyo;

期間で絞り込む(範囲条件の正しい書き方)

期間の絞り込みは「開始以上・終了の翌日未満(半開区間)」で書くのが確実です。20万行の注文テーブル(7分おき、ordered_atにインデックスあり)で確かめます。

-- PostgreSQL 16:テストデータ
CREATE TABLE orders(id serial primary key, ordered_at timestamp not null, amount int);
INSERT INTO orders(ordered_at, amount)
SELECT TIMESTAMP '2025-01-01' + (g * INTERVAL '7 minutes'), (g % 100) + 1
FROM generate_series(1, 200000) g;
CREATE INDEX idx_orders_ordered_at ON orders(ordered_at);
ANALYZE orders;

SELECT count(*) AS sept_rows FROM orders
WHERE ordered_at >= DATE '2026-09-01' AND ordered_at < DATE '2026-10-01';
SELECT count(*) AS between_wrong FROM orders
WHERE ordered_at BETWEEN DATE '2026-09-01' AND DATE '2026-09-30';
SELECT count(*) AS last_day_rows FROM orders
WHERE ordered_at >= DATE '2026-09-30' AND ordered_at < DATE '2026-10-01';
 sept_rows
-----------
      6171

 between_wrong
---------------
          5966

 last_day_rows
---------------
           206

BETWEEN '2026-09-01' AND '2026-09-30'は「9月30日 00:00:00まで」という意味になり、30日の0時ちょうどの1件を除く205件が抜けました(6171 − 206 + 1 = 5966)。MySQL 8.0で同じデータを作っても6171と5966で、同じ結果です。

列に関数をかけるとインデックスが使われない

「9月分」をto_char(ordered_at, 'YYYY-MM') = '2026-09'のように書くと、全行に関数を計算するため索引が使われません。

-- PostgreSQL 16:直す前
EXPLAIN SELECT count(*) FROM orders WHERE to_char(ordered_at, 'YYYY-MM') = '2026-09';
 Finalize Aggregate  (cost=4040.29..4040.30 rows=1 width=8)
   ->  Gather  (cost=4040.18..4040.29 rows=1 width=8)
         Workers Planned: 1
         ->  Partial Aggregate  (cost=3040.18..3040.19 rows=1 width=8)
               ->  Parallel Seq Scan on orders  (cost=0.00..3038.71 rows=588 width=0)
                     Filter: (to_char(ordered_at, 'YYYY-MM'::text) = '2026-09'::text)
-- PostgreSQL 16:直した後(列はそのまま、値の側で範囲を作る)
EXPLAIN SELECT count(*) FROM orders WHERE ordered_at >= DATE '2026-09-01' AND ordered_at < DATE '2026-10-01';
 Aggregate  (cost=247.76..247.77 rows=1 width=8)
   ->  Index Only Scan using idx_orders_ordered_at on orders  (cost=0.42..232.50 rows=6104 width=0)
         Index Cond: ((ordered_at >= '2026-09-01'::date) AND (ordered_at < '2026-10-01'::date))

推定コストは4040 → 247に下がりました。MySQL 8.0でも同じ傾向です。

-- MySQL 8.0:EXPLAIN(抜粋)
直す前  DATE_FORMAT(ordered_at, '%Y-%m') = '2026-09' → type=index(索引を全件走査) rows=198378
直した後 ordered_at >= '2026-09-01' AND ordered_at < '2026-10-01' → type=range rows=6171
書き方 結果 索引
ordered_at >= '2026-09-01' AND ordered_at < '2026-10-01' 正しい(6171件) 使われる
ordered_at BETWEEN '2026-09-01' AND '2026-09-30' 最終日が抜ける(5966件) 使われる
to_char(ordered_at, 'YYYY-MM') = '2026-09' 正しい 使われない
CAST(ordered_at AS date) = '2026-09-28' 正しい DBによって異なる(関数インデックスで対応可)

「今月分」「先月分」も同じ形で、値の側を関数で作ります。

-- PostgreSQL:今月分
WHERE ordered_at >= date_trunc('month', CURRENT_DATE)
  AND ordered_at <  date_trunc('month', CURRENT_DATE) + INTERVAL '1 month'
-- MySQL:今月分
WHERE ordered_at >= DATE_FORMAT(CURDATE(), '%Y-%m-01')
  AND ordered_at <  DATE_FORMAT(CURDATE(), '%Y-%m-01') + INTERVAL 1 MONTH
-- Oracle:今月分
WHERE ordered_at >= TRUNC(SYSDATE, 'MM') AND ordered_at < ADD_MONTHS(TRUNC(SYSDATE, 'MM'), 1)
-- SQL Server:今月分
WHERE ordered_at >= DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1)
  AND ordered_at <  DATEADD(month, 1, DATEFROMPARTS(YEAR(GETDATE()), MONTH(GETDATE()), 1))

応用:月別集計と連続した日付の生成

月別集計は「切り捨てた日付」でグループ化します。

-- PostgreSQL 16
SELECT date_trunc('month', ordered_at)::date AS month, count(*) AS orders, sum(amount) AS total
FROM orders
WHERE ordered_at >= DATE '2026-07-01' AND ordered_at < DATE '2026-10-01'
GROUP BY 1 ORDER BY 1;
   month    | orders | total
------------+--------+--------
 2026-07-01 |   6378 | 322791
 2026-08-01 |   6377 | 321199
 2026-09-01 |   6171 | 311331

データがない日も0件として表示したいときは、カレンダーを生成してLEFT JOINします。PostgreSQLはgenerate_seriesが使えます。

-- PostgreSQL 16
SELECT d::date AS day, to_char(d, 'Dy') AS dy
FROM generate_series(DATE '2026-09-28', DATE '2026-10-04', INTERVAL '1 day') d;
    day     | dy
------------+-----
 2026-09-28 | Mon
 2026-09-29 | Tue
 2026-09-30 | Wed
 2026-10-01 | Thu
 2026-10-02 | Fri
 2026-10-03 | Sat
 2026-10-04 | Sun

MySQL 8.0は再帰CTE(WITH RECURSIVE)、OracleはCONNECT BY LEVEL、SQL Server 2022以降はGENERATE_SERIES(数値を生成してDATEADDで日付に変換)で同じ表を作れます。

つまずきやすいポイントのチェックリスト

チェック 起きること こうするとよい
期間をBETWEENで書いていないか 日時型の列で最終日の0時以降が抜ける >= 開始 AND < 翌日
列に関数をかけていないか 索引が使われず全件走査 値の側で範囲を作る
月の加算を繰り返していないか 1/31 → 2/28 → 3/28 とずれる 起点に「nか月」を足す
Oracleで+ INTERVAL '1' MONTHを使っていないか 月末でORA-01839 ADD_MONTHSを使う
SQL ServerのDATEDIFFを満年数に使っていないか 境界をまたぐだけで1になる 誕生日前なら1引く補正
曜日番号の起点を確認したか DB・設定で値が違う ISO(月曜=1)で統一
セッションのタイムゾーンを確認したか UNIX時間・現在日時がずれる 接続時に明示的に設定

よくある質問

Q. 4つのDBで共通に使える日付関数はありますか? A. CURRENT_TIMESTAMPとEXTRACT(YEAR FROM d)は4DBで使えます(SQL ServerはEXTRACTの代わりにYEAR()/DATEPART)。加算・差分・書式変換は関数名が異なるため、早見表のようにDBごとに書き分けるのが確実です。

Q. 「1月31日の1か月後」はどうなりますか? A. 4DBとも2月末(2026年なら2月28日)になります。ただしOracleで+ INTERVAL '1' MONTHと書くとエラー(ORA-01839)になり、ADD_MONTHSでは2月28日の1か月後が3月31日になるなど、月末の扱いに差があります。

Q. 日付の範囲検索でBETWEENは使ってはいけませんか? A. 列がdate型(時刻なし)ならBETWEENでも正しく動きます。 列が日時型の場合は最終日の0時以降が含まれないため、>= 開始日 AND < 翌日の形にしておくと、型が変わっても同じ結果になります。

Q. MySQLとMariaDBの日付関数は同じですか? A. 主要な関数(DATE_ADD、DATEDIFF、DATE_FORMAT、LAST_DAYなど)は共通です。この記事の実行結果はMySQL 8.0.46のもので、細かな挙動は各製品のドキュメントで確認してください。

まとめ

  • 早見表で4DBの書き方を横並びで確認でき、PostgreSQL 16とMySQL 8.0は実行結果つき
  • 月の加算は月末の扱いがDBで違う(OracleのADD_MONTHSは月末同士を対応させ、+ INTERVALはエラー)
  • 差分は「何を数えるか」を確認する(SQL ServerのDATEDIFFは境界の数、MySQLのDATEDIFFは時刻を無視)
  • 年齢は誕生日の前日と当日で確認し、yyyymmddの差を10000で割る方法は全DBで使える
  • 曜日番号の起点とタイムゾーンはセッション設定に左右されるので明示する
  • 期間の絞り込みは>= 開始 AND < 翌日で書き、列に関数をかけずに索引を活かす

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

(記載例)出典:株式会社RJC「SQL 日付関数 一覧|4DB比較表とコピペSQL・実行結果つき」

株式会社RJCでは、複数のデータベースを扱うプロジェクトの中で、こうした細かな違いを仲間と確かめ合いながら知識を共有しています。一つひとつの検証を積み重ねて、日々技術を磨いています。

株式会社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で一緒に開発しながら、 成長を楽しみませんか?