「今の時刻をデータベースに記録したい」 「ログの記録時刻を正確に保存したい」 「日本時間?UTC?タイムゾーンがよくわからない…」
PostgreSQLで現在時刻を取得する方法、実はたくさんあるんです。 NOW()
、CURRENT_TIMESTAMP
、CURRENT_DATE
… どれを使えばいいか、迷いますよね。
しかも、タイムゾーンの扱いを間違えると、 9時間ずれた時刻が記録されてしまうことも。
この記事では、PostgreSQLの時刻取得機能を完全に理解できるよう、 基本から応用まで、実例たっぷりでお伝えします。
正確な時刻管理で、信頼できるシステムを作りましょう!
1. PostgreSQLの時刻取得関数、一覧で理解

🕐 主要な時刻取得関数の比較
まずは、どんな関数があるのか整理しましょう。
関数名 | 戻り値の型 | タイムゾーン | 用途 |
---|---|---|---|
NOW() | timestamp with time zone | あり | 最も汎用的 |
CURRENT_TIMESTAMP | timestamp with time zone | あり | SQL標準 |
CURRENT_DATE | date | なし | 日付のみ |
CURRENT_TIME | time with time zone | あり | 時刻のみ |
LOCALTIMESTAMP | timestamp | なし | ローカル時刻 |
LOCALTIME | time | なし | ローカル時刻(時刻のみ) |
🕐 実際に使ってみよう
-- それぞれの関数を実行
SELECT
NOW() AS now_result,
CURRENT_TIMESTAMP AS current_timestamp_result,
CURRENT_DATE AS current_date_result,
CURRENT_TIME AS current_time_result,
LOCALTIMESTAMP AS localtimestamp_result,
LOCALTIME AS localtime_result;
実行結果例:
now_result: 2025-01-15 14:30:45.123456+09
current_timestamp_result: 2025-01-15 14:30:45.123456+09
current_date_result: 2025-01-15
current_time_result: 14:30:45.123456+09
localtimestamp_result: 2025-01-15 14:30:45.123456
localtime_result: 14:30:45.123456
ポイント:
+09
はタイムゾーン情報(日本時間)- NOW() と CURRENT_TIMESTAMP は同じ結果
- LOCAL系はタイムゾーン情報なし
2. NOW()関数:最も使われる現在時刻取得
📌 NOW()の基本的な使い方
-- 現在時刻を取得
SELECT NOW();
-- 結果: 2025-01-15 14:30:45.123456+09
-- テーブルに記録
CREATE TABLE access_logs (
log_id SERIAL PRIMARY KEY,
user_id INT,
action VARCHAR(100),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- データ挿入時に自動で現在時刻が入る
INSERT INTO access_logs (user_id, action)
VALUES (1, 'ログイン');
-- 確認
SELECT * FROM access_logs;
📌 NOW()の特徴:トランザクション内で固定
重要な特徴: NOW()はトランザクション開始時刻を返します。
BEGIN;
SELECT NOW(); -- 2025-01-15 14:30:00
-- 3秒待つ
SELECT pg_sleep(3);
SELECT NOW(); -- 2025-01-15 14:30:00(同じ時刻!)
COMMIT;
この特徴は、一連の処理に同じタイムスタンプを付けたい時に便利です。
📌 リアルタイムが必要な場合:CLOCK_TIMESTAMP()
BEGIN;
SELECT CLOCK_TIMESTAMP(); -- 2025-01-15 14:30:00.123456
SELECT pg_sleep(3);
SELECT CLOCK_TIMESTAMP(); -- 2025-01-15 14:30:03.456789(3秒後!)
COMMIT;
使い分け:
- NOW(): トランザクション全体で一貫性が必要
- CLOCK_TIMESTAMP(): 実行時点の正確な時刻が必要
3. CURRENT_TIMESTAMPとその仲間たち
📅 CURRENT_DATE:日付だけが欲しい時
-- 今日の日付を取得
SELECT CURRENT_DATE;
-- 結果: 2025-01-15
-- 今日のデータを検索
SELECT * FROM orders
WHERE order_date = CURRENT_DATE;
-- 今月の初日を計算
SELECT DATE_TRUNC('month', CURRENT_DATE) AS first_day_of_month;
-- 結果: 2025-01-01
-- 今月の最終日を計算
SELECT
DATE_TRUNC('month', CURRENT_DATE) + INTERVAL '1 month' - INTERVAL '1 day'
AS last_day_of_month;
-- 結果: 2025-01-31
📅 CURRENT_TIME:時刻だけが欲しい時
-- 現在時刻を取得
SELECT CURRENT_TIME;
-- 結果: 14:30:45.123456+09
-- 営業時間内かチェック
SELECT
CASE
WHEN CURRENT_TIME BETWEEN '09:00:00'::TIME AND '18:00:00'::TIME
THEN '営業時間内'
ELSE '営業時間外'
END AS business_status;
📅 精度の指定
-- マイクロ秒まで不要な場合は精度を指定
SELECT CURRENT_TIMESTAMP(0); -- 秒まで: 2025-01-15 14:30:45+09
SELECT CURRENT_TIMESTAMP(3); -- ミリ秒まで: 2025-01-15 14:30:45.123+09
-- NOW()でも同様
SELECT NOW()::TIMESTAMP(0);
4. タイムゾーンの理解と対処法
🌍 タイムゾーンの基本概念
PostgreSQLは、内部的には**UTC(協定世界時)**で時刻を保存します。 表示時に、設定されたタイムゾーンに変換されるんです。
-- 現在のタイムゾーン設定を確認
SHOW timezone;
-- 結果: Asia/Tokyo
-- サーバーのタイムゾーン一覧
SELECT * FROM pg_timezone_names
WHERE name LIKE '%Tokyo%' OR name LIKE '%UTC%';
🌍 タイムゾーンの変換
-- 日本時間をUTCに変換
SELECT NOW() AT TIME ZONE 'UTC';
-- 具体例:日本時間 2025-01-15 14:00 → UTC 2025-01-15 05:00
SELECT
'2025-01-15 14:00:00+09'::TIMESTAMPTZ AS japan_time,
'2025-01-15 14:00:00+09'::TIMESTAMPTZ AT TIME ZONE 'UTC' AS utc_time;
-- 異なるタイムゾーンで表示
SELECT
NOW() AS current_japan,
NOW() AT TIME ZONE 'America/New_York' AS new_york,
NOW() AT TIME ZONE 'Europe/London' AS london;
🌍 タイムゾーンを意識したテーブル設計
-- グローバルサービスのイベント記録
CREATE TABLE global_events (
event_id SERIAL PRIMARY KEY,
event_name VARCHAR(200),
-- UTCで保存(推奨)
event_time_utc TIMESTAMP WITH TIME ZONE,
-- ローカル時刻も保存(表示用)
event_time_local TIMESTAMP WITHOUT TIME ZONE,
timezone_name VARCHAR(50)
);
-- データ挿入例
INSERT INTO global_events (
event_name,
event_time_utc,
event_time_local,
timezone_name
) VALUES (
'オンラインセミナー',
NOW(),
NOW() AT TIME ZONE 'Asia/Tokyo',
'Asia/Tokyo'
);
5. 日付・時刻の計算と操作

➕ 時刻の加算・減算
-- 基本的な計算
SELECT
NOW() AS current_time,
NOW() + INTERVAL '1 hour' AS one_hour_later,
NOW() - INTERVAL '30 minutes' AS thirty_min_ago,
NOW() + INTERVAL '7 days' AS next_week,
NOW() - INTERVAL '1 month' AS last_month;
-- 複数単位の計算
SELECT NOW() + INTERVAL '1 year 2 months 3 days 4 hours 5 minutes';
-- 動的な計算
SELECT NOW() + (INTERVAL '1 day' * 7) AS seven_days_later;
➕ 営業日の計算
-- 営業日を考慮した日付計算関数
CREATE OR REPLACE FUNCTION add_business_days(
start_date DATE,
days_to_add INT
) RETURNS DATE AS $$
DECLARE
result_date DATE := start_date;
days_added INT := 0;
BEGIN
WHILE days_added < days_to_add LOOP
result_date := result_date + 1;
-- 土日をスキップ
IF EXTRACT(DOW FROM result_date) NOT IN (0, 6) THEN
days_added := days_added + 1;
END IF;
END LOOP;
RETURN result_date;
END;
$$ LANGUAGE plpgsql;
-- 使用例:3営業日後
SELECT add_business_days(CURRENT_DATE, 3);
➕ 年齢や経過時間の計算
-- 年齢計算
SELECT
'1990-05-15'::DATE AS birth_date,
AGE('1990-05-15'::DATE) AS age_interval,
EXTRACT(YEAR FROM AGE('1990-05-15'::DATE)) AS years_old;
-- 経過時間の計算
SELECT
NOW() - '2025-01-15 09:00:00'::TIMESTAMP AS elapsed_time,
EXTRACT(EPOCH FROM (NOW() - '2025-01-15 09:00:00'::TIMESTAMP)) AS seconds_elapsed;
-- 人間が読みやすい形式に変換
CREATE OR REPLACE FUNCTION format_duration(duration INTERVAL)
RETURNS TEXT AS $$
DECLARE
years INT;
months INT;
days INT;
hours INT;
minutes INT;
BEGIN
years := EXTRACT(YEAR FROM duration);
months := EXTRACT(MONTH FROM duration);
days := EXTRACT(DAY FROM duration);
hours := EXTRACT(HOUR FROM duration);
minutes := EXTRACT(MINUTE FROM duration);
RETURN CONCAT_WS(', ',
CASE WHEN years > 0 THEN years || '年' END,
CASE WHEN months > 0 THEN months || 'ヶ月' END,
CASE WHEN days > 0 THEN days || '日' END,
CASE WHEN hours > 0 THEN hours || '時間' END,
CASE WHEN minutes > 0 THEN minutes || '分' END
);
END;
$$ LANGUAGE plpgsql;
-- 使用例
SELECT format_duration(NOW() - '2024-01-01'::TIMESTAMP);
-- 結果: 1年, 14日, 5時間, 30分
6. フォーマット変換と表示形式
📝 TO_CHAR()で自由自在にフォーマット
-- よく使うフォーマット例
SELECT
TO_CHAR(NOW(), 'YYYY-MM-DD') AS date_only, -- 2025-01-15
TO_CHAR(NOW(), 'YYYY/MM/DD HH24:MI:SS') AS datetime, -- 2025/01/15 14:30:45
TO_CHAR(NOW(), 'YYYY年MM月DD日') AS japanese_date, -- 2025年01月15日
TO_CHAR(NOW(), 'Day') AS day_name, -- Wednesday
TO_CHAR(NOW(), 'FMDD日 HH24時MI分') AS short_jp; -- 15日 14時30分
📝 フォーマット記号一覧
-- 主要なフォーマット記号
SELECT
TO_CHAR(NOW(), 'YYYY') AS year_4digit, -- 2025
TO_CHAR(NOW(), 'YY') AS year_2digit, -- 25
TO_CHAR(NOW(), 'MM') AS month_number, -- 01
TO_CHAR(NOW(), 'Month') AS month_name, -- January
TO_CHAR(NOW(), 'Mon') AS month_abbr, -- Jan
TO_CHAR(NOW(), 'DD') AS day_of_month, -- 15
TO_CHAR(NOW(), 'D') AS day_of_week, -- 4(水曜日)
TO_CHAR(NOW(), 'HH24') AS hour_24, -- 14
TO_CHAR(NOW(), 'HH12') AS hour_12, -- 02
TO_CHAR(NOW(), 'MI') AS minute, -- 30
TO_CHAR(NOW(), 'SS') AS second, -- 45
TO_CHAR(NOW(), 'AM') AS am_pm; -- PM
📝 実用的なフォーマット例
-- ログファイル名用
SELECT TO_CHAR(NOW(), 'YYYYMMDD_HH24MISS');
-- 結果: 20250115_143045
-- CSVエクスポート用
SELECT TO_CHAR(NOW(), 'YYYY-MM-DD"T"HH24:MI:SS"+09:00"');
-- 結果: 2025-01-15T14:30:45+09:00
-- 月次レポート用
SELECT TO_CHAR(NOW(), 'YYYY年FMMM月度');
-- 結果: 2025年1月度
-- 週次レポート用
SELECT
TO_CHAR(NOW(), 'YYYY年 第IW週') AS week_number,
TO_CHAR(DATE_TRUNC('week', NOW()), 'MM/DD') || '〜' ||
TO_CHAR(DATE_TRUNC('week', NOW()) + INTERVAL '6 days', 'MM/DD') AS week_range;
-- 結果: 2025年 第03週, 01/13〜01/19
7. パフォーマンスを意識した時刻処理
⚡ インデックスと時刻検索
-- 時刻カラムにインデックスを作成
CREATE INDEX idx_created_at ON orders(created_at);
-- 日付のみのインデックス(日付検索が多い場合)
CREATE INDEX idx_created_date ON orders(DATE(created_at));
-- 効率的な範囲検索
-- 良い例:インデックスが効く
SELECT * FROM orders
WHERE created_at >= CURRENT_DATE
AND created_at < CURRENT_DATE + INTERVAL '1 day';
-- 悪い例:関数をカラムに適用(インデックスが効かない)
SELECT * FROM orders
WHERE DATE(created_at) = CURRENT_DATE; -- 避けるべき
⚡ パーティショニングでの活用
-- 月別パーティションテーブル
CREATE TABLE events (
event_id BIGSERIAL,
event_time TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
event_data JSONB
) PARTITION BY RANGE (event_time);
-- 自動でパーティション作成
CREATE TABLE events_2025_01 PARTITION OF events
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE events_2025_02 PARTITION OF events
FOR VALUES FROM ('2025-02-01') TO ('2025-03-01');
-- 古いパーティションの自動削除設定
CREATE OR REPLACE FUNCTION drop_old_partitions()
RETURNS void AS $$
DECLARE
cutoff_date DATE;
BEGIN
cutoff_date := CURRENT_DATE - INTERVAL '3 months';
-- 3ヶ月以上前のパーティションを削除
EXECUTE format('DROP TABLE IF EXISTS events_%s',
TO_CHAR(cutoff_date, 'YYYY_MM'));
END;
$$ LANGUAGE plpgsql;
8. 実践的な使用例とベストプラクティス
💼 ケース1:監査ログの実装
-- 監査ログテーブル
CREATE TABLE audit_logs (
audit_id BIGSERIAL PRIMARY KEY,
table_name VARCHAR(50),
operation VARCHAR(10),
user_id INT,
old_data JSONB,
new_data JSONB,
-- 更新不可の作成時刻
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL,
-- IPアドレスなど追加情報
client_ip INET,
session_id VARCHAR(100)
);
-- トリガー関数で自動記録
CREATE OR REPLACE FUNCTION audit_trigger_function()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_logs (
table_name,
operation,
user_id,
old_data,
new_data,
client_ip,
session_id
) VALUES (
TG_TABLE_NAME,
TG_OP,
current_setting('app.current_user_id', true)::INT,
CASE WHEN TG_OP = 'DELETE' THEN row_to_json(OLD) ELSE NULL END,
CASE WHEN TG_OP IN ('INSERT', 'UPDATE') THEN row_to_json(NEW) ELSE NULL END,
inet_client_addr(),
current_setting('app.session_id', true)
);
RETURN CASE
WHEN TG_OP = 'DELETE' THEN OLD
ELSE NEW
END;
END;
$$ LANGUAGE plpgsql;
💼 ケース2:有効期限管理
-- クーポンテーブル
CREATE TABLE coupons (
coupon_id SERIAL PRIMARY KEY,
coupon_code VARCHAR(20) UNIQUE,
discount_percent INT,
valid_from TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
valid_until TIMESTAMP WITH TIME ZONE,
is_active BOOLEAN GENERATED ALWAYS AS (
NOW() BETWEEN valid_from AND valid_until
) STORED
);
-- 1週間有効なクーポンを作成
INSERT INTO coupons (coupon_code, discount_percent, valid_until)
VALUES ('WELCOME2025', 20, NOW() + INTERVAL '7 days');
-- 有効なクーポンのみ取得
SELECT * FROM coupons
WHERE NOW() BETWEEN valid_from AND valid_until;
-- もうすぐ期限切れのクーポン(24時間以内)
SELECT
coupon_code,
valid_until,
valid_until - NOW() AS time_remaining
FROM coupons
WHERE valid_until BETWEEN NOW() AND NOW() + INTERVAL '24 hours'
ORDER BY valid_until;
💼 ケース3:定期実行スケジュール
-- スケジュールテーブル
CREATE TABLE scheduled_tasks (
task_id SERIAL PRIMARY KEY,
task_name VARCHAR(100),
cron_expression VARCHAR(50),
last_run_at TIMESTAMP WITH TIME ZONE,
next_run_at TIMESTAMP WITH TIME ZONE,
is_active BOOLEAN DEFAULT TRUE
);
-- 次回実行時刻を計算する関数
CREATE OR REPLACE FUNCTION calculate_next_run(
cron_expr VARCHAR,
from_time TIMESTAMP WITH TIME ZONE DEFAULT NOW()
) RETURNS TIMESTAMP WITH TIME ZONE AS $$
DECLARE
next_time TIMESTAMP WITH TIME ZONE;
BEGIN
-- 簡略化した例(実際はcron式をパース)
-- 毎日午前9時の場合
IF cron_expr = '0 9 * * *' THEN
next_time := DATE_TRUNC('day', from_time) + INTERVAL '1 day 9 hours';
IF next_time <= from_time THEN
next_time := next_time + INTERVAL '1 day';
END IF;
-- 毎時実行の場合
ELSIF cron_expr = '0 * * * *' THEN
next_time := DATE_TRUNC('hour', from_time) + INTERVAL '1 hour';
END IF;
RETURN next_time;
END;
$$ LANGUAGE plpgsql;
-- 実行すべきタスクを取得
SELECT * FROM scheduled_tasks
WHERE is_active = TRUE
AND (next_run_at IS NULL OR next_run_at <= NOW())
ORDER BY next_run_at NULLS FIRST;
9. よくあるトラブルと解決方法

❌ 時刻が9時間ずれる問題
-- 原因確認:タイムゾーン設定
SHOW timezone;
-- セッション単位で修正
SET timezone = 'Asia/Tokyo';
-- データベース全体で設定
ALTER DATABASE mydb SET timezone = 'Asia/Tokyo';
-- 既存データの修正(UTCとして保存されていた場合)
UPDATE events
SET event_time = event_time AT TIME ZONE 'UTC' AT TIME ZONE 'Asia/Tokyo'
WHERE event_time < '2025-01-01';
❌ 日付の境界での不具合
-- 問題:23:59:59.999999 のデータが漏れる
-- 悪い例
SELECT * FROM orders
WHERE created_at BETWEEN '2025-01-15 00:00:00' AND '2025-01-15 23:59:59';
-- 良い例:半開区間を使用
SELECT * FROM orders
WHERE created_at >= '2025-01-15'::DATE
AND created_at < '2025-01-16'::DATE;
-- または DATE_TRUNC を使用
SELECT * FROM orders
WHERE DATE_TRUNC('day', created_at) = '2025-01-15'::DATE;
❌ NULL値での計算エラー
-- NULL安全な時刻計算
SELECT
COALESCE(updated_at, created_at, NOW()) AS last_modified,
GREATEST(created_at, updated_at) AS latest_time,
LEAST(start_time, end_time) AS earliest_time
FROM tasks;
-- 経過時間計算でNULL対策
SELECT
task_name,
CASE
WHEN completed_at IS NULL THEN '進行中'
ELSE format_duration(completed_at - started_at)
END AS duration
FROM tasks;
10. 高度なテクニックとTips
🎯 時系列データの集計
-- 時間帯別の集計
SELECT
DATE_TRUNC('hour', created_at) AS hour,
COUNT(*) AS order_count,
SUM(total_amount) AS total_sales
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '7 days'
GROUP BY DATE_TRUNC('hour', created_at)
ORDER BY hour;
-- 曜日別の傾向分析
SELECT
TO_CHAR(created_at, 'Day') AS day_of_week,
EXTRACT(DOW FROM created_at) AS day_number,
COUNT(*) AS order_count,
AVG(total_amount) AS avg_amount
FROM orders
WHERE created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY TO_CHAR(created_at, 'Day'), EXTRACT(DOW FROM created_at)
ORDER BY day_number;
🎯 ウィンドウ関数での時刻処理
-- 前回からの経過時間を計算
SELECT
user_id,
login_time,
LAG(login_time) OVER (PARTITION BY user_id ORDER BY login_time) AS prev_login,
login_time - LAG(login_time) OVER (PARTITION BY user_id ORDER BY login_time) AS time_since_last
FROM user_logins
ORDER BY user_id, login_time;
-- 移動平均の計算
SELECT
DATE_TRUNC('day', order_date) AS day,
daily_sales,
AVG(daily_sales) OVER (
ORDER BY DATE_TRUNC('day', order_date)
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
) AS moving_avg_7days
FROM (
SELECT
order_date,
SUM(total_amount) AS daily_sales
FROM orders
GROUP BY order_date
) daily_totals;
🎯 タイムゾーン変換のユーティリティ
-- 世界時計ビュー
CREATE VIEW world_clock AS
SELECT
'Tokyo' AS city,
NOW() AS local_time
UNION ALL
SELECT
'New York',
NOW() AT TIME ZONE 'America/New_York'
UNION ALL
SELECT
'London',
NOW() AT TIME ZONE 'Europe/London'
UNION ALL
SELECT
'Sydney',
NOW() AT TIME ZONE 'Australia/Sydney';
-- イベントの各地域での時刻表示
CREATE OR REPLACE FUNCTION show_event_times(
event_time TIMESTAMP WITH TIME ZONE
) RETURNS TABLE (
timezone_name TEXT,
local_time TEXT,
offset_from_utc TEXT
) AS $$
BEGIN
RETURN QUERY
SELECT
tz.name::TEXT,
TO_CHAR(event_time AT TIME ZONE tz.name, 'YYYY-MM-DD HH24:MI:SS')::TEXT,
tz.utc_offset::TEXT
FROM pg_timezone_names tz
WHERE tz.name IN (
'Asia/Tokyo',
'America/New_York',
'Europe/London',
'UTC'
)
ORDER BY tz.utc_offset;
END;
$$ LANGUAGE plpgsql;
まとめ:正確な時刻管理で、信頼性の高いシステムを!
PostgreSQLの現在時刻取得、思った以上に奥が深いですよね。 でも、基本を押さえれば、どんな要件にも対応できます。
重要ポイントのおさらい:
✅ NOW()が最も汎用的
- トランザクション内で一定
- タイムゾーン付きで安全
✅ 用途に応じた使い分け
- 日付のみ:CURRENT_DATE
- リアルタイム:CLOCK_TIMESTAMP()
- ローカル時刻:LOCALTIMESTAMP
✅ タイムゾーンは必ず意識
- データベースは内部的にUTC
- WITH TIME ZONEを推奨
- AT TIME ZONEで変換
✅ フォーマットはTO_CHAR()で自在に
- 日本語表示も可能
- ログファイル名にも活用
使い分けガイド:
ケース | 使う関数 | 理由 |
---|---|---|
ログ記録 | NOW() | 一貫性が重要 |
有効期限 | CURRENT_TIMESTAMP | SQL標準準拠 |
日次バッチ | CURRENT_DATE | 時刻不要 |
パフォーマンス測定 | CLOCK_TIMESTAMP() | 実時間必要 |
時刻の正確な管理は、システムの信頼性に直結します。 この記事を参考に、適切な時刻処理を実装してください。
🚀 次のステップ
今すぐ試すべきこと:
- NOW()とCLOCK_TIMESTAMP()の違いを確認
- タイムゾーン設定をチェック
- TO_CHAR()でフォーマット練習
スキルアップのために:
- pg_cronで定期実行を実装
- タイムゾーン変換の自動化
- 時系列データの分析に挑戦
この記事が、あなたのPostgreSQLでの 時刻処理スキル向上に役立つことを願っています!
正確な時刻管理で、ユーザーに信頼されるシステムを構築しましょう!
コメント