sfsDb 时序数据处理指南
1. 时序数据概述
时序数据是按时间顺序记录的一系列数据点,具有以下特点:
- 数据按时间顺序生成
- 通常具有高写入频率
- 常用范围查询(如查询最近N小时数据)
- 数据量随时间线性增长
- 支持多种精度需求(秒、毫秒、微秒)
2. sfsDb 时序数据支持
2.1 时间类型支持
| 类型 | 存储格式 | 优点 | 缺点 |
|---|---|---|---|
time.Time | 字符串(格式:2006-01-02 15:04:05) | 可读性好,易于理解 | 占用空间大(约19字节),比较速度慢 |
int64(时间戳) | 8字节二进制 | 存储效率高,比较速度快 | 可读性差,需要转换为时间格式查看 |
2.2 sfsDb 时间序列化机制
sfsDb 通过 util.AnyToBytes 函数将不同类型转换为字节数组:
// time.Time 序列化
case time.Time:
return []byte(val.Format(time.DateTime)) // 格式:2006-01-02 15:04:05
// int64 序列化
case int64:
return Int64ToBytes(val, EndianOrder) // 直接转换为8字节
3. 时间戳 vs time.Time 作为主键
3.1 性能对比
| 特性 | 时间戳(int64) | time.Time |
|---|---|---|
| 存储大小 | 8字节 | 约19字节 |
| 比较速度 | 极快(直接数值比较) | 较慢(字符串字典序比较) |
| 范围查询 | 高效(基于数值范围) | 低效(基于字符串范围) |
| 精度控制 | 灵活(秒、毫秒、纳秒) | 固定(sfsDb中为秒级) |
| 写入性能 | 高 | 中 |
| 查询性能 | 高 | 中 |
3.2 推荐选择
推荐使用 int64 时间戳作为主键,原因如下:
- 更高的存储效率
- 更快的查询性能
- 更好的扩展性
- 支持更高精度需求
- 更适合时序数据特征
4. 时序数据最佳实践
4.1 表设计
// 创建时序数据表
table, _ := TableNew("iot_sensor_data")
// 设置字段,使用时间戳作为主键
fields := map[string]any{
"timestamp": int64(0), // 时间戳主键(毫秒级)
"sensor_id": "", // 传感器ID
"temperature": 0.0, // 温度
"humidity": 0.0, // 湿度
"pressure": 0.0, // 压力
}
table.SetFields(fields)
// 创建时间戳主键索引
table.CreatePrimaryKey("timestamp")
// 可选:为传感器ID创建索引,优化按传感器查询
table.CreateSimpleIndex("idx_sensor_id", "sensor_id")
4.2 数据写入
// 生成毫秒级时间戳
timestamp := time.Now().UnixMilli()
// 准备传感器数据
sensorData := map[string]any{
"timestamp": timestamp,
"sensor_id": "sensor_001",
"temperature": 25.5,
"humidity": 60.2,
"pressure": 1013.25,
}
// 插入数据
_, err := table.Insert(&sensorData)
if err != nil {
log.Printf("插入数据失败: %v", err)
}
4.3 数据查询
4.3.1 范围查询
// 查询最近1小时的数据
oneHourAgo := time.Now().Add(-time.Hour).UnixMilli()
query := map[string]any{"timestamp": oneHourAgo}
// 使用 GreaterThan 操作符查询
iter := table.Search(&query, util.GreaterThan)
defer iter.Release()
// 获取结果
records := iter.GetRecords(true)
log.Printf("最近1小时数据量: %d", len(records))
4.3.2 多条件查询
// 查询特定传感器最近1小时的数据
oneHourAgo := time.Now().Add(-time.Hour).UnixMilli()
query := map[string]any{
"timestamp": oneHourAgo,
"sensor_id": "sensor_001",
}
// 范围+等值查询
iter := table.Search(&query, util.GreaterThan)
defer iter.Release()
4.3.3 时间窗口查询
// 查询昨天的数据
yesterday := time.Now().AddDate(0, 0, -1)
startOfDay := time.Date(yesterday.Year(), yesterday.Month(), yesterday.Day(), 0, 0, 0, 0, yesterday.Location())
endOfDay := startOfDay.Add(24 * time.Hour)
startTimestamp := startOfDay.UnixMilli()
endTimestamp := endOfDay.UnixMilli()
// 构造查询条件
startQuery := map[string]any{"timestamp": startTimestamp}
endQuery := map[string]any{"timestamp": endTimestamp}
// 先查询大于等于开始时间的数据,然后在应用层过滤
iter := table.Search(&startQuery, util.GreaterThan)
defer iter.Release()
// 在应用层过滤小于结束时间的数据
var filteredRecords []map[string]any
for _, record := range iter.GetRecords(true) {
if ts, ok := record["timestamp"].(int64); ok && ts < endTimestamp {
filteredRecords = append(filteredRecords, record)
}
}
log.Printf("昨天数据量: %d", len(filteredRecords))
4.4 数据更新与删除
// 更新指定时间戳的数据
updateData := map[string]any{
"timestamp": targetTimestamp, // 用于定位记录
"temperature": 26.0, // 更新温度值
"humidity": 58.5, // 更新湿度值
}
err := table.Update(&updateData)
if err != nil {
log.Printf("更新数据失败: %v", err)
}
// 删除指定时间戳的数据
deleteData := map[string]any{"timestamp": targetTimestamp}
err := table.Delete(&deleteData)
if err != nil {
log.Printf("删除数据失败: %v", err)
}
4. 时序数据优化建议
4.1 存储优化
-
使用适当的时间精度:
- 秒级:
time.Now().Unix()(适合低频数据) - 毫秒级:
time.Now().UnixMilli()(适合大多数IoT场景) - 微秒级:
time.Now().UnixMicro()(适合高精度传感器)
- 秒级:
-
数据压缩考虑:
- 合理设计表结构,避免冗余字段
- 考虑定期归档或删除旧数据
- 使用数据类型最小化存储(如使用
float32而非float64)
4.2 查询优化
-
创建合适的索引:
- 为主时间字段创建主键索引
- 为常用查询字段创建辅助索引
- 避免创建过多索引,影响写入性能
-
优化查询范围:
- 尽量缩小查询时间范围
- 使用
GetRecords(true)只获取需要的字段 - 考虑使用分页查询处理大量数据
-
批量操作:
- 批量插入数据,减少磁盘IO
- 批量更新或删除,提高操作效率
5. 时序数据应用场景
5.1 IoT 设备数据
- 场景:传感器实时采集数据(温度、湿度、压力等)
- 特点:高写入频率,低查询频率
- 最佳实践:使用毫秒级时间戳作为主键,定期聚合数据
5.2 系统监控数据
- 场景:服务器CPU、内存、磁盘使用率监控
- 特点:中等写入频率,高查询频率
- 最佳实践:使用秒级时间戳,创建多级索引
5.3 金融交易数据
- 场景:股票价格、交易量记录
- 特点:极高写入频率,高精度要求
- 最佳实践:使用微秒级时间戳,优化查询性能
5.4 日志数据
- 场景:应用日志、访问日志
- 特点:高写入频率,复杂查询需求
- 最佳实践:使用秒级时间戳,结合全文索引
6. 时序数据示例项目
6.1 IoT 传感器数据采集
package main
import (
"fmt"
"log"
"time"
"github.com/liaoran123/sfsDb/engine"
"github.com/liaoran123/sfsDb/storage"
"github.com/liaoran123/sfsDb/util"
)
func main() {
// 初始化数据库
if _, err := storage.OpenDefaultDb("./iot_data"); err != nil {
log.Fatalf("初始化数据库失败: %v", err)
}
defer storage.CloseDb()
// 创建传感器数据表
table, err := engine.TableNew("sensor_data")
if err != nil {
log.Fatalf("创建表失败: %v", err)
}
// 设置字段
fields := map[string]any{
"timestamp": int64(0), // 时间戳主键
"sensor_id": "", // 传感器ID
"temperature": 0.0, // 温度
"humidity": 0.0, // 湿度
"pressure": 0.0, // 压力
}
if err := table.SetFields(fields); err != nil {
log.Fatalf("设置字段失败: %v", err)
}
// 创建主键索引
if err := table.CreatePrimaryKey("timestamp"); err != nil {
log.Fatalf("创建索引失败: %v", err)
}
// 模拟100条传感器数据
for i := 0; i < 100; i++ {
timestamp := time.Now().Add(time.Duration(i) * time.Second).UnixMilli()
data := map[string]any{
"timestamp": timestamp,
"sensor_id": fmt.Sprintf("sensor_%03d", i%10),
"temperature": 20.0 + float64(i%10),
"humidity": 50.0 + float64(i%20),
"pressure": 1000.0 + float64(i%50),
}
if _, err := table.Insert(&data); err != nil {
log.Printf("插入数据失败: %v", err)
}
}
// 查询最近30秒的数据
thirtySecondsAgo := time.Now().Add(-30 * time.Second).UnixMilli()
query := map[string]any{"timestamp": thirtySecondsAgo}
iter := table.Search(&query, util.GreaterThan)
defer iter.Release()
records := iter.GetRecords(true)
log.Printf("最近30秒数据量: %d", len(records))
for _, record := range records {
log.Printf("时间: %d, 传感器: %s, 温度: %.1f",
record["timestamp"], record["sensor_id"], record["temperature"])
}
}
7. 总结
sfsDb 完全支持时序数据处理,通过以下最佳实践可以获得更好的性能:
- 使用
int64时间戳作为主键,提高存储和查询效率 - 选择合适的时间精度,根据场景需求平衡精度和性能
- 创建合理的索引,优化查询性能
- 优化查询范围,减少返回数据量
- 使用批量操作,提高写入效率
- 定期维护数据,归档或删除旧数据
通过遵循上述指南,可以充分利用 sfsDb 的轻量级优势,为各种时序数据场景提供高效的存储和查询解决方案。
8. 未来优化方向
sfsDb 在时序数据处理方面还有以下优化空间:
- 专门的时序数据压缩算法:针对时序数据的特点优化存储
- 自动数据过期机制:支持配置数据保留策略
- 时序数据聚合功能:内置常用聚合函数(如平均值、最大值、最小值)
- 时间窗口查询优化:原生支持时间窗口查询
- 更细粒度的时间精度支持:优化纳秒级时间戳处理
- 时序数据可视化工具:集成简单的数据可视化功能
这些优化将使 sfsDb 在时序数据处理方面更具竞争力,能够更好地满足各种时序数据场景的需求。