1. Apache IoTDB
目前工业物联网领域对应的主流开源产品是 Apache IoTDB (Internet of Things Database),它是一个专为物联网时序数据设计的高性能集成数据管理引擎,能够高效地存储、管理和查询大规模时间序列数据。
如果你面对的是其他相似名称(如工业领域的专用版本或某个企业的内部命名),核心的连接与操作思路大体类似。下文将以 Apache IoTDB 为例,提供一套完整的 .NET 连接与 CRUD 解决方案。
2. 环境准备与依赖安装
2.1 基础环境要求
- .NET SDK:≥ 5.0,或 .NET Framework 4.x
- Apache IoTDB 服务端:建议 0.13.x 或 0.14.x 版本(需提前部署并启动)
- Thrift 版本:服务端与客户端使用的 Thrift 版本需匹配(IoTDB 基于 Thrift RPC 框架通信)
2.2 NuGet 包安装
Apache IoTDB 为 C# 用户提供了两种 NuGet 包,可通过 .NET CLI 或 Visual Studio NuGet 管理器安装:
| 包名 | 说明 | 安装命令 |
|---|---|---|
Apache.IoTDB |
C# 原生客户端(推荐) | dotnet add package Apache.IoTDB |
Apache.IoTDB.Data |
ADO.NET 标准提供程序 | dotnet add package Apache.IoTDB.Data |
原生客户端提供更丰富的时序操作接口(如对齐序列、Tablet 批量写入等),ADO.NET 版本则更适合熟悉传统数据库编程模型的开发者。
2.3 其他依赖
- NLog:建议安装 4.7.9 或更高版本,用于客户端内部日志记录
- Thrift:≥ 0.14.1(通常是传递依赖,无需手动指定版本)
注意:各组件版本不匹配极易引发难以排查的运行时异常,建议严格按照上述标准版本安装。
3. 创建客户端连接与参数说明
3.1 原生接口建立会话
csharp
using Apache.IoTDB;
// 创建会话连接
var sessionPool = SessionPool.Builder()
.Host("192.168.1.100") // IoTDB 服务端 IP
.Port(6667) // 默认 Thrift RPC 端口
.User("root") // 初始超级用户
.Password("root") // 初始密码
.PoolSize(5) // 会话池大小
.Build();
// 获取一个可用会话
var session = sessionPool.GetSession();
关键参数说明:
| 方法 | 参数 | 默认值 | 含义 |
|---|---|---|---|
Host(string) |
IPv4 地址 | - | IoTDB 所在服务器地址 |
Port(int) |
整数 | 6667 | 服务端 RPC 监听端口 |
User(string) |
用户名 | "root" |
登录用户名 |
Password(string) |
密码 | "root" |
登录密码 |
PoolSize(int) |
整数 | 2 | 连接池并发会话数上限 |
FetchSize(int) |
整数 | 5000 | 每次从服务端拉取的行数 |
TimeOut(int) |
整数(毫秒) | 60000 | 单次操作超时时间 |
3.2 ADO.NET 方式建立连接
csharp
using Apache.IoTDB.Data;
var connectionString = "server=192.168.1.100;port=6667;username=root;password=root";
using var connection = new IoTDBConnection(connectionString);
connection.Open();
// 后续使用 IoTDBCommand 执行 SQL 操作
这种方式通过 IoTDBConnection、IoTDBCommand 等标准 ADO.NET 类进行数据库操作,与操作 SqlServer、MySQL 等传统关系型数据库的写法高度一致。
4. 数据模型基础
在具体操作前,先理解 IoTDB 的数据组织方式:
root (根节点)
└── factory (存储组)
└── workshop (设备)
└── temperature (物理量/测点) → 时间序列值
└── humidity (物理量/测点) → 时间序列值
- 存储组 (Storage Group):数据隔离与过期策略的基本单位
- 设备 (Device):对应工厂的一台具体物理设备或一个逻辑采集点
- 物理量 (Measurement):设备下某个具体的测点(如温度、压力)
- 时间戳 (Timestamp):数据点的采集时间(长整型,单位毫秒)
一个具体的时序数据点的完整路径为:root.factory.workshop.temperature。
5. 核心 CRUD 操作详解
温馨提示:时序场景中"更新"和"删除"通常是低频操作,核心操作集中在"写入"与"查询"。
5.1 创建 (Create) --- 建立存储组与序列
5.1.1 创建存储组
csharp
// 设置一个或多个存储组
session.SetStorageGroup("root.factory");
5.1.2 创建时间序列(定义物理量)
csharp
// 为指定测点创建时间序列,指定数据类型(FLOAT)和编码方式(GORILLA 适合浮点压缩)
session.CreateTimeseries(
"root.factory.workshop.temperature",
TSDataType.FLOAT,
TSEncoding.GORILLA,
CompressionType.SNAPPY
);
核心参数说明:
| 参数 | 含义 | 常用值 |
|---|---|---|
path |
序列全路径 | 例:"root.factory.workshop.temperature" |
dataType |
物理数据类型 | BOOLEAN, INT32, INT64, FLOAT, DOUBLE, TEXT |
encoding |
编码压缩方式 | 浮点推荐 GORILLA,整数推荐 TS_2DIFF,文本推荐 PLAIN |
compressionType |
通用压缩算法 | SNAPPY(速度快),GZIP(压缩率高),LZ4,UNCOMPRESSED |
5.2 写入数据 (Insert & Batch Insert)
5.2.1 单点写入
csharp
// 向一个时间序列写入单个数据点(时间戳单位为毫秒)
session.InsertRecord(
"root.factory.workshop.temperature",
1680000000000L, // 时间戳:2023-03-28 18:13:20.000
25.5f, // 当前温度值
false // false 表示不按对齐时序插入
);
5.2.2 批量写入(高效推荐方式)
csharp
var deviceIds = new List<string> {
"root.factory.workshop.temperature",
"root.factory.workshop.humidity"
};
var timestamps = new List<long> {
1680000001000L,
1680000002000L,
1680000003000L,
1680000004000L,
1680000005000L
};
var valuesList = new List<List<object>> {
new List<object> { 25.5f, 26.1f, 26.3f, 26.0f, 25.8f },
new List<object> { 60.1f, 61.0f, 61.5f, 60.8f, 60.3f }
};
var typesList = new List<List<TSDataType>> {
Enumerable.Repeat(TSDataType.FLOAT, 5).ToList(),
Enumerable.Repeat(TSDataType.FLOAT, 5).ToList()
};
// 批量插入多条记录(大幅提升写入吞吐量)
session.InsertRecords(deviceIds, timestamps, valuesList, typesList);
5.2.3 使用 Tablet 工具(最高性能写入)
Tablet 是 IoTDB 提供的一种高效数据载体,适合设备下多个测点具有相同时间戳序列的场景,可实现每秒数千万数据点的写入性能:
csharp
var schema = new Dictionary<string, TSDataType> {
{ "temperature", TSDataType.FLOAT },
{ "humidity", TSDataType.FLOAT },
{ "status", TSDataType.BOOLEAN }
};
var tablet = new Tablet("root.factory.workshop", schema, 1000);
// 向 Tablet 中添加数据行
for (int i = 0; i < 1000; i++)
{
tablet.AddTimestamp(i, 1680000000000L + i * 1000);
tablet.AddValue("temperature", i, 25.0f + i * 0.1f);
tablet.AddValue("humidity", i, 60.0f + i * 0.05f);
tablet.AddValue("status", i, i % 2 == 0);
}
// 一次性提交 Tablet 内所有数据
session.InsertTablet(tablet);
5.3 查询 (Read) --- 三种常用模式
5.3.1 原始数据查询 (Raw Data Query)
csharp
// 按时间范围查询原始数据,SessionDataSet 实现了迭代器模式
var dataSet = session.ExecuteRawDataQuery(
paths: new List<string> { "root.factory.workshop.temperature" },
startTime: 1680000000000L,
endTime: 1680003600000L
);
while (dataSet.HasNext())
{
var record = dataSet.Next();
long timestamp = record.GetTimestamp();
float value = record.GetFloat(1); // 索引 0 为时间戳,1 为值
Console.WriteLine($"[{timestamp}] temperature = {value}");
}
dataSet.Close();
ExecuteRawDataQuery 方法签名说明:
csharp
SessionDataSet ExecuteRawDataQuery(
List<string> paths, // 要查询的时间序列路径列表
long startTime, // 开始时间戳
long endTime // 结束时间戳
)
5.3.2 聚合查询 (Aggregation Query)
csharp
// 统计指定时间段内温度的平均值
var aggResult = session.ExecuteAggregationQuery(
paths: new List<string> { "root.factory.workshop.temperature" },
aggregations: new List<Aggregation> { Aggregation.AVG },
startTime: 1680000000000L,
endTime: 1680003600000L
);
while (aggResult.HasNext())
{
var row = aggResult.Next();
Console.WriteLine($"Avg Temp: {row.GetFloat(0)}");
}
aggResult.Close();
常用聚合操作符对照:
| 枚举值 | 含义 | 枚举值 | 含义 |
|---|---|---|---|
COUNT |
计数 | SUM |
求和 |
AVG |
平均值 | MIN_VALUE |
最小值 |
MAX_VALUE |
最大值 | FIRST_VALUE |
最早值 |
LAST_VALUE |
最新值 | EXTREME |
极值 |
5.3.3 降采样查询 (Downsampling / Group By Time)
csharp
// 每 60 秒计算一次温度平均值,适合趋势展示
var dsResult = session.ExecuteRawDataQuery(
paths: new List<string> { "root.factory.workshop.temperature" },
startTime: 1680000000000L,
endTime: 1680003600000L
);
// 如需更精细的分组控制,可结合 IoTDB SQL 中的 `GROUP BY` 语法
建议 :对于复杂降采样场景,推荐使用原生 SQL 模式,直接执行
SELECT avg(temperature) FROM root.factory.workshop GROUP BY ([start, end), 60s)。
5.3.4 原生 SQL 查询 (兼容模式)
csharp
// 直接执行 IoTDB 标准 SQL(与命令行语法完全一致)
var sqlResult = session.ExecuteQueryStatement(
"SELECT temperature, humidity FROM root.factory.workshop " +
"WHERE time >= 1680000000000 AND time <= 1680003600000 " +
"ORDER BY time DESC LIMIT 100"
);
while (sqlResult.HasNext())
{
var row = sqlResult.Next();
Console.WriteLine($"{row.GetTimestamp()}: {row.GetFloat(1)}, {row.GetFloat(2)}");
}
sqlResult.Close();
5.4 更新 (Update)
时序数据库的更新严格意义上是指**覆盖(Upsert)**操作。
- 若时间戳已存在,IoTDB 会保留最新值(后插入的值覆盖前值)
- 若时间戳未被占用,则直接插入新数据点
因此更新与插入使用相同代码,无需额外操作:
csharp
// 与 5.2.1 完全相同的调用,若时间戳 1680000000000L 已有值则覆盖
session.InsertRecord(
"root.factory.workshop.temperature",
1680000000000L,
28.0f,
false
);
5.5 删除 (Delete)
5.5.1 删除单个数据点
csharp
// 删除指定序列在指定时间戳的数据点
session.DeleteData(
"root.factory.workshop.temperature",
1680000000000L
);
注意:此操作需谨慎执行,IoTDB 会对删除数据生成墓碑标记,物理空间回收存在一定延迟。
5.5.2 删除整个时间序列
csharp
// 删除从序列到其下所有子节点的全部数据与元数据
session.DeleteTimeseries("root.factory.workshop.temperature");
5.5.3 删除存储组(级联删除)
csharp
// 删除整个存储组及其下全部序列与数据------不可逆操作
session.DeleteStorageGroup("root.factory");
核心 CRUD 操作速查表
| 操作 | 核心方法 | 说明 |
|---|---|---|
| 创建存储组 | SetStorageGroup("root.xxx") |
必须先于序列创建 |
| 创建序列 | CreateTimeseries(path, type, enc, comp) |
定义测点的数据类型与编码 |
| 单点写入 | InsertRecord(path, ts, value, false) |
简单场景使用 |
| 批量写入 | InsertRecords(devices, timestamps, values, types) |
推荐日常使用 |
| 高效写入 | InsertTablet(tablet) |
极大量数据场景首选 |
| 原始查询 | ExecuteRawDataQuery(paths, start, end) |
按时间范围检索 |
| 聚合查询 | ExecuteAggregationQuery(paths, aggs, start, end) |
统计计算 |
| SQL 查询 | ExecuteQueryStatement(sql) |
与命令行语法一致 |
| 更新 | InsertRecord(...) (覆盖) |
与写入共用相同方法 |
| 删除数据点 | DeleteData(path, ts) |
单个点精准删除 |
| 删除序列 | DeleteTimeseries(path) |
删除整个序列及历史数据 |
| 删除存储组 | DeleteStorageGroup("root.xxx") |
级联删除,不可逆 |
6. 注意事项与最佳实践
6.1 写入性能优化
- 优先批量写入 :单次批量万级以上数据点时,优先使用
InsertRecords;十万级以上数据点时,推荐使用InsertTablet,性能差距可达数量级。 - 启用会话池 :多线程并发写入时,通过
SessionPool共享物理连接,复用 RPC 链路,减少频繁建连开销。 - 合理设置
FetchSize:查询大结果集时,单次拉取行数不宜过小(建议 ≥ 1000),以降低网络往返次数。
6.2 资源管理
- 使用
using语句或手动Close():及时释放SessionDataSet等非托管资源。 - 应用退出时关闭连接池 :程序退出前调用
sessionPool.Close(),避免 Thrift 连接残余导致服务端资源泄露。
6.3 时间戳规范
- IoTDB 内部统一使用长整型毫秒时间戳(Unix 毫秒)。
- 可通过
DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()将当前时间转换为毫秒级时间戳。
6.4 查询性能优化
- 限制时间窗口 :查询时尽量指定精确的
startTime和endTime,避免全表扫描。 - 善用降采样 :展示历史趋势时,优先在服务端使用聚合 +
GROUP BY完成降采样,再返回少量汇总数据给客户端。
对于工业物联网场景而言,时序数据库的核心优势在于高并发写入性能 和高效的时序范围扫描能力,合理利用批量写入与降采样查询,可显著降低系统资源消耗并提升数据处理的实时性。