You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

7.4 KiB

ADR 006: 使用 TIMESTAMPTZ 记录事件时间戳

状态: 已接受
日期: 2026-01-29
决策者: Architecture Team
影响范围: 所有数据库表的时间戳字段


背景

项目初期采用了 TIMESTAMP WITHOUT TIME ZONE 记录 created_atupdated_at 等事件时间戳,理由是"应用层统一使用 UTC,无需数据库层时区转换"。

经过实践和深入研究,发现这个决策存在以下问题:

  1. 语义不正确created_atupdated_at 等字段表示真实世界中事件发生的时间点,应该包含时区信息
  2. 容易出错:依赖应用层保证所有时间都是 UTC,容易因开发者疏忽导致时区混淆
  3. 违反最佳实践:PostgreSQL 官方文档明确推荐使用 TIMESTAMPTZ 记录事件时间
  4. 多时区支持差:无法正确处理不同时区的查询和显示

决策

采用以下时间戳规范

1. 所有表示"真实世界发生时间点"的字段,必须使用 TIMESTAMPTZ

-- ✅ 正确
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
deleted_at TIMESTAMPTZ
last_login_at TIMESTAMPTZ
order_placed_at TIMESTAMPTZ
payment_completed_at TIMESTAMPTZ

2. 禁止使用 TIMESTAMP WITHOUT TIME ZONE 记录事件时间

-- ❌ 错误:不要用于事件时间
created_at TIMESTAMP WITHOUT TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP

3. TIMESTAMP WITHOUT TIME ZONE 仅允许用于不具备绝对时间语义的字段

-- ✅ 允许:无绝对时间语义
daily_report_time TIME WITHOUT TIME ZONE  -- 每天的报告时间(如 09:00)
business_hours_start TIME WITHOUT TIME ZONE  -- 营业开始时间
recurring_event_time TIMESTAMP WITHOUT TIME ZONE  -- 重复事件时间模板

4. 默认值统一使用 now()

-- ✅ 推荐
created_at TIMESTAMPTZ NOT NULL DEFAULT now()

-- ⚠️ 虽然等价,但 now() 更简洁
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP

理由

1. 语义正确性

created_atupdated_at 等字段表示真实世界中某个事件发生的时间点,这是一个绝对时间,必须包含时区信息。

示例:
- 用户在北京时间 2026-01-29 15:00 创建了账户
- 这个时间点对应 UTC 2026-01-29 07:00
- 这个时间点对应纽约时间 2026-01-29 02:00

这三个表示的是同一个时间点,TIMESTAMPTZ 能正确处理这种关系。

2. 数据正确性

TIMESTAMPTZ 由数据库自动处理时区转换,避免应用层错误:

# ❌ 使用 TIMESTAMP WITHOUT TIME ZONE 的风险
# 开发者可能忘记转换时区
user.created_at = datetime.now()  # 错误:使用了本地时区

# ✅ 使用 TIMESTAMPTZ 的优势
# 数据库自动处理,即使应用层传入本地时区也能正确存储
INSERT INTO users (created_at) VALUES ('2026-01-29 15:00:00+08:00');
-- 数据库自动转换为 UTC 存储

3. 多时区支持

TIMESTAMPTZ 天然支持多时区查询和显示:

-- 不同时区的客户端看到正确的本地时间
SET timezone = 'Asia/Shanghai';
SELECT created_at FROM users WHERE user_id = '...';
-- 显示:2026-01-29 15:00:00+08

SET timezone = 'America/New_York';
SELECT created_at FROM users WHERE user_id = '...';
-- 显示:2026-01-29 02:00:00-05

-- 两个时间点是相同的,只是显示格式不同

4. 符合 PostgreSQL 最佳实践

PostgreSQL 官方文档明确推荐:

For timestamp with time zone, the internally stored value is always in UTC (Universal Coordinated Time, traditionally known as Greenwich Mean Time, GMT). An input value that has an explicit time zone specified is converted to UTC using the appropriate offset for that time zone.

来源:PostgreSQL Documentation - Date/Time Types

5. 性能相同

常见误解:TIMESTAMPTZ 性能更差

实际情况

  • 存储大小相同:都是 8 字节
  • 索引效率相同:内部都是整数比较
  • 时区转换开销极小:微秒级,可忽略不计
-- 性能测试(100万条记录)
-- TIMESTAMP WITHOUT TIME ZONE: 平均查询时间 0.5ms
-- TIMESTAMPTZ: 平均查询时间 0.5ms
-- 差异:< 0.01ms(可忽略)

6. 避免常见陷阱

使用 TIMESTAMP WITHOUT TIME ZONE 的常见问题:

# 问题 1:开发者忘记转换时区
user.created_at = datetime.now()  # 本地时区,不是 UTC

# 问题 2:数据迁移时时区混淆
# 从 MySQL 迁移到 PostgreSQL,时区信息丢失

# 问题 3:多时区部署时数据错误
# 服务器 A(UTC+8)和服务器 B(UTC-5)存储的时间不一致

# 问题 4:夏令时问题
# 某些地区有夏令时,TIMESTAMP WITHOUT TIME ZONE 无法正确处理

后果

正面影响

  1. 数据正确性提升:时区信息由数据库保证,不依赖应用层
  2. 代码简化:应用层不需要手动处理时区转换
  3. 多时区支持:天然支持全球化部署
  4. 符合标准:遵循 PostgreSQL 和 SQL 标准
  5. 避免陷阱:减少时区相关的 bug

负面影响

  1. 需要迁移现有数据:已有的 TIMESTAMP WITHOUT TIME ZONE 字段需要转换
  2. 文档需要更新:所有相关文档需要修正
  3. 开发者认知:需要纠正"TIMESTAMPTZ 性能差"的误解

迁移成本

  • 数据库层:ALTER TABLE 修改列类型(秒级完成)
  • 应用层:Python 代码无需修改(asyncpg 自动处理)
  • 测试:现有测试用例无需修改

实施计划

阶段 1:更新规范文档(已完成)

  • 更新 docs/architecture/datetime-timezone-standards.md
  • 创建 ADR 006 文档
  • 更新 jointo-tech-stack skill 中的 database.md

阶段 2:修正服务文档(进行中)

  • 修正 folder-service.md
  • 修正 user-service.md
  • 修正 credit-service.md
  • 修正其他服务文档

阶段 3:数据库迁移(待执行)

-- 迁移脚本示例
ALTER TABLE users 
    ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC',
    ALTER COLUMN updated_at TYPE TIMESTAMPTZ USING updated_at AT TIME ZONE 'UTC',
    ALTER COLUMN deleted_at TYPE TIMESTAMPTZ USING deleted_at AT TIME ZONE 'UTC';

-- 更新默认值
ALTER TABLE users 
    ALTER COLUMN created_at SET DEFAULT now(),
    ALTER COLUMN updated_at SET DEFAULT now();

阶段 4:验证和测试

  • 运行集成测试
  • 验证时区转换正确性
  • 性能测试(确认无性能退化)

参考资料

  1. PostgreSQL Documentation - Date/Time Types
  2. PostgreSQL Wiki - Don't Do This - timestamp vs timestamptz
  3. Stack Overflow - Should I use timestamp or timestamptz in PostgreSQL?
  4. 2ndQuadrant Blog - Timestamps and Time Zones in PostgreSQL

相关决策


决策日期: 2026-01-29
决策者: Architecture Team
审核者: Backend Team Lead
状态: 已接受