Skip to content

NOW / CURRENT_DATE / CURRENT_TIME ​

获取「当前时间」看似最简单,却是日期时间函数中陷阱最密集的一组:同一个 now() 在同一事务里永远返回同一个值,而 clock_timestamp() 在同一语句里都会变;current_timestamp 不带括号、now() 带括号,二者却完全等价。选错函数会让审计日志、耗时统计、批量更新的时间戳出现难以复现的偏差。本节把 8 个「取当前时间」的函数按语义分成三族,并给出明确的选型规则。

前置知识

阅读本节前,建议先了解:数学常量与三角函数

基础概念 ​

「当前时间」在 PostgreSQL 中不是一个值,而是若干语义不同的值。区分它们的关键维度有两个:锚点时刻(事务开始 / 语句开始 / 真实此刻)与返回类型(timestamptz / timestamp / date / time / timetz / text)。

概念术语说明
事务开始时刻Transaction Start TimeBEGIN 或隐式事务第一条语句的时刻,事务内恒定
语句开始时刻Statement Start Time当前语句开始执行的时刻,同事务不同语句会变
墙上时钟Wall Clock Time真实此刻的时钟读数,语句执行中也会推进
易变性Volatility函数分类:IMMUTABLE / STABLE / VOLATILE
带时区时间戳timestamptztimestamp with time zone,内部存 UTC 微秒
本地时间localtime / localtimestamp按会话 TimeZone 渲染,但不带偏移信息
带时区的时间timetztime with time zone,只有时刻没有日期
精度参数Precisioncurrent_timestamp(n) 限制小数秒位数

8 个函数的三族划分:

函数返回类型锚点易变性同一事务内是否恒定
now()timestamptz事务开始STABLE是
transaction_timestamp()timestamptz事务开始STABLE是
current_timestamptimestamptz事务开始STABLE是
localtimestamptimestamp事务开始STABLE是
current_datedate事务开始(按会话时区取日期)STABLE是
current_timetimetz事务开始STABLE是
localtimetime事务开始STABLE是
statement_timestamp()timestamptz语句开始STABLE否
clock_timestamp()timestamptz真实此刻VOLATILE否
timeofday()text真实此刻VOLATILE否

表中共 10 项:前 7 项共用「事务开始时刻」这一个锚点,是同一时刻的 7 种类型投影;后 3 项各有自己的锚点。

选型流程:

text
你需要「当前时间」做什么?
  ├─ 给一行业务数据打时间戳 / 审计日志 → 用 now()
  │    (同一事务内多张表的时间戳一致,便于对账)
  ├─ 统计单条语句耗时(语句前后对比)→ 前后都用 statement_timestamp()
  ├─ 统计语句内部某段代码的真实耗时 → 用 clock_timestamp()
  ├─ 只要日期(按天分组、取今天)→ 用 current_date
  └─ 写入 timestamp 列且不想带时区 → 用 localtimestamp
       (但更推荐列本身用 timestamptz,见最佳实践)

一句话记忆:now() 关心「事务」,statement_timestamp() 关心「语句」,clock_timestamp() 关心「此刻」。

语法与用法 ​

sql
-- 三种等价写法:取事务开始时刻
now()
transaction_timestamp()
current_timestamp          -- SQL 标准关键字,不带括号
current_timestamp(3)       -- 带精度,秒保留 3 位小数

-- 语句级与实时
statement_timestamp()      -- 语句开始时刻(无精度参数)
clock_timestamp()          -- 真实此刻,语句执行中会推进
timeofday()                -- 真实此刻,text 格式

-- 日期与时间的「本地」投影
current_date               -- date
current_time               -- time with time zone
current_time(0)            -- 带精度
localtime                  -- time
localtime(0)               -- 带精度
localtimestamp             -- timestamp
localtimestamp(2)          -- 带精度

最小可运行示例:

sql
-- 一次查询中把 8 个函数的返回值并列展示
SELECT
  now()                     AS now_ts,
  transaction_timestamp()   AS tx_ts,
  statement_timestamp()     AS stmt_ts,
  clock_timestamp()         AS clock_ts,
  current_date              AS cur_date,
  localtime                 AS local_time,
  localtimestamp            AS local_ts,
  timeofday()               AS tod;
text
-[ RECORD 1 ]-------------------------------
now_ts     | 2026-09-24 10:30:00.123456+08
tx_ts      | 2026-09-24 10:30:00.123456+08
stmt_ts    | 2026-09-24 10:30:00.123456+08
clock_ts   | 2026-09-24 10:30:00.124001+08   -- 比 now() 略晚
cur_date   | 2026-09-24
local_time | 10:30:00.124001
local_ts   | 2026-09-24 10:30:00.123456
tod        | Thu Sep 24 10:30:00.124001 2026 CST

准备本节贯穿使用的示例表(在 demo 库中执行):

sql
-- 设备心跳表:用于演示「同一事务内时间戳一致」的价值
CREATE TABLE device_heartbeat (
  id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  device_id   TEXT        NOT NULL,
  reported_at TIMESTAMPTZ NOT NULL,        -- 设备上报时刻(由 now() 写入)
  lag_seconds INTEGER     NOT NULL,        -- 相对上一条的间隔秒数
  note        TEXT
);

CREATE INDEX idx_heartbeat_device_time ON device_heartbeat (device_id, reported_at);

-- 工单流转表:演示审计场景
CREATE TABLE ticket_log (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  ticket_id  BIGINT      NOT NULL,
  action     TEXT        NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

INSERT INTO device_heartbeat (device_id, reported_at, lag_seconds, note) VALUES
  ('dev-001', now() - INTERVAL '3 minutes', 180, '正常'),
  ('dev-001', now() - INTERVAL '2 minutes', 60,  '正常'),
  ('dev-002', now() - INTERVAL '90 seconds', 90, '网络抖动'),
  ('dev-002', now() - INTERVAL '30 seconds', 60, '正常'),
  ('dev-003', now(),                         300, '补报');
sql
-- 最小可用:查询最近 5 分钟内的心跳
SELECT device_id, reported_at, lag_seconds
FROM device_heartbeat
WHERE reported_at >= now() - INTERVAL '5 minutes'
ORDER BY reported_at DESC;
text
 device_id |         reported_at         | lag_seconds
-----------+-----------------------------+-------------
 dev-003   | 2026-09-24 10:30:00.1234+08 |         300
 dev-002   | 2026-09-24 10:29:30.1234+08 |          60
 dev-002   | 2026-09-24 10:28:30.1234+08 |          90
 dev-001   | 2026-09-24 10:28:00.1234+08 |          60
 dev-001   | 2026-09-24 10:27:00.1234+08 |         180
(5 rows)

详细说明 ​

事务族:now() / transaction_timestamp() / current_timestamp ​

这三个是完全等价的,返回的都是「当前事务开始时刻」:

sql
BEGIN;

-- 事务内第一条语句
SELECT now() AS first_call, clock_timestamp() AS real_first;

-- 模拟耗时操作(pg_sleep 期间时间在流逝)
SELECT pg_sleep(1);

-- 事务内第二条语句
SELECT now() AS second_call, clock_timestamp() AS real_second;

COMMIT;
text
      first_call       |         real_first
-----------------------+----------------------------
 2026-09-24 10:30:00.10+08 | 2026-09-24 10:30:00.105+08

      second_call      |         real_second
-----------------------+----------------------------
 2026-09-24 10:30:00.10+08 | 2026-09-24 10:30:01.108+08

now() 两次都是 10:30:00.10——事务内的所有 now() 是同一个值;而 clock_timestamp() 从 .105 走到了 1.108。这正是「给一批关联数据打同一个时间戳」所需要的语义。

current_timestamp 只是 now() 的 SQL 标准拼写。它有两种形式:

sql
-- 关键字形式不带括号(带括号只在有精度参数时)
SELECT current_timestamp AS full_precision;

-- 限制到毫秒
SELECT current_timestamp(3) AS ms_precision;

-- 限制到秒
SELECT current_timestamp(0) AS sec_precision;
text
       full_precision
----------------------------
 2026-09-24 10:30:00.123456+08
        ms_precision
------------------------
 2026-09-24 10:30:00.123+08
      sec_precision
------------------------
 2026-09-24 10:30:00+08

current_timestamp(0) 的精度参数会四舍五入而非截断:.6 秒会进位到下一秒。写入精度受限的列(如 TIMESTAMPTZ(0))时,PostgreSQL 同样按四舍五入处理,两者行为一致。

语句族:statement_timestamp() ​

statement_timestamp() 在每条语句开始时取一次值,同一条语句内的多次调用返回相同值,但不同语句之间会推进:

sql
BEGIN;

SELECT statement_timestamp() AS before_sleep;
SELECT pg_sleep(1);
SELECT statement_timestamp() AS after_sleep;

COMMIT;
text
        before_sleep
----------------------------
 2026-09-24 10:30:00.200+08
         after_sleep
----------------------------
 2026-09-24 10:30:01.201+08

注意它在 pg_sleep 前后的值不同,说明它跟着语句走。这是测量单条语句耗时的正确工具:

sql
-- 在应用层记录语句前后时间,用 statement_timestamp() 更贴近服务端时钟
\timing on
SELECT count(*) FROM device_heartbeat;

statement_timestamp() 没有精度参数,这是它与 current_timestamp 的一个签名差异。

实时族:clock_timestamp() 与 timeofday() ​

clock_timestamp() 返回调用瞬间的真实时间,即使在同一语句内多次调用也会前进:

sql
-- 同一条语句内三次调用,返回值逐次递增
SELECT
  clock_timestamp() AS t1,
  pg_sleep(0.05),
  clock_timestamp() AS t2,
  clock_timestamp() AS t3;
text
              t1              | t2 |              t3
------------------------------+----+------------------------------
 2026-09-24 10:30:00.300001+08    | 2026-09-24 10:30:00.350221+08
                                   | 2026-09-24 10:30:00.350230+08

t3 比 t2 晚了 9 微秒,证明它在同一语句里没有被缓存。因为它是 VOLATILE,还带来两个副作用:无法用于表达式索引,且会阻止某些查询优化(例如包含 clock_timestamp() 的子查询不能被当作常量折叠)。

timeofday() 返回同样的时刻,但类型是 text:

sql
SELECT timeofday();
text
              timeofday
---------------------------------------
 Thu Sep 24 10:30:00.350240 2026 CST

它的输出格式是固定的 C 风格 Day Mon DD HH:MI:SS.US YYYY TZ,不受 DateStyle 影响,也不适合直接参与日期运算——要计算请用 clock_timestamp()。

本地投影:current_date / current_time / localtime / localtimestamp ​

这四个函数返回的是同一事务开始时刻按会话时区渲染后的部分字段,区别只在返回类型与是否带时区:

表达式返回类型示例值
current_datedate2026-09-24
current_timetime with time zone10:30:00.123456+08
localtimetime10:30:00.123456
localtimestamptimestamp2026-09-24 10:30:00.123456
sql
SET TimeZone TO 'UTC';
SELECT current_date AS d, current_time AS ct, localtime AS lt, localtimestamp AS lts;

SET TimeZone TO 'Asia/Shanghai';
SELECT current_date AS d, current_time AS ct, localtime AS lt, localtimestamp AS lts;
text
-- TimeZone = 'UTC'
    d     |         ct         |        lt         |         lts
----------+--------------------+-------------------+------------------------
 2026-09-24 | 02:30:00.123456+00 | 02:30:00.123456   | 2026-09-24 02:30:00.123456

-- TimeZone = 'Asia/Shanghai'
    d     |         ct         |        lt         |         lts
----------+--------------------+-------------------+------------------------
 2026-09-24 | 10:30:00.123456+08 | 10:30:00.123456   | 2026-09-24 10:30:00.123456

关键差异有两点:

  1. current_time 与 localtime 的绝对时刻不同(02:30+00 与 10:30+08 是同一时刻),因为 current_time 带偏移。
  2. localtimestamp 丢掉偏移后不再是一个绝对时刻,只表示「本地墙上时钟读数」。跨时区存储这类值会造成歧义。

current_date 的边界完全由会话时区决定。当 UTC 刚跨过 0 点时,东八区已经是第二天:

sql
-- UTC 2026-09-24 16:00 时,上海已是 09-25 00:00
SET TimeZone TO 'UTC';
SELECT current_date AS date_in_utc;         -- 2026-09-24

SET TimeZone TO 'Asia/Shanghai';
SELECT current_date AS date_in_shanghai;    -- 2026-09-25

「今天是哪天」在跨时区系统里是一个必须显式约定时区的问题,不能依赖数据库会话的默认值。

写列时的时间戳:DEFAULT now() 与 GENERATED 列 ​

now() 最常见、最有价值的用法是作为列的默认值:

sql
CREATE TABLE ticket_log (
  id         BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  ticket_id  BIGINT      NOT NULL,
  action     TEXT        NOT NULL,
  created_at TIMESTAMPTZ NOT NULL DEFAULT now()   -- 插入时自动填写
);

-- 一次事务写入三条流转记录,三行时间戳完全相同
BEGIN;
INSERT INTO ticket_log (ticket_id, action) VALUES (1001, 'create');
INSERT INTO ticket_log (ticket_id, action) VALUES (1001, 'assign');
INSERT INTO ticket_log (ticket_id, action) VALUES (1001, 'close');
COMMIT;

SELECT ticket_id, action, created_at FROM ticket_log ORDER BY id;
text
 ticket_id | action |         created_at
-----------+--------+----------------------------
      1001 | create | 2026-09-24 10:30:00.400123+08
      1001 | assign | 2026-09-24 10:30:00.400123+08
      1001 | close  | 2026-09-24 10:30:00.400123+08

三行时间戳逐微秒相同,因为它们共用同一个事务开始时刻。若换成 clock_timestamp() 作为默认值,三行会落在不同微秒上:

sql
-- 若默认值写 clock_timestamp(),可看到三行时间戳不同
-- 这在需要「按批次聚合对账」时是麻烦,但在需要真实先后顺序时是优点
ALTER TABLE ticket_log ALTER COLUMN created_at SET DEFAULT clock_timestamp();

默认值选谁,取决于你要表达的是「同一批」还是「先后顺序」。

实战示例 ​

步骤 1:建立示例数据并确认基线 ​

sql
SELECT count(*) AS total, min(reported_at) AS earliest, max(reported_at) AS latest
FROM device_heartbeat;
text
 total |          earliest           |           latest
-------+-----------------------------+-----------------------------
     5 | 2026-09-24 10:27:00.1234+08 | 2026-09-24 10:30:00.1234+08

步骤 2:用 now() 计算每条心跳的「距今多久」 ​

sql
SELECT
  device_id,
  reported_at,
  date_trunc('second', now() - reported_at) AS age,
  EXTRACT(EPOCH FROM (now() - reported_at))::INTEGER AS age_seconds,
  CASE
    WHEN now() - reported_at > INTERVAL '2 minutes' THEN '异常'
    ELSE '正常'
  END AS status
FROM device_heartbeat
ORDER BY reported_at DESC;
text
 device_id |         reported_at         |   age    | age_seconds | status
-----------+-----------------------------+----------+-------------+--------
 dev-003   | 2026-09-24 10:30:00.1234+08 | 00:00:00 |           0 | 正常
 dev-002   | 2026-09-24 10:29:30.1234+08 | 00:00:30 |          30 | 正常
 dev-002   | 2026-09-24 10:28:30.1234+08 | 00:01:30 |          90 | 正常
 dev-001   | 2026-09-24 10:28:00.1234+08 | 00:02:00 |         120 | 正常
 dev-001   | 2026-09-24 10:27:00.1234+08 | 00:03:00 |         180 | 异常
(5 rows)

now() 在这里被调用了 5 次(3 处表达式 × 逐行求值),但所有行看到的是同一个值,因此 age_seconds 之间可以互相比较,不会出现「同一行里两个 now() 不一致」的荒谬结果。

步骤 3:验证 now() 的事务一致性 ​

sql
BEGIN;

-- 事务开始
SELECT now() AS tx_start;

-- 等待 1 秒后再取
SELECT pg_sleep(1);

-- 同一事务内再取,应与 tx_start 完全相同
SELECT now() AS tx_again, clock_timestamp() AS real_now;

COMMIT;
text
          tx_start
----------------------------
 2026-09-24 10:30:00.500000+08

          tx_again           |          real_now
------------------------------+----------------------------
 2026-09-24 10:30:00.500000+08 | 2026-09-24 10:30:01.502140+08

tx_again 与 tx_start 完全一致,而 real_now 前进了 1 秒。这就是「事务时间冻结」的直接证据。

步骤 4:按设备统计「最近一次心跳距今多久」(分组 + 聚合) ​

sql
SELECT
  device_id,
  max(reported_at) AS last_seen,
  EXTRACT(EPOCH FROM (now() - max(reported_at)))::INTEGER AS silent_seconds,
  CASE
    WHEN now() - max(reported_at) > INTERVAL '2 minutes' THEN '离线'
    ELSE '在线'
  END AS online_status
FROM device_heartbeat
GROUP BY device_id
ORDER BY device_id;
text
 device_id |          last_seen          | silent_seconds | online_status
-----------+-----------------------------+----------------+---------------
 dev-001   | 2026-09-24 10:28:00.1234+08 |            120 | 在线
 dev-002   | 2026-09-24 10:29:30.1234+08 |             30 | 在线
 dev-003   | 2026-09-24 10:30:00.1234+08 |              0 | 在线
(3 rows)

now() 出现在 SELECT 列表和 CASE 中,都基于同一个事务时刻求值,所以 silent_seconds 与 online_status 永远自洽。若换成 clock_timestamp(),两次调用可能跨过 120 秒的边界,导致同一行里 silent_seconds = 119 而 online_status = '离线'。

步骤 5:用 clock_timestamp() 测量语句内真实耗时 ​

sql
-- 模拟一段真实耗时的工作:统计 + 短暂暂停
SELECT
  clock_timestamp() AS t_start,
  count(*) AS rows_scanned,
  pg_sleep(0.2),
  clock_timestamp() AS t_end,
  date_trunc('millisecond', clock_timestamp() - clock_timestamp()) AS elapsed;
text
           t_start           | rows_scanned | t_end | elapsed
-----------------------------+--------------+-------+---------
 2026-09-24 10:30:00.600100+08 |            5 |       | 00:00:00.2

注意 elapsed 那一列的两个 clock_timestamp() 之间没有真正的时间间隔,所以结果为 0;要测量真实耗时必须在语句的不同位置取值(例如在 PL/pgSQL 中把开始值存入变量):

sql
DO $$
DECLARE
  t_start TIMESTAMPTZ := clock_timestamp();   -- 变量里存下真实开始时刻
  t_end   TIMESTAMPTZ;
BEGIN
  PERFORM pg_sleep(0.2);                      -- 模拟业务逻辑
  t_end := clock_timestamp();
  RAISE NOTICE '耗时: % ms',
    (EXTRACT(EPOCH FROM (t_end - t_start)) * 1000)::INTEGER;
END;
$$;
text
NOTICE:  耗时: 201 ms
DO

同一条语句里的两个 clock_timestamp() 几乎必然相等(相差几微秒),只有跨语句或存入变量才能真正测出耗时。这是初学者最常踩的一个坑。

注意事项 ​

  1. now() 在同一事务内恒定,不要用它做「循环内计时」或「逐行不同时间戳」:
    sql
    -- 错误!同一事务内所有行的时间戳完全相同
    BEGIN;
    INSERT INTO t (a, created_at) VALUES (1, now()), (2, now()), (3, now());
    COMMIT;
    -- 正确:需要递增时间戳时用 clock_timestamp()
    INSERT INTO t (a, created_at) VALUES (1, clock_timestamp()),
                                         (2, clock_timestamp()),
                                         (3, clock_timestamp());
  2. now() 与 clock_timestamp() 不可互换做业务主键或幂等键。用 clock_timestamp() 生成的键在重试时会变,破坏幂等。
  3. current_timestamp 不带括号:current_timestamp() 会被解析为 current_timestamp 后跟一个空参数列表,在 PostgreSQL 中虽然可运行,但不是标准写法,建议按标准写法省略括号。
  4. timeofday() 返回 text,不能直接参与日期运算:
    sql
    -- 错误!text 与 interval 相减会尝试把 text 转成 interval
    SELECT timeofday() - INTERVAL '1 hour';
  5. localtimestamp / localtime 丢失时区信息,写入这些类型的列后无法还原绝对时刻:
    sql
    -- 不推荐:列类型丢了时区
    CREATE TABLE bad_log (created_at TIMESTAMP NOT NULL DEFAULT localtimestamp);
  6. current_date 依赖会话 TimeZone,同一时刻在不同会话可能得到不同日期。跨时区业务必须显式 SET TimeZone 或改用 (now() AT TIME ZONE 'Asia/Shanghai')::DATE。
  7. clock_timestamp() 是 VOLATILE,不能建表达式索引:
    sql
    -- 错误!VOLATILE 函数不允许出现在索引表达式中
    CREATE INDEX idx_bad ON t (clock_timestamp());
  8. clock_timestamp() 会抑制优化。在 WHERE 或子查询中使用它,会让优化器无法把子查询当作常量、无法使用某些索引路径,务必只在必要处调用。
  9. current_timestamp(n) 是四舍五入不是截断。current_timestamp(0) 在 .5 秒之后会进到下一秒,做「按秒归集」时可能让数据落到预期之外的那一秒。
  10. now() 作为默认值写在 DEFAULT 中完全安全,但写进 CHECK 约束、生成列(GENERATED ALWAYS AS ... STORED)会失败,因为生成列要求表达式 IMMUTABLE:
    sql
    -- 错误!now() 是 STABLE,不能用于生成列
    ALTER TABLE t ADD COLUMN age_days INT GENERATED ALWAYS AS
      (EXTRACT(DAY FROM now() - created_at)) STORED;
  11. 不要用应用服务器的时间替换 now()。多台应用服务器时钟漂移会造成时间倒序,数据库统一取时更可靠。
  12. statement_timestamp() 在自动提交模式下与 now() 等价(每条语句自成一个事务),只有在显式 BEGIN 后多语句事务中才会分叉。

最佳实践 ​

  1. 业务时间戳列一律用 TIMESTAMPTZ + DEFAULT now(),不要用 localtimestamp;本地渲染交给查询时的 AT TIME ZONE。
  2. 服务端 TimeZone 统一设为 UTC,让 now() 的存储与展示都确定:
    ini
    # postgresql.conf
    timezone = 'UTC'
  3. 「同一批次」用 now(),「真实先后」用 clock_timestamp(),把这条规则写进团队的建表规范。
  4. 测量语句耗时用 statement_timestamp(),测量代码段耗时用 clock_timestamp() 存入变量。
  5. 只取日期时优先 current_date,它比 date_trunc('day', now()) 更直观,但记得它随会话时区变化——需要固定业务时区时写成 (now() AT TIME ZONE 'Asia/Shanghai')::DATE。
  6. 时间戳精度按业务需要收敛。日志表用 TIMESTAMPTZ(3)(毫秒)足够,能显著降低索引体积;金融场景保留微秒。
  7. 审计表加 DEFAULT now() 之外,再考虑 DEFAULT clock_timestamp() 的排序列,前者表达「批次」,后者表达「先后」,两张列各有用途。
  8. now() 用于数据过期判断时,把阈值写成 INTERVAL 而非整数秒,可读性与类型安全都更好:
    sql
    -- 推荐
    WHERE created_at >= now() - INTERVAL '7 days'
    -- 不推荐
    WHERE created_at >= now() - 604800
  9. 查询里让 now() 只出现一次,用 CTE 或子查询固定为常量,避免优化器对每行重复求值的疑虑,也让 EXPLAIN 更易读:
    sql
    WITH anchor AS (SELECT now() AS ts)
    SELECT d.device_id, a.ts - max(d.reported_at) AS silence
    FROM device_heartbeat d, anchor a
    GROUP BY d.device_id, a.ts;
  10. 跨时区报表先 SET LOCAL TimeZone 再取 current_date,避免同一份 SQL 在不同连接上产出不同分区。
  11. 不要用 timeofday() 做任何需要解析的输出,它的格式是给日志人看的;结构化输出统一用 to_char(clock_timestamp(), 'YYYY-MM-DD HH24:MI:SS.MS')。
  12. 升级或迁移后核对 now() 的行为预期:事务时间冻结是 PostgreSQL 的稳定语义,若某次迁移后看到同一事务内时间戳跳动,应排查是否误用了 clock_timestamp()。

下一节

继续学习:EXTRACT / DATE_PART

参考链接 ​