Skip to content

时序数据库Tdengine时区设计方案

🎯 核心问题

问题1: 什么是"今天"?

场景:用户查询"设备今天的数据"

┌─────────────────────────────────────────────────────────┐
│  用户在纽约 (UTC-5)                                      │
│  ├─ "今天" = 2025-10-28 00:00 ~ 23:59 (纽约时间)       │
│  └─ UTC   = 2025-10-28 05:00 ~ 2025-10-29 04:59        │
└─────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│  设备在北京 (UTC+8)                                      │
│  ├─ "今天" = 2025-10-28 00:00 ~ 23:59 (北京时间)       │
│  └─ UTC   = 2025-10-27 16:00 ~ 2025-10-28 15:59        │
└─────────────────────────────────────────────────────────┘

❓ 到底查询哪个"今天"?

问题2: 时区不都是整小时吗?

❌ 常见误解:所有时区都是 UTC±整数小时

✅ 真实情况:
┌─────────────────┬─────────────┬──────────────┐
│    国家/地区     │    时区      │   UTC偏移    │
├─────────────────┼─────────────┼──────────────┤
│ 中国            │ Asia/Shanghai│   +8:00      │
│ 印度 🇮🇳         │ Asia/Kolkata │   +5:30 ⭐   │
│ 尼泊尔 🇳🇵        │ Asia/Kathmandu│  +5:45 ⭐   │
│ 伊朗 🇮🇷         │ Asia/Tehran  │   +3:30 ⭐   │
│ 澳洲中部 🇦🇺     │ Australia/Adelaide│ +9:30 ⭐ │
│ 缅甸 🇲🇲         │ Asia/Yangon  │   +6:30 ⭐   │
└─────────────────┴─────────────┴──────────────┘

💡 必须用分钟而不是小时存储偏移量!

问题3: 夏令时怎么办?

美国纽约的时区偏移:

冬令时 (11月-3月):
  UTC-5:00  →  offset = -300 分钟

夏令时 (3月-11月):
  UTC-4:00  →  offset = -240 分钟

┌─────────────────────────────────────────────────┐
│  2025年3月9日 凌晨2点,时钟拨快1小时            │
│                                                 │
│  01:59:59 → 03:00:00                           │
│     ↓                                           │
│  这一天只有23小时!                              │
└─────────────────────────────────────────────────┘

⚠️ 偏移量不是固定的,需要动态更新!

🎨 设计原则

原则图解

                    ┌─────────────────┐
                    │   UTC时间戳     │
                    │ (唯一事实来源)   │
                    └────────┬────────┘

                    ┌────────▼────────┐
                    │  Single Source  │
                    │   of Truth      │
                    └────────┬────────┘

            ┌────────────────┼────────────────┐
            │                │                │
    ┌───────▼──────┐  ┌──────▼──────┐  ┌─────▼──────┐
    │ 存储层(MySQL)│  │时序层(TDengine)│  │ 展示层    │
    │              │  │                │  │           │
    │ • UTC时间    │  │ • UTC时间戳    │  │ • 本地时间 │
    │ • 时区信息   │  │ • 派生字段     │  │ • 用户时区 │
    │              │  │   - local_date │  │           │
    └──────────────┘  │   - local_hour │  └───────────┘
                      │   - local_minute│
                      └─────────────────┘

三层设计

┌─────────────────────────────────────────────────────────┐
│ 1️⃣  存储层:UTC为王                                      │
│    ├─ 所有时间统一存储为UTC                              │
│    ├─ timezone: 时区标识 (Asia/Shanghai)                │
│    └─ timezone_offset: 偏移分钟数 (480)                 │
└─────────────────────────────────────────────────────────┘
                        ⬇️
┌─────────────────────────────────────────────────────────┐
│ 2️⃣  计算层:预计算派生字段                               │
│    ├─ local_date: 本地日期                              │
│    ├─ local_hour: 本地小时                              │
│    └─ local_minute: 本地分钟                            │
│                                                         │
│    💡 空间换时间:查询速度提升 100-1000倍                │
└─────────────────────────────────────────────────────────┘
                        ⬇️
┌─────────────────────────────────────────────────────────┐
│ 3️⃣  展示层:灵活转换                                     │
│    ├─ 按设备时区显示                                     │
│    ├─ 按用户时区显示                                     │
│    └─ 按UTC显示                                         │
└─────────────────────────────────────────────────────────┘

🗄️ 数据库架构

MySQL - 元数据存储

sql
┌─────────────────── devices ─────────────────────┐
│                                                  │
│  device_id          VARCHAR(64)  ← 设备ID       │
│  device_name        VARCHAR(128) ← 设备名称     │
│  product_id         VARCHAR(64)  ← 产品ID       │
│                                                  │
│  ┌─────────────── 时区配置 ──────────────┐      │
│  │ timezone        VARCHAR(64)           │      │
│  │   "Asia/Shanghai"  (IANA标准)         │      │
│  │                                        │      │
│  │ timezone_offset INT                   │      │
│  │   480  (+8:00 = 480分钟)              │      │
│  │   330  (+5:30 = 330分钟) 🇮🇳          │      │
│  │   345  (+5:45 = 345分钟) 🇳🇵          │      │
│  │                                        │      │
│  │ dst_active      BOOLEAN               │      │
│  │   是否处于夏令时                        │      │
│  └────────────────────────────────────────┘     │
│                                                  │
│  latitude           DECIMAL(10,8)  ← 纬度       │
│  longitude          DECIMAL(11,8)  ← 经度       │
status             TINYINT        ← 状态       │
│  last_online_time   DATETIME       ← 最后在线   │
│  created_at         DATETIME       ← 创建时间   │
│  updated_at         DATETIME       ← 更新时间   │
└──────────────────────────────────────────────────┘

TDengine - 时序数据存储

sql
┌────────────────── device_data (超级表) ─────────────────┐
│                                                          │
│  📅 时间字段                                             │
│  ├─ ts              TIMESTAMP    ← UTC时间戳 (主字段)   │
│  │                                                      │
│  ├─ local_date      DATE         ← 派生:本地日期       │
│  ├─ local_hour      TINYINT      ← 派生:本地小时       │
│  └─ local_minute    TINYINT      ← 派生:本地分钟       │
│                                                          │
│  📊 业务数据                                             │
│  ├─ temperature     FLOAT        ← 温度                │
│  ├─ humidity        FLOAT        ← 湿度                │
│  ├─ pressure        FLOAT        ← 气压                │
│  └─ voltage         FLOAT        ← 电压                │
│                                                          │
│  🏷️  TAG字段 (不随时间变化)                              │
│  ├─ device_id       NCHAR(64)    ← 设备ID              │
│  ├─ product_id      NCHAR(64)    ← 产品ID              │
│  ├─ timezone        NCHAR(64)    ← 时区                │
│  └─ timezone_offset INT          ← 偏移(分钟)           │
└──────────────────────────────────────────────────────────┘

表关系图

┌──────────────┐
│   products   │
│              │
│  product_id  │
└──────┬───────┘
       │ 1

       │ N
┌──────▼───────┐         ┌─────────────────┐
│   devices    │ 1     N │ device_data     │
│              ├─────────┤ (TDengine)      │
│  device_id   │         │                 │
│  timezone    │         │  ts (UTC)       │
│  timezone_   │         │  local_date  ⚡  │
│    offset    │         │  local_hour  ⚡  │
│  dst_active  │         │  temperature    │
└──────────────┘         └─────────────────┘

⚡ 派生字段:从 ts + timezone 计算得出

⏰ 时区偏移详解

为什么用分钟而不是小时?

❌ 错误设计:用小时存储

timezone_offset_hours INT  -- 只能是整数

问题:
  • 中国 +8:00   ✅ 可以存储为 8
  • 印度 +5:30   ❌ 无法存储(5.5小时)
  • 尼泊尔 +5:45 ❌ 无法存储(5.75小时)
✅ 正确设计:用分钟存储

timezone_offset INT  -- 可以是任意整数

示例:
  • 中国    +8:00   →  480 分钟  (8 × 60)
  • 印度    +5:30   →  330 分钟  (5 × 60 + 30)
  • 尼泊尔  +5:45   →  345 分钟  (5 × 60 + 45)
  • 伊朗    +3:30   →  210 分钟  (3 × 60 + 30)
  • 纽约    -5:00   → -300 分钟  (冬令时)
  • 纽约    -4:00   → -240 分钟  (夏令时)

时区偏移计算示例

案例:印度设备 🇮🇳

┌──────────────────────────────────────────────────┐
│  时区:Asia/Kolkata                              │
│  偏移:UTC+5:30 = 330分钟                        │
└──────────────────────────────────────────────────┘

UTC时间:2025-10-28 10:00:00

         + 5小时30分 (330分钟)

本地时间:2025-10-28 15:30:00

┌─────────┬──────────────┬──────────────┐
│  字段    │   存储值     │    说明      │
├─────────┼──────────────┼──────────────┤
│ ts      │ 1730113200000│ UTC时间戳    │
│         │ (毫秒)       │              │
├─────────┼──────────────┼──────────────┤
│ local_  │ 2025-10-28   │ 派生:本地日期│
│ date    │              │              │
├─────────┼──────────────┼──────────────┤
│ local_  │ 15           │ 派生:本地小时│
│ hour    │              │              │
├─────────┼──────────────┼──────────────┤
│ local_  │ 30           │ 派生:本地分钟│
│ minute  │              │              │
└─────────┴──────────────┴──────────────┘

全球时区偏移一览

                   🌍 全球时区偏移分布

-12h              0 (UTC)              +12h   +14h
  │━━━━━━━━━━━━━━━│━━━━━━━━━━━━━━━━━━━│━━━━━│
  │               │                   │     │
  ▼               ▼                   ▼     ▼

🇺🇸 纽约        🇬🇧 伦敦           🇨🇳 北京   🇳🇿 新西兰
UTC-5/-4       UTC+0             UTC+8     UTC+12

        🇮🇳 印度         🇦🇺 阿德莱德
        UTC+5:30 ⭐     UTC+9:30 ⭐
              ↑               ↑
          非整小时偏移  (必须用分钟存储!)


特殊时区示例:
┌────────────┬──────────────┬────────────┐
│   地区      │   标准时区    │  偏移(分钟) │
├────────────┼──────────────┼────────────┤
│ 印度 🇮🇳     │ UTC+5:30     │    +330    │
│ 尼泊尔 🇳🇵   │ UTC+5:45     │    +345    │
│ 伊朗 🇮🇷     │ UTC+3:30     │    +210    │
│ 阿富汗 🇦🇫   │ UTC+4:30     │    +270    │
│ 缅甸 🇲🇲     │ UTC+6:30     │    +390    │
│ 查塔姆群岛 🇳🇿│ UTC+12:45   │    +765    │
└────────────┴──────────────┴────────────┘

🔄 数据流转

完整数据流程图

┌─────────────┐
│  设备上报    │
│  (任意时区)  │
└──────┬──────┘

       │ {"timestamp": "2025-10-28T10:00:00+05:30",
       │  "temperature": 25.5}


┌──────────────────────────────────────────┐
│  Step 1: 解析时间戳                       │
│  ├─ 识别时区:+05:30 (印度时间)          │
│  └─ 转换为UTC: 2025-10-28T04:30:00Z     │
└──────┬───────────────────────────────────┘


┌──────────────────────────────────────────┐
│  Step 2: 查询设备时区配置                 │
│  ├─ MySQL: SELECT timezone, timezone_    │
│  │          offset FROM devices           │
│  │          WHERE device_id = 'xxx'      │
│  └─ 结果: Asia/Kolkata, offset=330       │
└──────┬───────────────────────────────────┘


┌──────────────────────────────────────────┐
│  Step 3: 计算派生字段                     │
│                                          │
│  UTC: 2025-10-28 04:30:00               │
│   +   5小时30分 (330分钟)                │
│   =   2025-10-28 10:00:00 (本地)        │
│                                          │
│  派生:                                   │
│  ├─ local_date = 2025-10-28             │
│  ├─ local_hour = 10                     │
│  └─ local_minute = 0                    │
└──────┬───────────────────────────────────┘


┌──────────────────────────────────────────┐
│  Step 4: 写入TDengine                    │
│                                          │
│  INSERT INTO device_data VALUES (        │
│    ts:           1730091000000,         │
│    local_date:   '2025-10-28',          │
│    local_hour:   10,                    │
│    local_minute: 0,                     │
│    temperature:  25.5                   │
│  )                                       │
└──────┬───────────────────────────────────┘


┌──────────────────────────────────────────┐
│  Step 5: 查询展示                         │
│  ├─ 设备时区:显示 2025-10-28 10:00      │
│  ├─ 用户时区:根据用户位置转换            │
│  └─ UTC:显示 2025-10-28 04:30          │
└──────────────────────────────────────────┘

写入详细流程

java
// 写入时的计算过程

┌────────────────────────────────────────────────┐
1. 接收设备数据                                │
│  {                                             │
"device_id": "device_india_001",           │
"timestamp": "2025-10-28T10:00:00+05:30", │
"temperature": 25.5
│  }                                             │
└────────────────────────────────────────────────┘
                    ⬇️
┌────────────────────────────────────────────────┐
2. 解析并规范化为UTC                           │
│  Instant utc = Instant.parse(timestamp);      │
// 结果: 2025-10-28T04:30:00Z               │
└────────────────────────────────────────────────┘
                    ⬇️
┌────────────────────────────────────────────────┐
3. 查询设备时区配置                            │
│  Device device = getDevice(device_id);        │
// timezone: "Asia/Kolkata"                  │
// timezone_offset: 330                      │
└────────────────────────────────────────────────┘
                    ⬇️
┌────────────────────────────────────────────────┐
4. 计算本地时间字段                            │
│  ZonedDateTime local = utc.atZone(            │
│    ZoneId.of("Asia/Kolkata")                  │
│  );                                            │
// 结果: 2025-10-28T10:00:00+05:30          │
│                                                │
│  local_date   = local.toLocalDate()           │
= 2025-10-28
│  local_hour   = local.getHour()               │
= 10
│  local_minute = local.getMinute()             │
= 0
└────────────────────────────────────────────────┘
                    ⬇️
┌────────────────────────────────────────────────┐
5. 写入TDengine(包含所有字段)                │
│  {                                             │
│    ts:              1730091000000,  ← UTC     │
│    local_date:      "2025-10-28",   ← 派生    │
│    local_hour:      10,             ← 派生    │
│    local_minute:    0,              ← 派生    │
│    temperature:     25.5,           ← 业务    │
│    timezone:        "Asia/Kolkata", ← TAG     │
│    timezone_offset: 330             ← TAG     │
│  }                                             │
└────────────────────────────────────────────────┘

🔍 查询场景

场景1: 按设备本地日期查询

sql
-- 需求:查询印度设备在2025-10-28的数据

┌─────────────────────────────────────────────────┐
│  ❌ 方法1:实时计算(慢,不推荐)                 │
│                                                 │
SELECT
│    ts,                                          │
DATE(ADDTIME(ts,                            │
│         SEC_TO_TIME(330*60)))                  │
as local_date,                         │
│    temperature                                  │
FROM device_data                               │
WHERE device_id = 'device_india_001'
AND DATE(ADDTIME(ts,                        │
│              SEC_TO_TIME(330*60)))             │
= '2025-10-28'
│                                                 │
│  问题:                                          │
│  • 每行都要计算时间转换                          │
│  • 无法使用索引                                  │
│  • 查询时间:5-30秒 (百万级数据)                 │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│  ✅ 方法2:使用预存字段(快,推荐)               │
│                                                 │
SELECT
│    ts,                                          │
│    local_date,                                  │
│    local_hour,                                  │
│    local_minute,                                │
│    temperature                                  │
FROM device_data                               │
WHERE device_id = 'device_india_001'
AND local_date = '2025-10-28'  ← 直接过滤    │
│                                                 │
│  优势:                                          │
│  • 直接使用索引                                  │
│  • 不需要计算                                    │
│  • 查询时间:50-200毫秒                         │
│  • 速度提升:100-1000倍!                       │
└─────────────────────────────────────────────────┘

场景2: 按本地时间统计

sql
-- 需求:统计设备每天的平均温度

┌────────────────────────────────────────────────┐
│  查询:按设备本地日期分组统计                    │
│                                                │
SELECT
│    local_date,                                 │
AVG(temperature) as avg_temp,              │
MAX(temperature) as max_temp,              │
MIN(temperature) as min_temp,              │
COUNT(*) as data_count                     │
FROM device_data                              │
WHERE device_id = 'device_india_001'
AND local_date >= '2025-10-01'
AND local_date <= '2025-10-31'
GROUP BY local_date                          │
ORDER BY local_date                          │
│                                                │
│  执行计划:                                     │
1. 使用 device_id 索引定位数据                │
2. 使用 local_date 索引过滤范围               │
3. 直接分组聚合(无需计算)                    │
│                                                │
│  性能:毫秒级返回 ⚡                             │
└────────────────────────────────────────────────┘

场景3: 跨时区设备对比

需求:对比中国、印度、尼泊尔设备在各自"本地2025-10-28"的数据

┌──────────────┬──────────────┬─────────────────────┐
│   设备位置    │    时区       │  本地10-28对应UTC   │
├──────────────┼──────────────┼─────────────────────┤
│ 中国 🇨🇳      │ UTC+8:00     │ 10-27 16:00 ~      │
│              │ offset=480   │ 10-28 15:59        │
├──────────────┼──────────────┼─────────────────────┤
│ 印度 🇮🇳      │ UTC+5:30 ⭐  │ 10-27 18:30 ~      │
│              │ offset=330   │ 10-28 18:29        │
├──────────────┼──────────────┼─────────────────────┤
│ 尼泊尔 🇳🇵    │ UTC+5:45 ⭐  │ 10-27 18:15 ~      │
│              │ offset=345   │ 10-28 18:14        │
└──────────────┴──────────────┴─────────────────────┘

SQL查询:
┌────────────────────────────────────────────────┐
│  -- 每个设备用自己的local_date查询              │
│                                                │
│  SELECT                                        │
│    device_id,                                  │
│    timezone,                                   │
│    AVG(temperature) as avg_temp,              │
│    COUNT(*) as data_count                     │
│  FROM device_data                              │
│  WHERE device_id IN (                          │
│    'device_china',                             │
│    'device_india',                             │
│    'device_nepal'                              │
│  )                                             │
│  AND local_date = '2025-10-28'  ← 统一条件     │
│  GROUP BY device_id, timezone                 │
│                                                │
│  💡 虽然UTC时间范围不同,但local_date统一!     │
└────────────────────────────────────────────────┘

结果示例:
┌─────────────┬───────────┬──────────┬────────────┐
│  device_id  │ timezone  │ avg_temp │ data_count │
├─────────────┼───────────┼──────────┼────────────┤
│ device_china│ UTC+8:00  │  22.5°C  │    1440    │
│ device_india│ UTC+5:30⭐│  28.3°C  │    1440    │
│ device_nepal│ UTC+5:45⭐│  18.7°C  │    1440    │
└─────────────┴───────────┴──────────┴────────────┘

场景4: 24小时模式分析

sql
-- 需求:分析设备在"本地每个小时"的温度模式

SELECT 
  local_hour,
  AVG(temperature) as avg_temp,
  STDDEV(temperature) as std_temp,
  COUNT(*) as sample_count
FROM device_data
WHERE device_id = 'device_india_001'
  AND ts >= NOW - 30d  -- 最近30天
GROUP BY local_hour
ORDER BY local_hour

根据用户本地时区查询

核心原则

┌────────────────────────────────────────────────┐
│  按用户时区查询的3个步骤                        │
│                                                │
│  1️⃣  用户输入 → UTC时间范围                    │
│     ├─ 应用层计算                              │
│     ├─ 考虑用户时区偏移                        │
│     └─ 考虑夏令时状态                          │
│                                                │
│  2️⃣  UTC范围 → 查询数据库                      │
│     ├─ WHERE ts >= start AND ts < end         │
│     ├─ 充分利用UTC索引                         │
│     └─ 高性能查询                              │
│                                                │
│  3️⃣  结果 → 用户时区展示                       │
│     ├─ SELECT (ts + offset) as user_time      │
│     ├─ 一次性计算                              │
│     └─ 返回给前端                              │
└────────────────────────────────────────────────┘

SQL模板

sql
-- 模板1: 按用户本地日期查询
SELECT 
    ts,
    CAST((ts + :user_offset * 60000) / 86400000 AS DATE) as user_date,
    CAST(((ts + :user_offset * 60000) % 86400000) / 3600000 AS INT) as user_hour,
    temperature
FROM device_data
WHERE device_id = :device_id
  AND ts >= :start_utc_ms
  AND ts < :end_utc_ms
ORDER BY ts;

-- 模板2: 按用户本地日期分组统计
SELECT 
    CAST((ts + :user_offset * 60000) / 86400000 AS DATE) as user_date,
    AVG(temperature) as avg_temp,
    COUNT(*) as count
FROM device_data
WHERE device_id IN (:device_ids)
  AND ts >= :start_utc_ms
  AND ts < :end_utc_ms
GROUP BY user_date
ORDER BY user_date;

-- 模板3: 按用户本地小时统计
SELECT 
    CAST(((ts + :user_offset * 60000) % 86400000) / 3600000 AS INT) as user_hour,
    AVG(temperature) as avg_temp
FROM device_data
WHERE device_id = :device_id
  AND ts >= :start_utc_ms
  AND ts < :end_utc_ms
GROUP BY user_hour
ORDER BY user_hour;

关键要点

✅ DO's
  • 应用层计算UTC时间范围
  • SQL用UTC时间戳查询(高效)
  • SELECT中计算用户本地时间

📊 性能对比

查询性能对比

测试场景:查询设备某天的数据
数据量:1000万条记录

┌────────────────────┬──────────┬────────────┬──────────┐
│      方案          │ 查询时间  │  CPU占用   │  索引    │
├────────────────────┼──────────┼────────────┼──────────┤
│ 方案1:实时计算     │ 5-30秒   │   90-95%   │ ❌ 无效  │
│ (每次转换时区)     │          │            │          │
├────────────────────┼──────────┼────────────┼──────────┤
│ 方案2:预存字段     │ 50-200ms │   10-20%   │ ✅ 有效  │
│ (local_date)      │          │            │          │
├────────────────────┼──────────┼────────────┼──────────┤
│ 性能提升           │ 100-600倍│    5-10倍  │   ---    │
└────────────────────┴──────────┴────────────┴──────────┘

┌──────────────────────────────────────────────┐
│  详细对比                                    │
│                                              │
│  ❌ 方案1:实时计算                           │
│  SELECT ... WHERE                            │
│    DATE(ADDTIME(ts, SEC_TO_TIME(offset*60)))│
│    = '2025-10-28'                           │
│                                              │
│  执行计划:                                   │
│  1. 全表扫描                                 │
│  2. 每行计算时间转换                          │
│  3. 过滤结果                                 │
│                                              │
│  问题:                                       │
│  • 函数破坏了索引                             │
│  • CPU密集型计算                             │
│  • 无法并行优化                               │
│                                              │
│  ✅ 方案2:预存字段                           │
│  SELECT ... WHERE                            │
│    local_date = '2025-10-28'                │
│                                              │
│  执行计划:                                   │
│  1. 使用索引直接定位                          │
│  2. 范围扫描                                 │
│  3. 返回结果                                 │
│                                              │
│  优势:                                       │
│  • B树索引高效查找                            │
│  • IO密集型(速度快)                         │
│  • 支持并行扫描                               │
└──────────────────────────────────────────────┘

存储成本对比

┌──────────────────────────────────────────────┐
│  数据大小分析 (每条记录)                      │
│                                              │
│  基础字段:                                   │
│  ├─ ts (TIMESTAMP)       : 8 字节           │
│  ├─ temperature (FLOAT)  : 4 字节           │
│  ├─ humidity (FLOAT)     : 4 字节           │
│  └─ 其他业务字段          : ~16 字节         │
│      小计: 32 字节                           │
│                                              │
│  派生字段(新增):                            │
│  ├─ local_date (DATE)    : 4 字节           │
│  ├─ local_hour (TINYINT) : 1 字节           │
│  └─ local_minute (TINYINT): 1 字节          │
│      小计: 6 字节                            │
│                                              │
│  存储增加:6 / 32 = 18.75%                   │
│                                              │
└──────────────────────────────────────────────┘

✅ 最佳实践

设计检查清单

✅ 数据存储
  ├─ [✓] UTC时间戳作为唯一真相
  ├─ [✓] 使用分钟存储时区偏移
  ├─ [✓] 存储IANA时区标识符
  ├─ [✓] 预计算local_date, local_hour, local_minute
  └─ [✓] 记录DST状态

✅ 数据写入
  ├─ [✓] 写入时计算所有派生字段
  ├─ [✓] 验证时区配置正确性
  ├─ [✓] 使用批量写入提升性能
  └─ [✓] 记录详细日志便于排查

✅ 数据查询
  ├─ [✓] 优先使用预存的local字段
  ├─ [✓] 建立合适的复合索引
  ├─ [✓] 避免在WHERE中使用函数
  └─ [✓] 支持多种时区模式

✅ 运维维护
  ├─ [✓] 定时更新设备时区偏移
  ├─ [✓] 监控夏令时切换
  ├─ [✓] 验证派生字段一致性
  └─ [✓] 提供时区配置管理界面

关键代码模板

java
// ✅ 正确的写入模式
public void saveDeviceData(String deviceId, DeviceDataDTO dto) {
    // 1. 获取设备时区配置
    Device device = getDevice(deviceId);
    
    // 2. 规范化为UTC
    Instant utc = parseToUtc(dto.getTimestamp());
    
    // 3. 计算派生字段
    ZonedDateTime local = utc.atZone(
        ZoneId.of(device.getTimezone())
    );
    
    // 4. 一次性写入所有字段
    DataPoint point = DataPoint.builder()
        .ts(utc)                          // UTC主字段
        .localDate(local.toLocalDate())   // 派生字段
        .localHour(local.getHour())       // 派生字段
        .localMinute(local.getMinute())   // 派生字段
        .temperature(dto.getTemperature())// 业务字段
        .build();
    
    save(point);
}

// ✅ 正确的查询模式
public List<DataPoint> queryByLocalDate(
        String deviceId, 
        LocalDate date) {
    
    // 直接使用预存字段,不要计算!
    String sql = 
        "SELECT * FROM device_data " +
        "WHERE device_id = ? " +
        "  AND local_date = ? " +  // ← 关键:预存字段
        "ORDER BY ts";
    
    return query(sql, deviceId, date);
}

常见错误及解决方案

❌ 错误1:用小时存储偏移
  timezone_offset_hours INT
  
  问题:无法表示 +5:30, +5:45
  
✅ 解决:用分钟
  timezone_offset INT  -- 330, 345, etc.

---

❌ 错误2:只存UTC不存派生字段
  CREATE TABLE (ts TIMESTAMP, ...)
  
  问题:查询时实时计算,性能差
  
✅ 解决:预存local字段
  CREATE TABLE (
    ts TIMESTAMP,
    local_date DATE,
    local_hour TINYINT,
    ...
  )

---

❌ 错误3:WHERE中使用函数计算
  WHERE DATE(ts + offset) = '2025-10-28'
  
  问题:无法使用索引
  
✅ 解决:用预存字段
  WHERE local_date = '2025-10-28'

---

❌ 错误4:硬编码偏移量
  int offset = 480;  -- 固定+8小时
  
  问题:无法处理夏令时
  
✅ 解决:动态获取
  int offset = getCurrentOffset(timezone);

---

❌ 错误5:忘记处理夏令时切换
  设备时区偏移never更新
  
  问题:夏令时期间数据错误
  
✅ 解决:定时任务
  @Scheduled(cron = "0 0 * * * ?")
  public void updateTimezoneOffsets() {
    // 检查并更新所有设备偏移
  }

性能优化建议

┌────────────────────────────────────────────────┐
│  1️⃣  索引优化                                   │
│                                                │
│  -- 设备+本地日期复合索引                       │
│  CREATE INDEX idx_device_local_date           │
│  ON device_data(device_id, local_date);       │
│                                                │
│  -- 设备+本地小时复合索引                       │
│  CREATE INDEX idx_device_local_hour           │
│  ON device_data(device_id, local_date,        │
│                 local_hour);                  │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│  2️⃣  分层查询                                   │
│                                                │
│  • 7天内:查原始表 (按小时)                     │
│  • 7-90天:查每日汇总表                         │
│  • 90天以上:查每月汇总表                       │
│                                                │
│  优势:根据时间跨度自动选择最优粒度             │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│  3️⃣  物化视图/流式计算                          │
│                                                │
│  -- TDengine创建自动聚合流                     │
│  CREATE STREAM device_data_daily AS           │
│  SELECT                                        │
│    _wstart as day,                            │
│    device_id,                                  │
│    AVG(temperature) as avg_temp               │
│  FROM device_data                              │
│  INTERVAL(1d, timezone_offset * 60);          │
│                                                │
│  优势:实时汇总,查询更快                       │
└────────────────────────────────────────────────┘

┌────────────────────────────────────────────────┐
│  4️⃣  批量操作                                   │
│                                                │
│  • 批量写入:每次1000-5000条                   │
│  • 批量更新:使用事务                           │
│  • 避免逐条操作                                 │
│                                                │
│  性能提升:10-100倍                            │
└────────────────────────────────────────────────┘

📝 总结

核心设计理念

                  ┌─────────────┐
                  │  UTC is     │
                  │  the Truth  │
                  └──────┬──────┘

        ┌────────────────┼────────────────┐
        │                │                │
   ┌────▼────┐    ┌──────▼──────┐   ┌────▼────┐
   │ 唯一真相 │    │  派生字段    │   │ 灵活展示 │
   │         │    │             │   │         │
   │ • UTC   │    │ • local_date│   │ • 设备   │
   │   时间戳 │───▶│ • local_hour│──▶│   时区   │
   │         │    │ • local_    │   │ • 用户   │
   │ • IANA  │    │   minute    │   │   时区   │
   │   时区   │    │             │   │ • UTC   │
   │         │    │ 空间换时间   │   │         │
   │ • 偏移   │    │ 100倍加速   │   │ 按需转换 │
   │  (分钟) │    │             │   │         │
   └─────────┘    └─────────────┘   └─────────┘

三个关键原则

1️⃣  UTC为王
   ├─ 所有时间统一存储为UTC
   ├─ UTC是唯一的时间真相
   └─ 派生字段可以重新计算

2️⃣  空间换时间
   ├─ 预计算local_date, local_hour, local_minute
   ├─ 增加<20%存储,提升100-1000倍查询速度
   └─ 投资回报率极高

3️⃣  分钟为单位
   ├─ timezone_offset用分钟存储
   ├─ 支持+5:30, +5:45等非整小时时区
   └─ 覆盖全球所有时区