PostgreSQL 时间/日期函数和操作符

下面是一份 PostgreSQL 时间/日期函数与操作符完整实战手册,涵盖 数据类型、函数、操作符、时区、间隔、格式化、性能优化、常见陷阱与最佳实践,适合开发、DBA、架构师使用。


一、PostgreSQL 时间/日期数据类型

类型存储范围推荐用途
DATE4 bytes4713 BC – 5874897 AD纯日期
TIME [WITHOUT TIME ZONE]8 bytes00:00:00 – 24:00:00本地时间
TIME WITH TIME ZONE (TIMETZ)12 bytes同上带时区时间
TIMESTAMP [WITHOUT TIME ZONE]8 bytes4713 BC – 294276 AD最常用
TIMESTAMP WITH TIME ZONE (TIMESTAMPTZ)8 bytes同上跨时区推荐
INTERVAL16 bytes-178000000 年 ~ +178000000 年时间间隔

黄金法则能用 TIMESTAMPTZ 就不要用 TIMESTAMP


二、当前时间函数

函数返回类型说明
NOW()TIMESTAMPTZ事务开始时间(推荐)
CURRENT_TIMESTAMPTIMESTAMPTZNOW()
TRANSACTION_TIMESTAMP()TIMESTAMPTZNOW()
STATEMENT_TIMESTAMP()TIMESTAMPTZ语句开始时间
CLOCK_TIMESTAMP()TIMESTAMPTZ调用时实时时间
CURRENT_DATEDATE当前日期
CURRENT_TIMETIMETZ当前时间(带时区)
SELECT 
    NOW(),                    -- 2025-04-05 10:23:45.123456+08
    CURRENT_DATE,             -- 2025-04-05
    CURRENT_TIME;             -- 10:23:45.123456+08

三、创建时间值

1. 字符串 → 时间

-- 自动识别格式
SELECT '2025-04-05 10:30:00'::TIMESTAMP;
SELECT '2025-04-05'::DATE;
SELECT '10:30:00'::TIME;

-- 带时区
SELECT '2025-04-05 10:30:00+08'::TIMESTAMPTZ;

2. MAKE 系列函数

SELECT MAKE_DATE(2025, 4, 5);              -- 2025-04-05
SELECT MAKE_TIME(10, 30, 45.5);            -- 10:30:45.5
SELECT MAKE_TIMESTAMP(2025, 4, 5, 10, 30, 45.5); -- 2025-04-05 10:30:45.5
SELECT MAKE_TIMESTAMPTZ(2025, 4, 5, 10, 30, 45.5, 'Asia/Shanghai');

四、提取部分(EXTRACT / DATE_PART)

SELECT 
    EXTRACT(YEAR FROM NOW()) AS year,
    EXTRACT(MONTH FROM NOW()) AS month,
    EXTRACT(DAY FROM NOW()) AS day,
    EXTRACT(HOUR FROM NOW()) AS hour,
    EXTRACT(DOW FROM NOW()) AS weekday,  -- 0=周日
    EXTRACT(DOY FROM NOW()) AS yearday;

-- 等价
SELECT DATE_PART('hour', NOW());

常用字段

字段说明
YEAR, MONTH, DAY年月日
HOUR, MINUTE, SECOND时分秒
DOW星期几(0=周日)
DOY一年中的第几天
EPOCH秒级 Unix 时间戳
CENTURY, DECADE, QUARTER世纪、十年、季度
SELECT EXTRACT(EPOCH FROM NOW());  -- 1743944625.123456

五、格式化与解析(TO_CHAR / TO_TIMESTAMP)

1. TO_CHAR:时间 → 字符串

SELECT TO_CHAR(NOW(), 'YYYY-MM-DD HH24:MI:SS');     -- 2025-04-05 10:23:45
SELECT TO_CHAR(NOW(), 'Mon DD, YYYY');              -- Apr 05, 2025
SELECT TO_CHAR(NOW(), 'Dy, DDth Month');            -- Sat, 05th April

常用格式符

说明示例
YYYY4位年2025
MM2位月04
DD2位日05
HH2424小时10
MI分钟23
SS45
MS毫秒123
US微秒123456
TZ时区缩写CST
Mon月份简称Apr
Day星期全称Saturday

2. TO_TIMESTAMP:字符串 → 时间

SELECT TO_TIMESTAMP('2025-04-05 10:30:00', 'YYYY-MM-DD HH24:MI:SS');
SELECT TO_TIMESTAMP('05/04/2025 10:30', 'DD/MM/YYYY HH24:MI');

六、时间运算(+ / – / INTERVAL)

-- 加减
SELECT NOW() + INTERVAL '1 day';
SELECT NOW() - INTERVAL '2 hours';

-- 日期差
SELECT AGE(TIMESTAMP '2025-12-31', TIMESTAMP '2025-01-01');  -- 11 months 30 days
SELECT NOW() - TIMESTAMP '2025-01-01';  -- INTERVAL 类型

-- 间隔乘除
SELECT INTERVAL '1 month' * 3;
SELECT INTERVAL '2 hours' / 2;

AGE() 函数

SELECT AGE(birthday) FROM users;  -- 当前年龄
SELECT AGE('2025-12-31'::DATE);   -- 距离年底

七、时区处理(关键!)

PostgreSQL 默认存储 UTC,显示时自动转换。

-- 设置会话时区
SET TIME ZONE 'Asia/Shanghai';
SET TIME ZONE 'UTC';
SET TIME ZONE 'America/New_York';

-- 查看当前时区
SHOW TIMEZONE;

-- 转换时区
SELECT NOW() AT TIME ZONE 'UTC';           -- 转成 UTC
SELECT '2025-04-05 10:00:00+08'::TIMESTAMPTZ AT TIME ZONE 'UTC';

最佳实践

-- 存储:用 TIMESTAMPTZ
created_at TIMESTAMPTZ DEFAULT NOW()

-- 查询:前端指定时区
SELECT created_at AT TIME ZONE 'Asia/Shanghai' FROM events;

八、间隔(INTERVAL)函数

-- 构造
SELECT INTERVAL '3 years 2 months';
SELECT 'P1Y2M3DT4H5M6S'::INTERVAL;  -- ISO 8601 格式

-- 提取
SELECT JUSTIFY_DAYS(INTERVAL '35 days');   -- 1 mon 5 days
SELECT JUSTIFY_HOURS(INTERVAL '30 hours'); -- 1 day 6 hours
SELECT JUSTIFY_INTERVAL(INTERVAL '1 mon 40 days');

九、范围查询与索引优化

1. 推荐写法(Sargable)

-- 推荐:常量在右边
WHERE created_at >= '2025-04-01 00:00:00+08'
  AND created_at < '2025-04-02 00:00:00+08'

-- 避免:函数包裹列
-- WHERE DATE(created_at) = '2025-04-01'  -- 不能用索引

2. 索引

CREATE INDEX idx_orders_created_at ON orders(created_at);

3. 分区表(按月)

CREATE TABLE orders_202504 PARTITION OF orders
    FOR VALUES FROM ('2025-04-01') TO ('2025-05-01');

十、常见函数汇总表

类别函数示例
当前时间NOW(), CURRENT_TIMESTAMPNOW()
构造MAKE_DATE, MAKE_TIMESTAMPMAKE_DATE(2025,4,5)
提取EXTRACT(), DATE_PART()EXTRACT(MONTH FROM NOW())
格式化TO_CHAR()TO_CHAR(NOW(), 'YYYY-MM-DD')
解析TO_TIMESTAMP()TO_TIMESTAMP('2025/04/05', 'YYYY/MM/DD')
运算+ INTERVAL, AGE()NOW() + INTERVAL '1 day'
时区AT TIME ZONENOW() AT TIME ZONE 'UTC'
间隔JUSTIFY_INTERVAL()JUSTIFY_DAYS(INTERVAL '35 days')

十一、实战示例

1. 统计最近 7 天活跃用户

SELECT DATE(created_at) AS day, COUNT(DISTINCT user_id)
FROM logins
WHERE created_at >= NOW() - INTERVAL '7 days'
GROUP BY DATE(created_at)
ORDER BY day;

2. 查询本月第一天和最后一天

SELECT 
    DATE_TRUNC('month', NOW()) AS first_day,
    (DATE_TRUNC('month', NOW()) + INTERVAL '1 month - 1 day')::DATE AS last_day;

3. 时间段重叠判断

-- 两个预约是否冲突
SELECT *
FROM bookings b1, bookings b2
WHERE b1.room_id = b2.room_id
  AND b1.id <> b2.id
  AND b1.start_time < b2.end_time
  AND b1.end_time > b2.start_time;

4. 生成时间序列(PostgreSQL 8.4+)

SELECT GENERATE_SERIES('2025-04-01'::DATE, '2025-04-30'::DATE, INTERVAL '1 day') AS day;

十二、性能优化技巧

技巧说明
TIMESTAMPTZ自动处理时区
避免 DATE(col)破坏索引
DATE_TRUNC 分组高效
范围查询用 < 而非 BETWEEN避免边界问题
BRIN 索引(大表)CREATE INDEX ON logs USING BRIN(created_at);

十三、常见陷阱与避坑

错误原因解决
时区错乱TIMESTAMP 存本地时间改用 TIMESTAMPTZ
夏令时重复2025-03-30 02:30 不存在TIMESTAMPTZ
BETWEEN 包含边界00:00:00 被重复算改用 <
TO_CHAR 性能差大量调用缓存或前端格式化
INTERVAL '1 month' 不固定2月加1月 → 3月DATE_TRUNC

十四、最佳实践脚本

1. 标准时间列定义

CREATE TABLE events (
    id BIGSERIAL PRIMARY KEY,
    name TEXT,
    start_time TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    end_time TIMESTAMPTZ,
    created_at TIMESTAMPTZ DEFAULT NOW(),
    updated_at TIMESTAMPTZ DEFAULT NOW(),
    CHECK (end_time > start_time)
);

-- 更新触发器
CREATE TRIGGER trg_events_updated_at
    BEFORE UPDATE ON events
    FOR EACH ROW
    EXECUTE FUNCTION update_updated_at();

2. 按天分区 + 自动创建

CREATE OR REPLACE FUNCTION create_partition_if_not_exists(p_date DATE)
RETURNS VOID AS $$
DECLARE
    partition_name TEXT := 'events_' || TO_CHAR(p_date, 'YYYYMMDD');
BEGIN
    IF NOT EXISTS (
        SELECT 1 FROM pg_tables 
        WHERE tablename = partition_name
    ) THEN
        EXECUTE format(
            'CREATE TABLE %I PARTITION OF events 
             FOR VALUES FROM (%L) TO (%L)',
            partition_name,
            p_date,
            p_date + INTERVAL '1 day'
        );
    END IF;
END;
$$ LANGUAGE plpgsql;

十五、速查表

操作SQL
当前时间NOW()
今天 0 点CURRENT_DATE
本月第一天DATE_TRUNC('month', NOW())
7 天前NOW() - INTERVAL '7 days'
年龄AGE(birthday)
格式化TO_CHAR(NOW(), 'YYYY-MM-DD')
解析TO_TIMESTAMP('2025/04/05', 'YYYY/MM/DD')
时区转换ts AT TIME ZONE 'UTC'

十六、决策树

graph TD
    A[需要存时间?] -->|是| B{是否跨时区?}
    B -->|是| C[用 TIMESTAMPTZ]
    B -->|否| D{是否需时区?}
    D -->|是| E[用 TIMETZ]
    D -->|否| F[用 TIMESTAMP]
    A -->|否| G[用 DATE]

十七、常见面试题

问题答案
TIMESTAMP vs TIMESTAMPTZ后者带时区,推荐
NOW()CLOCK_TIMESTAMP() 区别?前者事务固定,后者实时
如何避免夏令时问题?TIMESTAMPTZ
DATE(col) 会用索引吗?不会
如何生成时间序列?GENERATE_SERIES()

需要我提供一个完整的 “多时区订单系统 + 分区 + 时区转换 API” 的生产级模板吗?

类似文章

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注