跳到主要内容

数据分区

IEDB 按数据的时间戳组织 Parquet 文件,而非按写入时间。这使得历史数据回填正确、查询性能最优。

概述

数据时间分区确保数据根据事件实际发生的时间落入正确的分区目录中,而非写入 IEDB 的时间。

核心特性:

  • 历史回填 — 过去的数据落入正确分区(如 12 月数据进入 2024/12/ 目录)
  • 文件内排序 — 每条 Parquet 文件内数据按时间戳排序
  • 自动拆分 — 跨多小时的数据批次自动拆分为独立文件
  • 分区裁剪 — 启用准确的时间范围查询优化

数据时间分区默认开启,无需配置。


为何重要

写入时间分区的问题

传统的按写入时间分区,在回填历史数据时会产生问题:

场景:2025年1月4日写入2024年12月的传感器数据

❌ 写入时间分区:
data/mydb/cpu/2025/01/04/14/... (错误:写入当天的分区)

✅ 数据时间分区:
data/mydb/cpu/2024/12/01/14/... (正确:数据的时间戳)
data/mydb/cpu/2024/12/01/15/...

影响:

  • 查询失效 — 时间范围查询无法找到历史数据
  • 无分区裁剪 — DuckDB 必须扫描所有文件,而非仅相关分区
  • 数据混合 — 历史数据和当前数据混在同一分区
  • 压缩低效 — 时间戳混杂的文件无法高效合并

使用数据时间分区后

-- 查询12月数据仅扫描12月分区
SELECT * FROM mydb.cpu
WHERE time >= '2024-12-01' AND time < '2025-01-01'

→ DuckDB 仅扫描:data/mydb/cpu/2024/12/**/*.parquet
→ 完全跳过 2025 年所有分区

收益:

  • 10-100x 查询加速 — 分区裁剪消除无关文件
  • 历史分析准确 — 数据位于所属位置
  • 压缩高效 — 相似时间戳的文件合并在一起
  • 存储可预测 — 按日期目录管理保留策略

工作原理

单小时批次

当批次内所有记录属于同一小时:

传入批次(所有记录在 2024-12-15 14:xx):
┌─────────────────────────┬────────┬───────┐
│ time │ host │ value │
├─────────────────────────┼────────┼───────┤
│ 2024-12-15T14:05:00.000 │ srv01 │ 45.2 │
│ 2024-12-15T14:32:00.000 │ srv01 │ 47.8 │
│ 2024-12-15T14:58:00.000 │ srv01 │ 44.1 │
└─────────────────────────┴────────┴───────┘

结果:单个排序文件
→ data/mydb/cpu/2024/12/15/14/cpu_20241215_140500_123456789.parquet

跨小时批次

当批次跨多个小时,IEDB 自动拆分:

传入批次(跨 14:00-16:00):
┌─────────────────────────┬────────┬───────┐
│ time │ host │ value │
├─────────────────────────┼────────┼───────┤
│ 2024-12-15T14:30:00.000 │ srv01 │ 45.2 │
│ 2024-12-15T15:15:00.000 │ srv01 │ 47.8 │
│ 2024-12-15T15:45:00.000 │ srv01 │ 46.3 │
│ 2024-12-15T16:10:00.000 │ srv01 │ 44.1 │
└─────────────────────────┴────────┴───────┘

结果:三个独立排序文件
→ data/mydb/cpu/2024/12/15/14/cpu_*.parquet(1 条记录)
→ data/mydb/cpu/2024/12/15/15/cpu_*.parquet(2 条记录)
→ data/mydb/cpu/2024/12/15/16/cpu_*.parquet(1 条记录)

分区目录结构

数据按时间层级组织:

data/ # 存储根目录
├── default/ # 数据库
│ └── cpu/ # 表(measurement)
│ ├── 2024/ # 年份 (YYYY)
│ │ └── 12/ # 月份 (MM)
│ │ ├── 01/ # 日期 (DD)
│ │ │ ├── 14/ # 小时 (HH)
│ │ │ │ └── abc123.parquet
│ │ │ └── 15/
│ │ │ └── def456.parquet
│ │ └── 15/
│ │ └── ...
│ └── 2025/
│ └── 01/
│ └── ...

文件命名格式{measurement}_{YYYYMMDD}_{HHMMSS}_{nanos}.parquet


文件内排序

每条 Parquet 文件内的数据按时间戳升序排列:

  • 无运行时排序 — 数据预先排好
  • 高效 LIMIT — 读取 N 行后停止扫描
  • 快速聚合 — MIN/MAX 读取文件元数据
  • 最优压缩 — 相似时间戳压缩率更高

UTC 一致性

所有分区路径使用 UTC 时间,与服务器时区无关:

服务器在北京(UTC+8):
本地时间:2024-12-15 22:00 CST
UTC时间: 2024-12-15 14:00 UTC

→ 数据写入:data/mydb/cpu/2024/12/15/14/... (UTC 小时,非本地小时)

使用 UTC 确保跨时区服务器分区一致,避免夏令时切换导致分区错位。


查询分区裁剪

IEDB 查询引擎根据时间谓词自动裁剪分区:

SELECT host, AVG(value) as avg_value
FROM mydb.cpu
WHERE time >= '2024-12-01T00:00:00Z'
AND time < '2025-01-01T00:00:00Z'
GROUP BY host

执行过程:

  1. IEDB 从 WHERE 子句解析时间范围
  2. 将范围转换为分区路径:2024/12/**/*.parquet
  3. DuckDB 仅接收相关文件列表
  4. 范围外的文件永远不会被打开

性能影响:

  • 一年数据中查询一个月 → 约减少 92% 文件扫描
  • 一月数据中查询一天 → 约减少 97% 文件扫描
  • 一天数据中查询一小时 → 约减少 96% 文件扫描

历史数据回填

数据时间分区使历史回填简单直观:

import msgpack
import requests
from datetime import datetime

# 回填 2024年12月 传感器数据
historical_data = {
"m": "sensors",
"columns": {
"time": [
1701388800000000, # 2024-12-01T00:00:00Z
1701475200000000, # 2024-12-02T00:00:00Z
1701561600000000, # 2024-12-03T00:00:00Z
],
"sensor_id": ["temp-01", "temp-01", "temp-01"],
"value": [22.5, 23.1, 21.8],
}
}

response = requests.post(
"http://localhost:8000/api/v1/write/msgpack",
headers={
"Authorization": "Bearer $TOKEN",
"Content-Type": "application/msgpack",
},
data=msgpack.packb(historical_data)
)

# 数据落入正确分区:
# → data/default/sensors/2024/12/01/00/...
# → data/default/sensors/2024/12/02/00/...
# → data/default/sensors/2024/12/03/00/...

与压缩的协同

数据时间分区与文件压缩无缝配合:

  1. 写入 — 小文件写入正确的每小时分区
  2. 压缩 — 每个分区内的文件合并为大文件
  3. 结果 — 每小时拥有一个大型、排序、优化的文件
压缩前:
data/mydb/cpu/2024/12/15/14/
├── file1.parquet (5 MB, 100K 条)
├── file2.parquet (4 MB, 80K 条)
├── file3.parquet (6 MB, 120K 条)
└── ... (100+ 小文件)

压缩后:
data/mydb/cpu/2024/12/15/14/
└── compacted_abc123.parquet (450 MB, 1000万条, 已排序)

最佳实践

时间戳要求

确保时间戳精确:

# ✅ 推荐:微秒 Unix 时间戳(UTC)
"time": [1701388800000000, 1701388801000000]

# ✅ 推荐:纳秒 Unix 时间戳(UTC)
"time": [1701388800000000000, 1701388801000000000]

# ❌ 避免:字符串时间戳(需解析)
"time": ["2024-12-01T00:00:00Z", "2024-12-01T00:00:01Z"]

批量导入

导入大型历史数据集时:

  1. 先按时间排序 — 预排序数据写入更快
  2. 按小时分批 — 减少文件拆分开销
  3. 使用列式格式 — MessagePack 列式最快
# 批量导入后触发压缩
curl -X POST http://localhost:8000/api/v1/compaction/trigger \
-H "Authorization: Bearer $TOKEN"

监控分区分布

检查数据是否落入预期分区:

SELECT
EXTRACT(YEAR FROM time) as year,
EXTRACT(MONTH FROM time) as month,
COUNT(*) as records
FROM mydb.sensors
GROUP BY year, month
ORDER BY year, month