now() 是 PostgreSQL 里最常被随手调用的函数之一,取当前时间、写日志、算过期……几乎无处不在。但如果你在事务里用它,它返回的并不是"此刻",而是事务开始的那一刻。一个跑了两分钟的事务,从头到尾每次 now() 拿到的都是同一个时间戳。这个行为符合 SQL 标准,却和大多数人的直觉相悖,最近 Emmett 库的作者 Marcin 就因此踩了一个实打实的 bug。
事务级时间冻结:到底发生了什么
PostgreSQL 的时间函数分两类:
| 函数 | 时间基准 | 同一事务内是否变化 |
|---|---|---|
now() / current_timestamp |
事务开始 | ❌ 冻结 |
clock_timestamp() |
真实墙钟时间 | ✅ 每次调用都更新 |
下面直接跑一段 SQL 就能看到差异:
-- 开启事务
BEGIN;
-- 第一次调用
SELECT now() AS txn_now_1, clock_timestamp() AS wall_1;
-- 等待 3 秒(在 psql 里可以用 \sleep,这里用 pg_sleep)
SELECT pg_sleep(3);
-- 第二次调用
SELECT now() AS txn_now_2, clock_timestamp() AS wall_2;
COMMIT;
运行结果大致如下:
txn_now_1 | wall_1
2025-06-15 10:00:00 | 2025-06-15 10:00:00
txn_now_2 | wall_2
2025-06-15 10:00:00 | 2025-06-15 10:00:03
now() 两次返回完全一致,而 clock_timestamp() 正确推进了 3 秒。这不是 bug,是 SQL 标准定义的行为——current_timestamp 取的是语句开始的时间,PostgreSQL 把"语句"升级成了"事务",让同一事务内所有操作共享一个时间基准,保证一致性。
这个"一致性"什么时候会变成坑
想象一个典型场景:批量处理订单,每条订单记录自己的处理时间。
import psycopg
conn = psycopg.connect("dbname=shop")
with conn.transaction():
for order_id in order_ids:
# 处理逻辑……可能每条耗时几十毫秒
process_order(order_id)
conn.execute(
"UPDATE orders SET processed_at = now() WHERE id = %s",
[order_id]
)
如果处理了 1000 条订单,总耗时 30 秒,那么所有订单的 processed_at 都是同一秒。你事后想按处理顺序排查问题,时间字段完全帮不上忙。
更隐蔽的坑出现在过期判断里:
BEGIN;
-- 检查会话是否过期(假设超时 5 分钟)
SELECT * FROM sessions
WHERE expires_at > now(); -- 事务开始时的时间
-- ……这里做了一堆耗时操作,实际已经过了 5 分钟……
-- 再次检查,now() 还是事务开始时的时间
-- 本该过期的会话仍然"看起来有效"
SELECT * FROM sessions
WHERE expires_at > now();
COMMIT;
事务越长,这个偏差越大。几毫秒的短事务基本无感,但涉及外部 API 调用、批量计算的长事务就可能产生实质性的逻辑错误。
怎么取到"真正的现在"
方案一:换函数——clock_timestamp()
最直接的办法,把 now() 替换为 clock_timestamp():
-- 需要真实时间的地方
UPDATE orders SET processed_at = clock_timestamp() WHERE id = 123;
但要注意:如果你确实需要事务内时间一致性(比如同一笔事务的所有日志记录同一个时间戳以便关联),就不要盲目替换。两种函数各有适用场景,关键是知道区别后按需选择。
方案二:把时间戳从应用层传进去
在 Python 侧生成时间,作为参数传入 SQL,彻底绕开数据库的时间基准问题:
from datetime import datetime, timezone
with conn.transaction():
for order_id in order_ids:
process_order(order_id)
# 每条记录取自己的真实时间
processed_at = datetime.now(timezone.utc)
conn.execute(
"UPDATE orders SET processed_at = %s WHERE id = %s",
[processed_at, order_id]
)
好处是时间来源完全可控,不受事务长短影响,也不依赖特定 PostgreSQL 函数。缺点是应用层时间和数据库服务器时间可能有微小偏差,对大多数业务场景可以忽略,但对强一致性要求高的系统(比如金融对账)需要确保应用和数据库使用同一个时间源(NTP 同步或数据库时间函数)。
方案三:缩短事务
很多时间冻结问题本质上是事务太长。把批量操作拆成多个短事务,每个事务几秒内完成,now() 的偏差就小到可以忽略:
for order_id in order_ids:
with conn.transaction(): # 每条订单一个事务
process_order(order_id)
conn.execute(
"UPDATE orders SET processed_at = now() WHERE id = %s",
[order_id]
)
代价是失去了跨订单的原子性——如果第 500 条失败了,前 499 条已经提交。需要根据业务决定是否可以接受部分成功,或者引入补偿机制。
一份自查清单
在用到 now() / current_timestamp 的项目里,可以快速排查:
- 事务内是否有耗时操作(外部调用、批量循环、大查询)?如果有,检查依赖
now()的逻辑是否需要真实时间。 - 时间字段是否用于排序或过期判断?如果是,确认
now()的冻结行为是否会导致误判。 - 是否需要事务内时间一致性?如果同一事务的多条记录必须共享同一时间戳(审计日志关联),保留
now();如果每条记录需要独立时间,换clock_timestamp()或应用层传入。 - 短事务场景可以直接用
now(),偏差在毫秒级,基本无感。
PostgreSQL 的这个行为不是 bug,而是标准定义的特性。问题出在我们把 now() 当成了"墙钟时间"的快捷方式,却忘了它在事务里是一把冻结的秒表。知道区别之后,选哪个函数就不再是盲猜,而是根据场景做出明确决定。