1. 简介
从 TDengine 3.3.0.0 版本之后,新增了复合主键的功能。
TDengine 原来的时间列是不允许有重复时间戳的,有了复合主键功能后,时间列即允许有重复,重复后的时间戳按紧跟其后第二列主键列的值来确定唯一性。
此功能的常用场景如高速收费口,同一时间内可能会多辆车通过,股票交易记录,同一时间内笔成交等多种实际应用场景。
2. 功能说明
2.1 建表
用户可以在建表时用 PRIMARY KEY
关键字,额外指定除主键时间戳列以外的另一列作为主键列,该列与时间戳列共同组成一行数据的键值,其类型必须为整型(int32, int64, uint32, uint64) 或 字符串类型(varchar)。超级表与普通表均支持复合主键。复合主键列不支持修改、增加和删除操作。PRIMARY KEY 列只能设置为第二列。
具体 SQL 语句如下:
CREATE TABLE t ( ts TIMESTAMP, obj_id VARCHAR(64)
PRIMARY KEY, data1 FLOAT, data2 int );
2.2 写入
只有当时间戳相同且 PRIAMRY KEY 列值都相同时,两行数据才会被认为是重复数据,否则被认为是两行不同数据。如果时间戳相同但 Primary key 较小数据 后写入,则视为乱序数据处理。
schemaless 写入不支持复合主键的情况,因为 schemaless 本身就是没有 schema,没法确定复合主键,目前的三种 schemaless 协议 (influxdb line/opentsdb telnet/opentsdb json) 也没有复合主键这个概念。
2.3 删除
删除操作与现有删除操作一样,只支持按时间段删除,不支持按主键范围删除。
2.4 查询
2.4.1 数据读取
查询复合主键数据时,时间戳相同且 PRIMARY KEY 列值相同时,被认为是一行数据。对于复合主键(时间戳+PRIMARY KEY 列)相同但被多次写入的重复数据则在合并更新后返回。只有复合主键不同的行被认为不同的行数据,被查询分别返回。每个表的读取结果是按(时间戳主键,Primary Key)有序。其他查询行为也将相应地变动。具体变化内容见后续章节。
查询行为的适配源自于TSDB reader 返回给上层的基础数据结构 SSDataBlock中包含了时间戳相同,但primary key 不同的记录。
2.4.2 标量查询
此原来行为相同,无改变
2.4.3 聚合查询
- 非时间窗口类:用户可见层面无变化。
- 时间窗口聚合类:TDengine 系统中针对时间窗口采用闭区间进行描述,并且相同时间戳不同primary key的记录归属于同一个时间窗口。故用户可见的时间窗口无变化。
- 在超级表的查询中,已经有归属于不同时间线的相同时间戳的查询,增加了 primary key 以后,用户可见表现层面无任何变化。
2.4.4 查询函数
|----------|------------------|------|
| 类别 | 函数名 | 是否变化 |
| 数据函数 | ABS | 无影响 |
| 数据函数 | ACOS | 无影响 |
| 数据函数 | ASIN | 无影响 |
| 数据函数 | ATAN | 无影响 |
| 数据函数 | CEIL | 无影响 |
| 数据函数 | COS | 无影响 |
| 数据函数 | FLOOR | 无影响 |
| 数据函数 | LOG | 无影响 |
| 数据函数 | POW | 无影响 |
| 数据函数 | ROUND | 无影响 |
| 数据函数 | SIN | 无影响 |
| 数据函数 | SQRT | 无影响 |
| 数据函数 | TAN | 无影响 |
| 字符串函数 | CHAR_LENGTH | 无影响 |
| 字符串函数 | CONCAT | 无影响 |
| 字符串函数 | CONCAT_WS | 无影响 |
| 字符串函数 | LENGTH | 无影响 |
| 字符串函数 | LOWER | 无影响 |
| 字符串函数 | LTRIM | 无影响 |
| 字符串函数 | RTRIM | 无影响 |
| 字符串函数 | SUBSTR | 无影响 |
| 字符串函数 | UPPER | 无影响 |
| 时间和日期函数 | TIMEDIFF | 无影响 |
| 时间和日期函数 | TIMETRUNCATE | 无影响 |
| 转换函数 | CAST | 无影响 |
| 转换函数 | TO_ISO8601 | 无影响 |
| 转换函数 | TO_JSON | 无影响 |
| 转换函数 | TO_UNIXTIMESTAMP | 无影响 |
| 转换函数 | TO_CHAR | 无影响 |
| 转换函数 | TO_TIMESTAMP | 无影响 |
| 聚合函数 | APERCENTILE | 无影响 |
| 聚合函数 | AVG | 无影响 |
| 聚合函数 | COUNT | 无影响 |
| 聚合函数 | ELAPSED | 无影响 |
| 聚合函数 | LEASTSQUARES | 无影响 |
| 聚合函数 | SPREAD | 无影响 |
| 聚合函数 | STDDEV | 无影响 |
| 聚合函数 | SUM | 无影响 |
| 聚合函数 | HYPERLOGLOG | 无影响 |
| 聚合函数 | HISTOGRAM | 无影响 |
| 聚合函数 | PERCENTILE | 无影响 |
| 选择函数 | BOTTOM | 无影响 |
| 选择函数 | FIRST | 有变化 |
| 选择函数 | INTERP | 有变化 |
| 选择函数 | LAST | 有变化 |
| 选择函数 | LAST_ROW | 有变化 |
| 选择函数 | MAX | 无影响 |
| 选择函数 | MIN | 无影响 |
| 选择函数 | MODE | 无影响 |
| 选择函数 | SAMPLE | 无影响 |
| 选择函数 | TAIL | 无影响 |
| 选择函数 | TOP | 无影响 |
| 选择函数 | UNIQUE | 有变化 |
| 时序数据特有函数 | CSUM | 无影响 |
| 时序数据特有函数 | DERIVATIVE | 有变化 |
| 时序数据特有函数 | DIFF | 有变化 |
| 时序数据特有函数 | IRATE | 有变化 |
| 时序数据特有函数 | MAVG | 无影响 |
| 时序数据特有函数 | STATECOUNT | 无影响 |
| 时序数据特有函数 | STATEDURATION | 无影响 |
| 时序数据特有函数 | TWA | 有变化 |
变化的函数行为变化如下:
1. Interp
Interp 函数返回设定时间点 T 0 的插值(断面)数据,其前置时间戳 Tprev(小于 T0的最大时间戳)或后置时间戳Tnxt(大于T0的最小时间戳)可能有重复时间,并且其对应的数值不同。
进行插值计算时,前置时间戳 Tprev 或后置时间戳 Tnxt 均只使用首次出现的记录进行计算,丢弃其后出现的相同时间戳不同主键的记录。
首次出现 的定义:对于任一时间戳T x,在按照升序返回的数据记录中,(同一个表中数据)第一条 T x 的记录即为首次出现记录。降序返回的数据记录中(同一个表中数据)Tx 最后一次出现的记录为首次出现。
需要注意,首次出现的判定只针对同一个表,对于超级表下的不同的表,无首次出现的判定。
2. FIRST
First 返回时间戳最小的记录。对于同一个表中具有相同(最小)时间戳不同主键的情况,返回该表中具有该时间戳的所有记录中++首次出现++的记录。
++首次出现++定义同上。
超级表查询中,在完成上述逻辑以后,还需要对来自不同表的时间戳进行比较和取舍,针对来自不同vnode的记录,如果 ts 相同需要进行 primary key 的比较。因此 first 函数返回的全局最小的(ts + primary key) 最早的结果。
3. LAST
Last 返回时间戳最大的记录。对于同一个表中具有相同(最大)时间戳不同主键的情况,返回该表中具有该时间戳的所有记录中++末次出现++的记录。
++末次出现++定义对应于首次出现的定义。
针对超级表跨 vnode 查询返回的结果,其判定逻辑于 FIRST 函数处理逻辑相同,需要同时比较 ts + primary key
4. LAST_ROW
与 last 函数相同。
5. UNIQUE
Unique 返回任一时间戳第一次出现的记录。对于同一个表中的记录,只返回++首次出现++的记录。不同子表中的相同时间戳记录,判定逻辑不变。
++首次出现++定义同上。
6. DERIVATIVE
同一个子表中的记录,使用++首次出现++记录进行计算,忽略后续出现的相同时间戳不同主键的记录。
++首次出现++定义同上。
7. DIFF
同一个子表中的记录,使用++首次出现++记录进行计算,忽略后续出现的相同时间戳不同主键的记录。
++首次出现++定义同上。
8. IRATE
同 derivative 函数处理逻辑相同。
9. TWA
同一个子表中数据进行窗口边界插值的时候,均使用++首次出现++记录进行插值计算返回结果。非首次出现记录不参与计算。
++首次出现++定义同上。
10. show create
Show create
返回创建(超级)表的SQL 语句,对于有 primary key 的(超级)表,相应的 SQL 语句有变化
11. DESC <table_name>
DESC 获取 (超级)表的 schema 信息,返回的结果中,需要标记第二列(primary key 只能为第二列)是否是 primary key 列。
12. INSERT INTO <table_name> SELECT
会进行符合性检查,具有 primary key 的(超级)表的不能向无 primary key 的表中写入数据。无 primary key 的表可以向具有 primary key 的表写入数据。
向 primary key 的表写入数据的时候,要求 primary key 列必须非 NULL。
2.4.5 查询子句
|----|-----------------|------|
| 序号 | 子句 | 行为变化 |
| 1 | 比较表达式/条件子句 | 无影响 |
| 2 | fill 子句 | 无影响 |
| 3 | 窗口子句 | 无影响 |
| 4 | group by 子句 | 无影响 |
| 5 | partition by 子句 | 无影响 |
| 6 | join 查询 | 无影响 |
| 7 | Distinct 子句 | 无影响 |
| 8 | union子句 | 无影响 |
| 9 | slimit子句 | 无影响 |
| 10 | limit子句 | 无影响 |
| 11 | order by子句 | 无影响 |
| 12 | having子句 | 无影响 |
| 13 | range子句 | 无影响 |
| 14 | every子句 | 无影响 |
| 15 | 子查询 | 无影响 |
2.5 流计算
2.5.1 流计算窗口
对我们所支持的窗口类型,即 Interval、Session窗口,如果数据源表是复合主键,数据先按时间戳排序,时间戳相同的,再按复合主键排序,然后才是其他列排序。所以复合主键场景,流计算当前的设计和实现能够透明处理,符合流计算窗口当前的预期。存储流计算结果的表是复合主键,则Interval、Session 窗口输出结果时,需要按照时间戳和复合主键排序。
流计算对于乱序数据的界定方式:当前这批写入的数据,与之前写入的数据做对比。窗口计算要求数据按时间戳有序,只是要求当前这批数据,这个是局部的,并不是要求数据源的全部数据有序。
2.5.2 创建流计算
需要考虑存储流计算结果的表。对于自动创建超级表场景,需要提供复合主键的信息。新增语法,在创建流计算的SQL中,显式指定复合主键信息,即流计算结果中,哪些列是复合主键。对于写入已存在超级表,通过元数据能够获取复合主键的信息,不需要SQL中指定。
语法如下:
CREATE STREAM
[IF NOT EXISTS] stream_name [stream_options]
INTOstb_name[(field1_name, field2_name [
PRIMARY KEY], field3_name, ...)] [
TAGS(create_definition [, create_definition] ...)]
SUBTABLE(expression)
ASsubquery
PRIMARY KEY 使用规则和限制与建表SQL相同。
2.5.3 流计算中不支持的场景
由于需要按照 PK 删除 已经生成的记录。因此,不支持状态窗口、事件窗口、计数窗口在有 pk 的表上进行计算。对于全是标量函数的流计算,不支持数据源是复合主键,且目标表不是复合主键。
2.6 订阅
2.6.1 接口变更
-
tmq_get_json_meta(获取meta信息的 json 描述)
该接口内部创建表数据结构里增加列是否是复合主键参数("isPrimarykey":true)。已在文档里更新: https://jira.taosdata.com:18090/pages/viewpage.action?pageId=158206215
-
tmq_get_raw 接口返回的 raw data 做了升级,请查看文档 数据订阅结果序列化方案
2.6.2 其它接口无变更
2.7 Last 缓存
-
cache_model: none,返回最大的时间戳和该时间戳下最大的主键所对应的行或值
-
cache_mode: last_value, last_row, both,查询行为受缓存更新规则影响,更新规则如下:
缓存的更新粒度仍然是表级别。在主时间戳列相同情况下,主键列按大小顺序取最大值。即在更新缓存时,新到达的数据与当前 last 缓存中的数据相比,如果时间戳更大或时间戳相同但主键值更大则更新缓存,否则不更新缓存。
3. 性能影响
3.1 写入性能
-
非重复时间戳:在没有任何两条记录时间戳相同的情况下,其写入性能与未引入 primary key 相比完全相同
-
有重复时间戳的场景:在极端情况下,时间戳全部相同,而 primary key 单调递增,预估这种场景下写入性能下降 50%,原因是要进行两次键值比较。
-
纯乱序数据写入:即任意一行记录的时间戳和 primary key 都有可能小于之前已经写入的部分记录,这种情况下仍旧无法预估性能下降的比例。原则乱序程度越高,性能下降越明显
3.2 查询性能
3.2.1 数据读取
读取过程中,在merge阶段需要同时比较 ts 和 primary key column,因此 merge 过程会消耗更多的 CPU,记录合并过程会出现明显地性能下降。
合并过程是在 tsdb 完成,因此所有的查询均受影响(只使用 head 文件 和 SMA 索引的查询除外)。性能受到的影响程度受 key长度、数据写入模式、数据在文件中物理分布、数据重复比例、数据读取开销在整个查询过程中资源开销占比等诸多因素共同影响,无法简单评估性能下降的程度。
3.2.2 不同类型查询性能表现
在排除数据读取性能降低的场景下,对于非分组类型的查询(没有 partition by 和 group by 子句),没有明显的变化。
4. 兼容性
-
不会对已存在的数据产生影响,数据不支持版本回退,因为低版本识别不出复合主键。
-
流计算会有影响。需要删除删除流计算才能进行升级。
5. 约束和限制
-
只允许额外指定除时间戳以外的一列作为主键
-
支持定长和变长类数据类型,暂定支持非浮点数及 varchar。
-
主键与时间戳列一样,不允许缺省或为 NULL
-
主键列不允许 alter 或 drop 等操作
-
已建无主键列的表不可以通过 alter 语句添加主键列
-
不支持对主键列的范围删除
6. 总结
本章主要介绍了新功能复合主键的对外接口使用、影响的功能及函数内部技术实现,受约条件等,帮助技术开发人员更好了解及使用此功能。