2 月 26 日面向 Apache IoTDB 树表双模型的功能特性、适用场景、建模选择和未来规划,田原同学通过直播进行了全面解答。以下为直播讲稿(下),干货满满,建议收藏⬇️⬇️
⚡️注意:
1. 功能演示部分请直接查看视频(1:21:17 开始)观看
2. 需要获取直播 PPT 请联系小助手欧欧(微信号 apache_iotdb)
目录
时序数据模型介绍
表模型功能介绍(本期文字)
创建表模型连接
数据库&表管理
数据写入
数据删除
数据更新
数据查询
表模型 RoadMap(本期文字)
02
表模型功能介绍
跟大家简单介绍了背景之后,会再花一些时间去介绍一下怎么创建表模型的连接,怎么进行表跟数据库的管理,还有增删改查这四个常见的操作,可能查询会讲得久一点。
(1)创建表模型连接
首先是创建表模型的连接,我们有 client,就是我们自己提供的命令行,还有 JDBC,还有 Java 的这些 SDK,那它们的指定方式分别是什么呢?就是刚刚也提到过的 -sql_dialect。我们是把 table 和 tree 作为两种视图、两种方言,去对同一个数据进行操作的,所以是通过 -sql_dialect table 的方式去指定连接用哪一个视图去看你的这份数据。这个参数是可选的,这是为了以前的树模型用户做兼容,因为以前树模型用户是不需要指定这个的,所以你不指定的话,我们还是默认为你是树模型用户,你只有指定了这个之后,你才是一个表模型用户。连进来之后,我们在 CLI 里面也可以执行 SET SQL_DIALECT=TABLE/TREE 进行模型切换,或者执行 SHOW CURRENT_SQL_DIALECT 去看当前的模型到底是树,或者当前的语法到底是树还是表。
JDBC 也是一样的,它通过这个问号后面的 sql_dialect=table 去决定是 table 还是 tree,你不指定的话默认还是tree。跟树模型不太一样的是我们在这个端口后面还多增加了一级数据库,对于表模型来讲,你可以直接连到这个默认数据库的空间,也可以执行 SET SQL_DIALECT=TABLE/TREE 去进行模型的切换。
Java 的 SDK 就比较简单了,我们重新为表模型抽象了一套接口,也就是我在用表试图进行写入和查询的时候,它是用 ITableSessionPool 和 ITableSession 这两个组件进行读写的,原来的 Session 或者 SessionPool 就服务于树模型了。通过这样做区分之后,它就不支持模型切换了,因为本身接口名字就已经决定了它一定是一个表模型。
(2)数据库&表管理
接下来一块是我们的数据库和表管理。刚刚说不同的模型,它在空间上面目前是分离的,所以一个数据库它要么属于树模型,要么属于表模型,取决于是在树模型连接下面创建的,还是表模型连接下面创建的。一个 DATABASE 下面可以有多张表,我们的时间分区也只能指定到 DATABASE 层级,就是我要按多少天或者默认一周进行分区。数据副本也只能指定到数据库层级,但是 TTL 是可以指定到表级别的。
对于这个逻辑视图而言,左下角的逻辑视图,用户看到的就是一个 IoTDB 下面可以有多个 DATABASE,每个 DATABASE 下面有多张表。但是到 IoTDB 的物理文件组织上面,data 目录下面可能先分了顺序/乱序,顺序下面分了不同的 DATABASE。DATABASE 到表之间还有两层,这两层就是我们做写入并发或者查询并发的时候,IoTDB 在内部帮我们做了 region,就是我们的一个并发单元。这个 region 下面会有刚刚说的指定时间分区,每个时间分区数据是会被放在一个文件夹下面的,一个时间分区下面会有多个 table 的数据。物理上面是这样组织的,会比逻辑视图要稍微复杂一点。
那我们怎么去创建一个表呢?创建语法其实也跟关系型数据库比较类似,唯一的区别就是我们多了一个列类别,我在这边用颜色标出来的,包括标签列、属性列、TIME 列和测点列,我们要在数据类型后面去指定到底这一列属于哪个类别。写出来的这个创建示例以 region、car_id 作为 TAG 列,model,就是属于哪一种车型作为属性列,速度还有温度作为 FIELD 列。
(3)数据写入
表模型的功能首先是数据写入。数据写入目前是支持 CLI 里面的 SQL、JDBC SQL、Java SDK、Python SDK 以及 REST API SDK。
大家可能会比较好奇我应该用哪一个?首先对于 CLI 里面的 SQL,它肯定不适合在生产里面去写数据,因为它只适合我们测试的时候去随时构造测试的数据集,并且随时去查看这个数据集对不对。JDBC SQL 是一个标准的接口协议,适合跟第三方的生态做集成,比如 DBeaver。目前不太推荐在生产里面直接用 JDBC 进行写入,因为它的写入性能相较于下面几个会低一些。Java SDK 就是我们原生提供的 Java 的 API,它的性能非常高,我们推荐生产里面直接用;Python 也是,这个取决于你的业务到底是用 Java 写的还是用 Python 写的,可以选择不同的 SDK 进行写入。REST API 的性能也比 JDBC SQL 要高,但是它比 Java 和 Python 这种原生的数据接口要稍微低一些,它适合一些业务开发语言 SDK 还没有提供的时候使用,比如 C、C++、Go 语言还没提供,可以用 REST API 进行写入。
表格中也对各类写入方式的便捷程度、性能以及是否支持自动建表和自动扩展列做了一个对比,大家也可以根据这个表格去决定自己到底要用什么。这里为什么有一些是不支持的,比如 CLI SQL 和 JDBC SQL 为什么不支持自动建表和自动扩展列?因为我们的写入 SQL 是向标准 SQL 做兼容的,我们不会在 insert 里面去加入指定这一列的功能,因为只有在 insert 的 SQL 里面去指定了我插的这一列是什么类别,它到底是 TAG 列还是属性列还是测点列,我才能知道该怎么样去建这个表,但是标准 SQL 里面是不会给你指定这些的,所以 CLI SQL 和 JDBC SQL 里面就不支持自动建表和自动扩展列了。但是原生的 Java SDK、Python SDK 和 REST API,我们在设计的时候就把这些 schema 信息给带上了,让用户去指定,这样我们就可以达到自动建表和自动扩展列的效果。
(4)数据删除
写入讲完了,下面是删除。IoTDB 的删除分成三种,一种是自动删除过期数据,一种是手动删除数据,一种是手动删除设备,这三者有什么区别呢?自动删除过期数据其实就是 TTL,这个适用场景是我们某一个 table 里面的数据只需要保留一周,那我就可以在建表的时候去指定这样一个 TTL,CREATE TABLE with (TTL=毫秒级别的精度)。你如果不指定的话,TTL 默认就是 Infinity 无穷大,就是永不过期的。
那创建表之后我能不能修改呢?比如我一开始创建的时候忘了指定了,我也不想 TTL 无穷大,那可以通过 ALTER TABLE 这种标准的语法去 set properties,让 TTL 等于这个值。同样的,这个精度也是毫秒,这是我们设置数据过期自动删除的策略。
第二种就是手动删除,场景是因为我错误地录入了某一些数据,我想要手动地删掉某一个设备某一段时间内的所有数据,可以用 DELETE FROM,然后在 where 里面指定设备的过滤条件和时间范围。当前有一些限制条件,大家可以看一下下面的这个表为例,如果想删掉 car1 的 time 在 3-4 之间的数据,我就可以写一个语句,时间戳 3/4 的数据就会被删掉了。
还有一种方式是我直接删除整个设备,包括这个设备的元数据,这个跟 DELETE FROM 有什么差别呢?DELETE FROM 不指定时间范围也可以把这个设备的所有数据都删掉,但 DELETE DEVICES 它删得更彻底,它会删掉一些元数据,也就是设备标识和设备的静态属性,DELETE DEVICES 之后,运行 SHOW DEVICES FROM TABLE,是看不到这个设备的。比如我想要删掉 car2 这个车的所有数据,删掉之后它连 SHOW DEVICES 都不展示了,但是我如果用 DELETE FROM 这个语法,它只会删除数据,也就是会把 car2 的所有数据删掉,但是 car2 这个车还是保留着的,所以我在 SHOW DEVICES 的时候仍然能看到 car2 这个车的这条记录。这是它们俩的一个区别,DELETE DEVICES 删除会更加彻底。
(5)数据更新
第三个功能就是数据更新,IoTDB 有一些列是可以直接更新的,有一些列是需要间接的更新的。
我们可以直接更新的列有哪些呢?刚刚说了列类别分为四种,TIME 列、TAG 列、FIELD 列、ATTRIBUTE 列,当中的 ATTRIBUTE 列(属性列)和 FIELD 列(测点列)是能够直接被更新的。
ATTRIBUTE 列刚刚说了是一个设备的静态属性,它可以直接用 UPDATE 语句更新,这个跟标准关系 SQL 定义的 UPDATE 是一样的。update 某一个 table 里面的某一个属性链,我要把 car1 的 su7 更新成 yu7,就可以直接用 UPDATE 语句进行更新。它的代价是很低的,因为我们之前说过,虽然看上去有多少行就应该改多少行,但是其实在物理存储里面只存了一行,所以更新代价是很低的。
FIELD 列,就是某一设备某一时刻采集的值也是可以直接更新的,可以通过写入重复的时间戳和需要更新的值去做。比如我要去更新 car1 的时间戳为 1 的速度改成了 15 千米/小时,原来是 10 千米/小时,那我就直接 INSERT INTO table,指定 car1、我想要修改的时间和我想要修改的列的值为 15.0,它自动就更新掉了。当然这个修改的代价跟你更新的行数有关,你更新一行就相当于往数据库里面多插了一行,这个数据会变成乱序数据,可能会对查询性能产生一定的影响,但这个影响会随着 IoTDB 的合并进行消除。
下面是两个需要间接更新的列,也就是 TAG 列和 TIME 列,它们的定义上是不可以修改的,但是我们可以通过组合 SELECT、INSERT、DELETE 的方式间接地更新。比如我想把 car1 改名为 car2,我们需要把原设备的所有数据查出来,然后再分批写入到我们想要改名的新数据当中。比如原始数据叫 car1,我先把它查出来,查出来的所有数据再 INSERT 分批写入要重命名的 car2,之后再用 delete devices 把所有设备、元数据、数据都删除,这样就实现了 car1 到 car2 的改名功能。
第二个就是 TIME 列,更新时间戳理论上是不行的,但同样可以通过这种组合的方式达到。我们把原来时间戳那一行查出来,比如要把 2 改成 3,那我把 2 先查出来,然后 INSERT 一条 3 的数据行,这是一个中间状态。INSERT 成功之后,我们再把 2 这一行 delete 掉,用 delete from 就能做到,这样也能实现间接的更新。
(6)数据查询
主要的时间留给查询,查询内容是比较丰富的,我也列了一些典型的查询场景。
原始数据查询
首先,第一个查询场景是原始数据查询。这个场景一般是我们想要去获取某一个设备下某段时间内的原始数据,可以在 where 子句里面去指定设备过滤条件及时间范围,比如 select from table1,然后指定 device_id=’d1’ ,time 在 1-2,这样我就可以得到原来这张表里面的这两行数据。
那如何获取多个设备某段时间内的原始数据?device_id IN 或者 device_id=’d1’ or device_id=’d2’,用 in 和用 or 去连接两个等号是没什么区别的,可以得到 d1 和 d2 在时间范围 1-2 里面的 4 行数据。
如果想获得一个设备某段时间内某个指标满足某个条件的数据,可以前面的过滤范围不动,再加上 s1 ≥2。
如果想获取某一个设备某段时间内所有数据的时间戳,也就是我只关心时间戳,有可能我这个数据特别大,数据层是一个 binary 对象,我只关心我在哪些时间点采集了这些数据,并不关心这个数据是什么,这个时候就可以在 select 里面仅写 time 列查到这些数据。这个在表模型其实没有办法做到,因为表模型里面默认是必须带有 time 的,并且这个 time 就不需要去写。
这块也跟大家说一下,表模型建表或写入的时候,列的类别分了四类,但是表模型的查询当中,逻辑上已经不区分列的类别了。查询里面 TAG/FIELD/ATTRIBUTE/TIME 都是一样的、普通的列,TIME 就是一个普通的数据类型为 TIMESTAMP 的列。当然,在物理执行上面这些列的筛选效率还是有区别的,但这个是内部执行的事,从用户的逻辑视图上面来看,这些列的地位都是等价的。就像关系型数据库里面你给某一些列建了主键索引,它只会在写入/更新/删除的时候有一些约束条件,在查询的时候这些主键索引的地位跟其他列是一样的。
聚合查询
接下来是聚合查询,比如想获得某张表某段时间内写入的总行数,我们就可以用 count(*),这也是一个标准 SQL 的查询方式,也只有 count 这个聚合函数可以用 (*),这一块是跟树模型不太一样的。我能查出来 1-2 这个时间范围这张表有 4 行数据,所以它 total_line 是 4。
我想获取每一个设备的最新一行数据,那就可以用 last_by 聚合函数,配合 where 子句里面指定设备的过滤条件。比如我要 device1 的最新数据,语句就写最后一个时间戳以及对应最后一行的 s1 last by time、s2 last by time,读出来的结果就能把最后一行给筛选出来。
如果想获得每一个设备的最新数据,该怎么做呢?我可以 from table1 不指定 where 的设备过滤条件,在语句中指定 group by device_id,按 device 分组之后得到每一个分组的最后一行。
聚合查询还有很多过滤方式,比如想去获得某一个指标某段时间内的平均值,并只想返回平均值 > 2 的设备,我们可以先按设备分组之后去求它的平均值,也就是 avg,得到 d1 的平均值是 2,d2 的平均值是 20。然后二次过滤用 having 语句去要平均大于 2 的设备这一列,过滤出来只会返回 d2,d1 就不会返回了。
降采样查询
讲完聚合查询,其实降采样一定程度上也是属于聚合查询的一种,只是它在时序场景里面比较特殊,是在 time 上面的分组。降采样是什么概念呢?可能我想获取某一个设备某个指标某段时间内的平均值,需要在时间上做一个降采样。跟在树模型里面的实现方式不一样,我们是做了一种跟标准关系 SQL 兼容的,通过 data_bin 函数加上标准的 group by clause 去达到这个概念。
data_bin 就是将一个时间戳列的所有数据全部规整到对应时间分段的起点并返回,有点绕口,其实大家看这个图能比较明白。比如我想按小时对数据进行规整,那 0:30 规整到小时应该是凌晨0点的时刻,1:30 应该规整到 1 点。规整完之后这些数据是一个标准的标量函数,它会得到的结果就是全部按小时把数据给对齐了。有了对齐的数据之后,我们就直接可以用 group by 去达到一个真正降采样的效果。我如果只需要某个范围某个设备的每小时的平均温度,那先把时间按照一小时做规整,根据过滤条件之后,我应该得到 d1 的这四行数据,得到规整的时间轴之后再执行 group by,再在每一个分组里面求平均温度值就可以了。
大家熟悉树模型的可能会看到,我们在 where 条件里面指定的是 ≥4 点、<12 点,但是中间其实有 9/10 点钟没有展示出来,原来用树模型的时候是能展示出来的。这是因为我们用这套标准语法去写的时候,降采样结果集里面本来就没有这些行,结果就不会展示,但后面我们有一个特殊的语法去帮助解决这件事情,把一些我们并没有数据的时间区间填充上。
在讲这个语法之前,我们可以看一下,刚刚是针对一个设备的,现在是每一个设备。其实就是在 group by 的时候,在 where 里面不指定设备的过滤条件,在 group by 里面加一个 device_id 的分组。这样我就把每一个 device 的数据分别按小时规整,再按设备以及对应的小时去进行分组,得到这个数据。
这里解释了一下 data_bin 和 avg 函数怎么样达到降采样的效果。如果我们需要获取所有设备某个数据时间范围内每小时的平均温度,也就是我不关注每一个设备内部了,我要关注整个表的概念,或者我关注更上一个层级的集团下所有设备总体的平均值,我在做的时候就不需要 group by device_id 了,规整之后有很多设备的 8 点钟数据就会被最后平均到一行上面了。
这个就是我刚刚跟大家提到的 gapfill。之前用 group by+data_bin 的时候是不会展示没有时间、没有数据的时间分区的,加了 gapfill 尾缀后就能够把这些空余的时间戳填上了。如果值有 null 的话,后面还会讲到空值填充功能,可以把这些 null 值也填上。
gapfill 的限制在官网也能看到,我在这边就不跟大家赘述了。唯一有一个特殊点是,如果整个数据都没有的话,gapfill 也不会帮你填充。
空值填充
刚刚提到表模型的空值填充,也就是通过 gapfill 之后有一些 null 值希望去填充,和树模型是一样的,可以用 FILL 语句去填充,也支持三种相同的方式。刚刚的数据部分可以用前值填充的方式,但有 3 行没有前面的值了,所以我指定 fill method previous 的时候没有办法填充前面这三行。
这里解释一下表模型 FILL 语句跟树模型的差异点。树模型是在 ORDER BY 子句之后执行 FILL 子句,而表模型是在 ORDER BY 子句之前执行的,所以用 ORDER BY+FILL 的时候,树模型和表模型的执行结果可能是有一些差异的。在线性填充的时候有一个辅助列的概念,我要去填充 y,那怎么得到中间没有值的 y 呢?我们要通过 x0 和 x1 去得到比例,然后用 y1 和 y0 去得到 y,所以 x 就成为带填充列的辅助列。
原来在树模型里面,我们的辅助列只能是 TIME 列,但是在表模型里面辅助列是能够通过 TIME_COLUMN 去指定的。不指定的话我们会自动寻找你在 select 里面写的第一个类型为 TIMESTAMP 的列作为辅助列,如果你指定了就可以用指定的那一列作为辅助列。比如有 TIME 和 event_time 这两个不同的 TIMESTAMP,你可以指定用 event_time,也就是第 2 列作为辅助列。
还有一个差异,树模型里面它分组内的 FILL 是针对结果集的,也就是 align by device 做填充的时候可能会错用前面的值,不会区分 d1/d2/d3,所以可能用 d2 的值去填充 d3 的数据。但是在表模型里面,你可以多指定一个分组内填充,也就是指定分组键在 device_id 相同的里面进行填充,如果 device_id 不相同就不能填充了。所以指定 FILL_GROUP 为 2 的时候,d3 的数据就不会用 d2 的数据填充了。这个也是我们对于 FILL 功能在表模型里面一个加强。
结果集去重
下面是结果集排序 order by,这个应该是比较常见的功能。默认是降序去排,也就是最先写的时间戳会在前面,我们可以通过 order by time desc 让原始数据倒序地输出,也可以去查最近 5 分钟之内设备最新的两行数据,还是按照时间轴去倒序排列并多加一个 limit 2。这里不限定 device 了,最新的两行一个是 d2 的 time=3,一个是 d1 的 time=3,那到底是 d1 在前面还是 d2 在前面?如果你没有指定 order by time desc , device_id,那这个顺序是随机的,有可能每次执行的都不一样。这个其实就是关于数据库的语义了,因为你没有针对这一列排序,所以它只会保证 TIME 列是有序的,但 TIME 列一样的情况下,它的行顺序是可以随意调整的。
结果集去重也可以用 select distinct,比如想获得某一个设备某段时间内指标去重后的所有值,我们就可以写 select distinct s1 from,可以看到去重之后只剩下两行。distinct 遵循关系型数据库 SQL 的语法,它也可以用在 aggregation function 里面。我们想统计 s1 已出现不同值的有多少行,那就可以用 count(distinct s1)。同样,原来如果不写 distinct,有多少个非 null 值都会统计出来,写了 distinct 之后统计出来会是两行。
多表联合查询
IoTDB 的树模型只支持 TIME 列的 FULL OUTER JOIN 作为默认的 JOIN 方式,但是 IoTDB 在表模型里面其实是计划支持所有标准类型的 JOIN,包括 INNER JOIN、LEFT JOIN、RIGHT JOIN 和 FULL OUTER JOIN,这个是原来树模型就支持的。
在关系型数据库标准代数定义的这四类之外,我们还会支持有时序语义特色的 ASOF JOIN。JOIN 其实就是多表的联合查询,当然也可以用这张表的子查询结果集和这张表的另外一个子查询结果集做表的自 JOIN。ASOF JOIN 是一个具有时序特色的 JOIN 方式,这个使用场景是什么呢?比如待分析的两个时间序列,采集频率是一样的,但是因为网络延迟或者上报误差等原因,两个序列的采集时间戳并不完全一致,会在一定时间范围内波动。那用户就希望把两个序列的数据根据最近的时间戳进行对齐显示,比如 s1、 s2 都是秒级采集的,但毫秒部分有一点点误差,用户想做的就是两个表的第一行、第二行、第三行接在一起,得到下表的结果。现有的四种 JOIN 方式都无法做到,因为只有一模一样的时间戳数据才会连在一起,而 ASOF JOIN 是可以做到这个效果的,可以写 table1 ASOF JOIN table2 ON table1.time ≤ table2.time,这样就能得到我们的预期结果了。
子查询
子查询功能也在表模型里面引入了。子查询就是嵌套在另一个 SQL 查询当中的查询语句,它的查询结果会作为外层查询的条件、数据源或者计算字段的一部分,子查询可以出现在 SELECT、FROM、WHERE、HAVING 里面,是我们去处理复杂逻辑的常用工具。比如这边举的一个关联查询的例子,我们在内部的子查询里用到了外部的父级字段。
子查询大体上也可以分为关联查询和非关联查询,IoTDB 目前的版本里面实现了非关联查询,在 SELECT、FROM、WHERE、HAVING 里面都可以出现。我们的关联子查询部分在 master 上,也就是最新分支上面已经实现了,在后续的版本里很快就会释放出来关联子查询的功能。
这里举几个典型的例子,比如子查询用在 FROM 子句里面,我们想先求某个设备某段时间内指标的平均值,然后获得其中的最大平均值,那就可以先把每个设备查询的平均值得到,查询出来之后再在外层套一个 max 查询,就能得到每一个设备里面 avg 的最大值。
还有一个例子,它可以用在我们的 where 子句,也就是标量子查询里面,可以作为 s1= 的一个过滤条件。我们想得到一个设备某段时间内某个指标最大值对应的所有行,有一种解法是先把最大值查到,再由客户端去发一个查询过来,s1=最大值 4 去查到,但是这样用户就要写两条 SQL 了。通过子查询的方式,我可以直接写 s1=某一个子查询的标量结果,它是可以直接得到这个结果的。同样的,比如我想查 >P99 所有的行,用分位数函数就是 s1 > select P99(s1) from table1,能查出来所有大于 P99 的异常行,这个在查询里面还是非常有用的。
03
表模型 Roadmap
表模型的功能已经大概介绍完了,下面跟大家简单介绍一下表模型的 Roadmap。
我们打算 3 月上旬实现基本的读写功能,这其实已经 OK 了,包含在 beta 版本里面了,以及 beta 版本释出、测试的时候,发现的一些 bug 的修复,这个会在 3 月上旬发出来一个 2.0.1 的正式版本。大概在 3 月下旬,我们想去发一个 2.0.2 的版本,去包含一些比较重要的功能更新,比如用户和权限的管理、自定义函数,包括自定义聚合函数和自定义标量函数,还有 MQTT 的写入协议,这个在一些工业场景里还是比较常见的,还有我们的系统表。
6 月下旬我们会去进一步扩展、补充查询功能,包括窗口函数、动态表函数,DBeaver 可视化工具,其他 SDK 的补齐,比如 C、C++、Go、C#、Rust 等等。在 9 月下旬,我们会支持完整的 JOIN 方式,比如刚刚提到的 ASOF JOIN,还有双模型转换的功能,关联子查询的功能可能会提前,因为 master 上面现在可能已经有了。还有 SQL:2016 里面提到的行模式识别,也有可能在 9 月下旬的 2.0.4 版本释出。最后,今年年底可能会对我们的 AI 能力、安全能力,包括模糊查询和序列模式识别这些高阶的功能做一个加强,会在 12 月下旬的 2.0.5 版本跟大家见面。
整体上我们是这么一个规划,里面有一些功能如果提前实现了,也会在前面的版本去放出来。
欢迎点击阅读原文下载 IoTDB 2.0.1-beta 版本试用!
规上企业应用实例
能源电力:中核武汉|国网信通产业集团|华润电力|大唐先一|上海电气国轩|清安储能|某储能厂商|太极股份
航天航空:中航机载共性|北邮一号卫星
钢铁、金属冶炼:宝武钢铁|中冶赛迪|中国恩菲
交通运输:中车四方|长安汽车|城建智控|德国铁路
智慧工厂与物联:PCB 龙头企业|博世力士乐|德国宝马|北斗智慧物联|京东|昆仑数据|怡养科技|绍兴安瑞思