客户端Client/Session编写 Java 代码把传感器的数据打包发给服务器也就是写第一步和第二步的打数据逻辑。引擎内核Engine/TsFile研究怎么优化 Flush 机制或者怎么让底层 TsFile 压缩得更小。查询引擎Query/Plan研究北京总部分析师敲入的那句 SQL是怎么被解析并去成百上千个 TsFile 里捞数据的。import org.apache.iotdb.rpc.IoTDBConnectionException; import org.apache.iotdb.rpc.StatementExecutionException; import org.apache.iotdb.session.Session; import org.apache.iotdb.tsfile.enums.TSDataType; import org.apache.iotdb.tsfile.write.record.Tablet; import org.apache.iotdb.tsfile.write.schema.MeasurementSchema; import java.util.ArrayList; import java.util.List; public class WindFarmDataWriter { public static void main(String[] args) throws IoTDBConnectionException, StatementExecutionException { // 1. 初始化并打开 Session 连接 (连接本地的 IoTDB 服务) Session session new Session(127.0.0.1, 6667, root, root); session.open(); System.out.println(成功连接到 IoTDB!); // 2. 设置数据库 (Database旧版本叫 Storage Group 存储组) // root.windfarm 代表我们的风电场业务 String database root.windfarm; try { session.setStorageGroup(database); } catch (StatementExecutionException e) { // 如果已经存在会报错这里忽略已存在的报错 } // 3. 定义设备路径和传感器 (Measurements) String deviceId root.windfarm.turbine_1; // 1号风机 ListMeasurementSchema schemaList new ArrayList(); // 定义两个物理量风速(FLOAT) 和 温度(FLOAT) schemaList.add(new MeasurementSchema(speed, TSDataType.FLOAT)); schemaList.add(new MeasurementSchema(temperature, TSDataType.FLOAT)); // 4. 创建 Tablet (数据块) - 这是实现“极高写入吞吐”的秘密武器 // Tablet 相当于内存中的一张表我们先在内存里攒够一批数据然后一次性发给服务器 int maxRowNumber 100; // 假设我们一次攒 100 行数据 Tablet tablet new Tablet(deviceId, schemaList, maxRowNumber); // 5. 模拟采集数据并填充到 Tablet 中 long timestamp System.currentTimeMillis(); for (int row 0; row 100; row) { int rowIndex tablet.rowSize; // 获取当前写入的行号 // 写入共享的时间戳 (因为是对齐序列) tablet.addTimestamp(rowIndex, timestamp row * 1000L); // 每秒采集一次 // 写入风速 (假设在 15.0 上下浮动) tablet.addValue(schemaList.get(0).getMeasurementId(), rowIndex, 15.0f (float)Math.random()); // 写入温度 (假设在 65.0 上下浮动) tablet.addValue(schemaList.get(1).getMeasurementId(), rowIndex, 65.0f (float)Math.random()); } // 6. 执行批量插入 (由于是对齐设备调用 insertAlignedTablet) // 这一步底层会把 Tablet 序列化成字节流通过 RPC (Thrift协议) 发送给 IoTDB Server session.insertAlignedTablet(tablet); System.out.println(成功批量写入 100 条对齐的时序数据); // 7. 清空 Tablet 以便复用并关闭连接 tablet.reset(); session.close(); } }去 IoTDB 源码里看这几个地方追踪 RPC这段 Java 代码在客户端调用insertAlignedTablet后数据去了哪里你可以去源码的server模块中搜索ClientRPCServiceProcessor.java或者类似处理 Thrift 请求的类看看服务端是怎么接收这个Tablet的。追踪写入内存服务端收到数据后会把它扔进一个叫MemTable的组件里。你可以看看org.apache.iotdb.db.engine.memtable包下的代码。追踪 TsFile 生成刚才的Tablet里用到了TSDataType.FLOAT。你可以去tsfile模块里看看对于FLOAT类型默认用了什么编码通常是Gorilla它是怎么把这 100 个温度值压缩成几十个字节的。整个过程可以分为四个关键步骤你可以把它想象成**“前台接待 - 记备忘录 - 写黑板 - 存入档案柜”**的过程第一步前台接待 —— RPC 接收与解析当你的 Java 客户端执行发送时底层其实是通过网络协议Apache Thrift把这块Tablet变成了一串二进制字节流发给了服务器。接收服务端的网络通信模块RPC 模块接收到这串字节流。解析服务器把它反序列化还原成服务端能认识的对象通常叫InsertTabletNode插入计划节点。找位置服务器根据你传过来的设备名root.windfarm.turbine_1在内存里查找“这个设备的数据应该由哪个存储模块来负责”第二步记备忘录 —— 写入预写日志 (WAL, Write-Ahead Log)这是所有高性能数据库保证“数据不丢”的保命手段。如果数据直接写进内存万一这个时候机房突然断电这 100 条风机数据就永远丢失了。所以在真正处理数据之前IoTDB 会先以最快的速度把这批数据追加Append写到一个本地的日志文件里。这个日志文件就叫 WAL。追加写是非常快的磁盘顺序写。只要 WAL 记成功了服务器就可以放心地对客户端说“我收到你的数据了”哪怕这时候数据还在内存里没真正落盘。万一宕机重启时 IoTDB 会读取 WAL 把内存里的数据恢复出来。第三步写黑板 —— 写入内存表 (MemTable)写完 WAL 后这批数据就被正式放入了服务器的内存中。这个内存结构叫MemTable内存表。为什么不直接写进 TsFile因为你一秒钟可能要发成千上万次请求如果每次都去打开文件、修改文件磁盘会被卡死。所以要先在内存里“攒着”。在 MemTable 里数据是非常结构化的。对于你的turbine_1设备它会在内存里建一个列表把刚刚发过来的时间戳、风速、温度整整齐齐地按列排好。这个时候如果北京总部的分析师发来一条 SQL 查询最新的风机温度IoTDB 会直接从 MemTable 里把热乎的数据取出来返回给他。第四步存入档案柜 —— 刷盘 (Flush) 生成 TsFile风机源源不断地发数据MemTable 里的数据越来越多。当内存表达到一定大小比如 128MB或者按时间触发时IoTDB 就会执行一个极其重要的操作Flush刷盘。这也是 TsFile 真正诞生的时刻这部分代码的核心是TsFileWriter。编码 (Encoding)服务端看到你要存的是FLOAT类型的温度它开始运行Gorilla 算法。原本65.1,65.2,65.1占据很多字节经过算法计算它们之间的差值变成了一小串紧凑的二进制比特。压缩 (Compression)接着调用Snappy 或 LZ4等通用压缩算法把这串比特再挤压一遍。写文件最后把压缩好的数据块Chunk按特定的格式写入到磁盘上的一个.tsfile文件中。清空黑板文件写完后之前那个满的 MemTable 被清空对应的 WAL 日志也可以删除了。CtrlShiftF全局搜索搜这几个关键词看第二步日志搜WALNode或WALWriter。看第三步内存搜IMemTable接口它有一个核心实现叫TsFileProcessor负责管理内存和刷盘的协调者。看第四步写文件搜MemTableFlushTask这是把内存变成 TsFile 的后台任务以及TsFileWriter。TsFile是这样井然有序地“套娃”的ChunkGroup (设备块)代表一个设备如 1号风机。它把这个设备的所有传感器数据打包在一起。Chunk (测点块)代表一个传感器如 温度。它里面全是这个传感器的历史数据。Page (数据页)这是 TsFile 最小的读取单元。一个 Chunk 由很多个 Page 组成。这是极其重要的概念PageHeader (页头统计量)每个 Page 开头都有一小段“元数据”里面记录了这一页里数据的最大值 (Max)、最小值 (Min)、总和 (Sum)、第一条和最后一条的时间戳。索引区 (Index)在文件的末尾TsFile 会建一个目录树索引记录“1号风机的温度在文件的第几千到第几万个字节”。【文件头】 (Magic String: TsFile, Version: 000003) - 字典的封面▼ 【ChunkGroup】(设备块) - 归属root.windfarm.turbine_1(这里面打包了 1 号风机这一上午所有的传感器数据)▼ 【Time Chunk】(时间戳块) - 因为是对齐的时间戳单独拎出来存一次▼ Page 1 (数据页 1早上 08:00 - 10:00)- PageHeader (页头): 包含 08:00 到 10:00 的时间范围等统计信息- 压缩数据: [T1, T2, T3 ... T7200] (经过特殊编码的几千个时间戳)▼ Page 2 (数据页 2早上 10:00 - 12:00)- PageHeader: ...- 压缩数据: [T7201 ... T14400]▼ 【Value Chunk】(数值块) - 归属风速 (speed)▼ Page 1 (对应时间段 08:00 - 10:00)- PageHeader (页头关键!): 【Max: 16.5, Min: 14.2, Sum: 110502】- 压缩数据: [15.1, 15.2, 14.8 ...] (经过 Gorilla 编码和 Snappy 压缩的浮点数)▼ Page 2 (对应时间段 10:00 - 12:00)- PageHeader: 【Max: 18.1, Min: 15.0, Sum: 120800】- 压缩数据: [...]▼ 【Value Chunk】(数值块) - 归属温度 (temperature)▼ Page 1 (对应时间段 08:00 - 10:00)- PageHeader: 【Max: 68.5℃, Min: 64.2℃, Sum: ...】- 压缩数据: [65.1, 65.2, 65.0 ...]▼ Page 2 (对应时间段 10:00 - 12:00)- PageHeader: 【Max: 82.1℃(异常!), Min: 66.0℃, Sum: ...】- 压缩数据: [...]---------------------------------------------------------(如果还有 2 号风机的数据下面就会紧接着另一个 ChunkGroup)---------------------------------------------------------【TsFileMetadata 索引区】 - 字典的目录在文件最末尾- 记录turbine_1 的 speed 数据在文件的 第 1500 字节 到 第 8500 字节。- 记录turbine_1 的 temperature 数据在 第 8501 字节 到 第 16000 字节。【文件尾】 (Magic String: TsFile) - 字典的封底写链路客户端 Tablet - 网络 Thrift RPC - WAL 日志 - MemTable 内存 - Flush 成 TsFile。读链路SQL - 解析计划 - 定位文件/内存 - 利用页头统计量智能过滤 - 提取返回。简单使用iotdb数据库步骤1.cmd进入文件夹 输入 .\sbin\start-standalone.bat2.得到 服务端DataNode 和 ConfigNode3.重新开启一个命令窗输入.\sbin\start-cli.bat -h 127.0.0.1 -p 6667 -u root -pw root准备进入客户端第一步创建数据库在关系型数据库里这叫建 Database在 IoTDB 的树形模型里这其实是在root根节点下挂一个叫windfarm风电场的分支。CREATE DATABASE root.windfarm;你可以用这行命令确认一下它在不在SHOW DATABASES;第二步让风机转起来自动推断与写入数据你不需要提前建表也不需要提前定义字段类型假设现在 1 号风机turbine_1开机了传感器传来了当前的风速speed和温度temperature。INSERT INTO root.windfarm.turbine_1(time, speed, temperature) values(now(), 15.2, 65.1);必须要指定time时间戳这里我们用了自带的函数now()代表当前精确到毫秒的时间。数据库底层发现你传了15.2它会自动帮你把speed注册成浮点数类型。这在动辄几十万个传感器的工业界省去了巨大的人工建表工作量第三步查看监控数据时序查询SELECT * FROM root.windfarm.turbine_1;IoTDB 的魔法它在内存里专门维护了每个传感器的“Last Cache”最新值缓存。SELECT LAST_VALUE(speed), LAST_VALUE(temperature) FROM root.windfarm.turbine_1;PageHeader页头IoTDB 写入数据时已经在底层把最大值、最小值、总行数提前算好存起来了。SELECT COUNT(speed), MAX_VALUE(temperature), MIN_VALUE(temperature) FROM root.windfarm.turbine_1;假设你的风机每秒钟发一条数据一天就有 8 万多条。如果前端要把一整年的折线图画出来浏览器会直接崩溃。这时候就需要降采样比如把数据按“每 1 分钟”切成一个窗口算出一个平均值返回。SELECT AVG(speed) FROM root.windfarm.turbine_1 GROUP BY ([now() - 5m, now() 5m), 1m);插入数据package com.start1; import org.apache.iotdb.session.Session; import org.apache.tsfile.enums.TSDataType; import org.apache.tsfile.write.record.Tablet; import org.apache.tsfile.write.schema.MeasurementSchema; import java.util.ArrayList; import java.util.List; public class WindfarmSimulator { public static void main(String[] args) throws Exception { // 1. 建立连接 (这就是你之前在黑框框里敲的 -h 127.0.0.1 -p 6667 -u root -pw root) Session session new Session.Builder() .host(127.0.0.1) .port(6667) .username(root) .password(root) .build(); session.open(false); System.out.println(✅ 成功连接到本地 IoTDB!); // 2. 构造 Tablet (还记得吗Tablet 是把多行数据转成“列式数组”的超级结构) // 先定义这批数据有哪些传感器 (测点)以及它们的数据类型 ListMeasurementSchema schemaList new ArrayList(); schemaList.add(new MeasurementSchema(speed, TSDataType.FLOAT)); schemaList.add(new MeasurementSchema(temperature, TSDataType.FLOAT)); // 创建一个容量为 10000 的 Tablet目标设备是 1号风机 Tablet tablet new Tablet(root.windfarm.turbine_1, schemaList, 100); // 3. 疯狂灌入 10000 条模拟数据 System.out.println( 开始组装 100 条风机数据...); long timestamp System.currentTimeMillis(); // 获取当前时间的毫秒值 for (int row 0; row 100; row) { int rowIndex tablet.rowSize; // 塞入时间戳 (每条数据间隔 1 秒 1000 毫秒) tablet.addTimestamp(rowIndex, timestamp (row * 1000L)); // 塞入风速 (模拟 10.0 ~ 20.0 之间的浮点数) tablet.addValue(speed, rowIndex, (float) (10.0 Math.random() * 10)); // 塞入温度 (模拟 50.0 ~ 80.0 之间的浮点数) tablet.addValue(temperature, rowIndex, (float) (50.0 Math.random() * 30)); } // 4. 发送给服务端(瞬间完成) System.out.println( 数据组装完毕正在通过网络轰炸服务端...); session.insertTablet(tablet); // 5. 打完收工 session.close(); System.out.println( 10000 条数据写入完成且已断开连接); } }Java 程序造数机在内存里把 10000 条数据拼成了一个Tablet列式数据块。通过session.insertTablet()数据被打包顺着网络TCP/IP发送给了本地运行的 IoTDB 服务端进程DataNode。服务端接到数据后并没有立刻写进硬盘的 TsFile因为如果每来一条数据就写一次硬盘硬盘早就冒烟了。它会先把它放在服务端的内存里这个内存结构叫MemTable。当内存里的数据攒到一定大小比如 1GB时服务端会在后台默默触发一个Flush动作瞬间把这 1GB 的数据打包压缩真真正正地变成硬盘上的一个.tsfile文件。取出数据import org.apache.iotdb.rpc.IoTDBConnectionException; import org.apache.iotdb.rpc.StatementExecutionException; import org.apache.iotdb.session.Session; import org.apache.iotdb.session.SessionDataSet; import org.apache.tsfile.read.common.Field; import org.apache.tsfile.read.common.RowRecord; public class WindfarmQuery { public static void main(String[] args) throws IoTDBConnectionException, StatementExecutionException { // 1. 建立连接 (和写入时一模一样) Session session new Session.Builder() .host(127.0.0.1) .port(6667) .username(root) .password(root) .build(); session.open(false); // 2. 准备我们要执行的 SQL 语句 (查最新的 5 条记录) String sql SELECT * FROM root.windfarm.turbine_1 ORDER BY time DESC LIMIT 5; System.out.println( 正在执行查询: sql); // 3. 执行查询拿到结果集 (SessionDataSet) SessionDataSet dataSet session.executeQueryStatement(sql); // 4. 打印表头 (列名) System.out.println(表头: dataSet.getColumnNames()); // 5. 遍历结果集 (像剥洋葱一样一行一行拿出来) while (dataSet.hasNext()) { // 拿到当前这一行的数据 (RowRecord) RowRecord rowRecord dataSet.next(); // 第一列永远是时间戳 (Timestamp) long timestamp rowRecord.getTimestamp(); // 拿到这一行的其他传感器字段 (Fields) // field.getObjectValue() 可以把底层的数据原封不动取出来 StringBuilder rowString new StringBuilder(时间戳: timestamp | ); for (Field field : rowRecord.getFields()) { rowString.append(field.getObjectValue()).append( | ); } // 打印这一行 System.out.println(rowString.toString()); } // 6. 养成好习惯关闭结果集和连接 dataSet.closeOperationHandle(); session.close(); System.out.println(✅ 查询完毕连接已关闭); } }元数据模板你只需要定义一次模具一号风机长什么样二号也长这样然后把模具“挂”在root.windfarm风电场这个节点上。 之后只要你往这个节点下扔数据IoTDB 就会自动用模具“压”出一个风机来不仅免去了建表的麻烦还能节约极大的内存第 1 步打造“饼干模具”创建模板在黑框框里输入以下 SQL注意分号我们建一个名为t1的模板里面包含风速和温度CREATE DEVICE TEMPLATE t1 ( speed FLOAT encodingRLE, temperature FLOAT encodingRLE );第 2 步把模具挂在树枝上挂载模板为了不和刚才的数据打架我们新建一个叫windfarm_v2的新风电场。把t1模板挂在它上面SET DEVICE TEMPLATE t1 TO root.windfarm_v2;第 3 步见证魔法免建表直接插数据现在root.windfarm_v2下面其实没有任何风机。但是我们直接向一个根本不存在的turbine_888888 号风机插入一条数据看看会发生什么INSERT INTO root.windfarm_v2.turbine_888(timestamp, speed, temperature) VALUES(now(), 18.5, 62.1);再执行一下这句查询看看 888 号风机是不是“无中生有”地诞生了SELECT * FROM root.windfarm_v2.turbine_888;下采样既然你的 10000 条数据间隔是 1 秒现在我们就向 IoTDB 下达一个指令“请帮我把这 3 个小时的数据按『每 1 分钟』切成一个块然后算出这一分钟内的『平均风速』和『最高温度』”SELECT AVG(speed), MAX_VALUE(temperature) FROM root.windfarm.turbine_1 GROUP BY ([now() - 10m, now() 3h), 1m);AVG(speed)和MAX_VALUE(temperature)告诉数据库每个时间块里我要看什么指标。GROUP BY (...)这是时序数据库独有的“按时间窗口分组”语法。[now() - 10m, now() 3h)这是时间范围。因为你刚才跑 Java 代码是从“现在”开始往后推了 3 个小时所以我们框定当前时间往前 10 分钟往后 3 小时确保把你造的 10000 条数据全包进去。[代表包含)代表不包含极其严谨。1m这是步长Interval。意思是“每 1 分钟1 minute作为一个时间窗口进行聚合”。TTL第 1 步设置 TTL30天自动销毁我们让整个root.windfarm风电场下的所有风机数据只保留最近 30 天。超过 30 天的自动灰飞烟灭。 输入并回车SET TTL TO root.windfarm 30d;第 2 步查看你的 TTL 规则有没有生效查看当前系统里所有的过期规则SHOW ALL TTL;你会看到一个表格清晰地列出root.windfarm的 TTL 被设置成了 2592000000补充知识第 3 步老板反悔了怎么办如果有一天老板说“不行硬盘我加钱买数据永久保留” 你可以随时用这句命令撤销过期机制UNSET TTL TO root.windfarm;IoTDB 是怎么做到“删数据不卡顿”的你可能又会好奇既然不写定时任务那 IoTDB 是怎么把过期数据删掉的“延迟物理删除”。查询时的“障眼法”当你设置了 30 天 TTL 后如果有超过 30 天的老数据还在硬盘里当你执行SELECT查询时IoTDB 只要发现这个数据的时间戳太老了就直接装作没看见不返回给你。对用户来说它已经“被删了”。合并时的“顺手牵羊”IoTDB 底层会定期把一些零碎的小.tsfile文件合并成大文件这个过程叫 Compaction 合并。在合并的时候它一旦读到过期的数据就直接丢弃不写进新的大文件里。SessionPool在系统启动时建一个“连接池SessionPool”。里面常驻几十个连接谁要用就借走用完马上还回来。新建一个配置类IoTDBConfig.javapackage com.example.windfarm; import org.apache.iotdb.session.pool.SessionPool; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class IoTDBConfig { // Bean 会让 Spring Boot 启动时自动执行这个方法把连接池准备好 Bean(destroyMethod close) public SessionPool sessionPool() { System.out.println( 正在初始化 IoTDB 连接池...); // 参数IP, 端口, 账号, 密码, 最大连接数 (比如 10 个并发连接) return new SessionPool(127.0.0.1, 6667, root, root, 10); } } 第四步编写暴露给前端的 API 接口最后一步我们来写一个控制器Controller。当前端通过 HTTP 发起请求时这个类负责接收请求、去连接池拿数据、并转成 JSON。WindfarmController.javapackage com.start1.iotdbdemo.demos; import org.apache.iotdb.isession.pool.SessionDataSetWrapper; import org.apache.iotdb.rpc.IoTDBConnectionException; import org.apache.iotdb.rpc.StatementExecutionException; import org.apache.iotdb.session.pool.SessionPool; import org.apache.tsfile.read.common.Field; import org.apache.tsfile.read.common.RowRecord; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RestController; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; RestController // 这个注解告诉 Spring我要写 Web 接口了并且返回 JSON public class WindfarmController { Autowired private SessionPool sessionPool; // 把刚才配置好的连接池自动注入进来 // 定义接口路径比如访问 /api/turbine/1/data 就会触发这个方法 GetMapping(/api/turbine/{turbineId}/data) public ListMapString, Object getTurbineData(PathVariable String turbineId) { ListMapString, Object resultList new ArrayList(); // 动态拼装 SQL查最新 5 条降采样数据或者原始数据都可以 String sql SELECT * FROM root.windfarm.turbine_ turbineId ORDER BY time DESC LIMIT 5; // 声明一个包装类 (记得按 AltEnter 导入 org.apache.iotdb.session.pool.SessionDataSetWrapper) SessionDataSetWrapper wrapper null; try { // 1. 执行查询拿到 Wrapper wrapper sessionPool.executeQueryStatement(sql); // 2. 遍历结果集 while (wrapper.hasNext()) { // 这里的 next() 就能完美返回 RowRecord 了红色波浪线会立刻消失 RowRecord rowRecord wrapper.next(); MapString, Object rowMap new HashMap(); rowMap.put(timestamp, rowRecord.getTimestamp()); ListField fields rowRecord.getFields(); if (fields.size() 2) { rowMap.put(speed, fields.get(0).getObjectValue(fields.get(0).getDataType())); rowMap.put(temperature, fields.get(1).getObjectValue(fields.get(1).getDataType())); } resultList.add(rowMap); } } catch (IoTDBConnectionException | StatementExecutionException e) { e.printStackTrace(); } finally { // 3. 【极其重要】把连接还给连接池不写这句服务器半天就会内存溢出/连接耗尽 if (wrapper ! null) { sessionPool.closeResultSet(wrapper); } } return resultList; // 直接返回 List大功告成 } }基础创建CREATE TIMESERIES path DATATYPE。你需要记住常用的数据类型BOOLEAN, FLOAT, TEXT, INT32/64。对齐序列AlignedCREATE ALIGNED TIMESERIES。关键理解这是工业场景的杀手锏。对齐序列意味着一组传感器如 GPS 的经度和纬度共享同一个时间戳列这能极大地节省存储空间并提高查询速度。1.TIMESERIES形象理解它是一根“带刻度的长绳子”想象一个工厂里的温度计。它每秒钟产生一个数字。 如果你把这些数字按时间顺序串起来它就像一根无限延伸的绳子。这根绳子本身就是 TimeSeries。绳子的名字就是路径root.factory1.d1.speed。绳子上的刻度是时间戳。刻度上的值是具体的温度或速度。对比传统数据库MySQL像一个收纳盒。你得先打好格子定义列然后往里填。IoTDB像一束光纤。每根光纤就是一个传感器TimeSeries。你想加一个传感器就直接拉一根新光纤进来不需要动原来的盒子。2. 拆解CREATE TIMESERIES当你执行CREATE TIMESERIES root.factory1.d1.speed FLOAT你实际上是在告诉数据库你是谁你的名字叫root.factory1.d1.speed这是它的唯一身份证。你存什么你只存浮点数FLOAT。3. 什么是“对齐” (Aligned)普通序列非对齐 你有两个运动员速度传感器、温度传感器。他们各跑各的各拿一张计时表。速度传感器 10:00 记了一次温度传感器可能 10:01 才记。他们在数据库里是两根独立的绳子。对齐序列AlignedCREATE ALIGNED TIMESERIES root.factory1.device_gps(latitude FLOAT, longitude FLOAT)这就像是一个双人赛艇。经度和纬度必须坐在同一条船上。时间戳是共享的10:00 时这条船打个卡同时记录下经度和纬度。物理存储在一起在磁盘里这两个值是紧挨着存的。这就像把两根光纤绑在了一个套管里。数据流处理框架Pipe 机制