ClickHouse存储引擎之MergeTree引擎——数据TTL

一、数据TTL

​ TTL(Time to Live),表示数据的存活时间。在MergeTree中,可以为某个列字段或整张表设置TTL时间。若为列字段的TTL,当时间到期时,则会删除这一列的数据;若为表级别的TTL,当时间到期时,则会删除整张表的数据;若一张表同时设置了列级别和表级别的TTL,则会以先到期的为主。

​ 无论是列级别还是表级别的TTL,都需要依托于某个DateTime或Date类型的字段,通过对这个时间字段的INTERVAL操作,来描述TTL的过期时间,设置TTL过期时间的INTERVAL完整操作包括:SECOND、MINUTE、HOUR、DAY、WEEK、MONTH、QUARTER和YEAR。例如:

1
2
3
4
5
-- 表示数据存活时间为,time_col时间的三天内
TTL time_col + INTERVAL 3 DAY

-- 表示数据存活时间为,time_col一月内
TTL time_col + INTERVAL 1 MONTH

二、列级别TTL

​ 若设置了列级别的TTL,当列字段中的值过期时,ClickHouse会将他们替换成默认值。如果一个分区内,某一列的所有值都已过期,那么ClickHouse会从文件系统中删除这个分区目录下的列文件。

​ 如果想要设置列级别的TTL,需要在定义表字段的时候,为列声明TTL表达式,主键字段不能被声明成TTL。示例数据如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
-- 创建有TTL列的表结构
CREATE TABLE ttl_table_t1
(
`id` String,
`create_time` DateTime,
`code` String TTL create_time + toIntervalSecond(10),
`type` UInt8 TTL create_time + toIntervalSecond(10)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(create_time)
ORDER BY id

-- 可以看到,create_time为日期类型,列字段code和type都被设置了TTL时间,他们存活时间,都为create_time建立后向后10s
-- 写入测试数据
INSERT INTO ttl_table_t1 VALUES('A000', now(), 'c1', 1), ('A000', now() + INTERVAL 2 MINUTE, 'c1', 2);

-- 查看数据
select * from ttl_table_t1;
┌─id───┬─────────create_time─┬─code─┬─type─┐
│ A000 │ 2020-07-27 21:29:15 │ c1 │ 1 │
│ A000 │ 2020-07-27 21:31:15 │ c1 │ 2 │
└──────┴─────────────────────┴──────┴──────┘

ClickHouse看到数据已经过期的时候,将执行合并,合并的频率由merge_with_ttl_timeout参数控制,SETTINGS merge_with_ttl_timeout = 86400 默认为86400s即1天,如果这个值太低,表示ClickHouse需要执行许多计划外的合并,可能消耗大量的资源。

如果在ClickHouse合并期间进行查询,可能会获得过期的数据,所以在select查询之前可使用optimize命令强制触发TTL清理机制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 查看merge_with_ttl_timeout参数的值
select * from system.merge_tree_settings where name='merge_with_ttl_timeout';
┌─name───────────────────┬─value─┬─changed─┬─description───────────────────────────────────────────────────┐
│ merge_with_ttl_timeout │ 86400 │ 0 │ Minimal time in seconds, when merge with TTL can be repeated. │
└────────────────────────┴───────┴─────────┴───────────────────────────────────────────────────────────────┘

-- 10s后强制触发TTL清理,FINAL表示触发所有分区合并,没有FINAL表示触发一个分区合并
optimize TABLE ttl_table_t1 FINAL;

-- 再次查看数据
-- 可以看到由于第一行满足TTL过期条件,所以被还原为了列的默认值
select * from ttl_table_t1;
┌─id───┬─────────create_time─┬─code─┬─type─┐
│ A000 │ 2020-07-27 21:29:15 │ │ 0 │
│ A000 │ 2020-07-27 21:31:15 │ c1 │ 2 │
└──────┴─────────────────────┴──────┴──────┘

​ 如果需要修改列字段的TTL或为已有字段添加TTL,可使用ALTER语句设置:ALTER TABLE ttl_table_t1 MODIFY COLUMN code STRING TTL create_time + INTERVAL 1 DAY,目前ClickHouse没有提供取消列级别TTL的方法。

三、表级别TTL

​ 如果需要为整张表设置TTL过期时间,则需要在MergeTree的表参数重增加TTL表达式,例如:

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE ttl_table_t2
(
`id` String,
`create_time` DateTime,
`code` String TTL create_time + toIntervalSecond(10),
`type` UInt8 TTL create_time + toIntervalSecond(10)
)
ENGINE = MergeTree
PARTITION BY toYYYYMM(create_time)
ORDER BY id
TTL create_time + INTERVAL 1 DAY

​ 上面示例中整张表都被设置了TTL,当触发到TTL清理时,满足过期时间的数据行将被整行删除。表级别的TTL也支持修改,同样适用ALTER语句:ALTER TABLE ttl_table_t2 MODIFY TTL create_time + INTERVAL 3 DAY同样,表级别的TTL目前也不支持删除。

1
2
3
4
5
6
7
8
9
10
11
-- 执行上述alter语句修改后表结构如下:
clickhouse-server_1 :) desc ttl_table_t2

DESCRIBE TABLE ttl_table_t2

┌─name────────┬─type─────┬─default_type─┬─default_expression─┬─comment─┬─codec_expression─┬─ttl_expression─────────────────────┐
idString │ │ │ │ │ │
│ create_time │ DateTime │ │ │ │ │ │
│ code │ String │ │ │ │ │ create_time + toIntervalSecond(10) │
type │ UInt8 │ │ │ │ │ create_time + toIntervalSecond(10) │
└─────────────┴──────────┴──────────────┴────────────────────┴─────────┴──────────────────┴────────────────────────────────────┘

四、TTL的运行原理

​ 当一张MergeTree表被设置了TTL表达式,在写入数据时,会以数据分区为单位,在每个分区目录内生成一个ttl.txt的文件,以上面的表ttl_table_t2为例,它既包含了列级别TTL,还包含了表级别的TTL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 向ttl_table_t2表插入数据
insert into ttl_table_t2 VALUES('A000', now(), 'haha', 2), ('A001', now(), 'xx', 2) ;

-- 查询
select * from ttl_table_t2

┌─id───┬─────────create_time─┬─code─┬─type─┐
│ A000 │ 2020-07-28 14:28:23 │ haha │ 2
│ A001 │ 2020-07-28 14:28:23 │ xx │ 2
└──────┴─────────────────────┴──────┴──────┘

-- 由于这里插入了两条数据,所以会有两个分区目录
[root@xxxx ttl_table_t2]# ll
total 8
drwxr-x--- 2 101 101 4096 Jul 28 14:28 202007_1_1_0
drwxr-x--- 2 101 101 272 Jul 28 14:28 202007_1_1_1
drwxr-x--- 2 101 101 10 Jul 28 14:28 detached
-rw-r----- 1 101 101 1 Jul 28 14:28 format_version.txt

​ 在写入数据后,每个分区目录内都会生成ttl.txt文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
## tree查看当前分区目录结构,用202007_1_1_0分区为例
tree ./202007_1_1_0
.
├── checksums.txt
├── code.bin
├── code.mrk2
├── columns.txt
├── count.txt
├── create_time.bin
├── create_time.mrk2
├── id.bin
├── id.mrk2
├── minmax_create_time.idx
├── partition.dat
├── primary.idx
├── ttl.txt
├── type.bin
└── type.mrk2

## 查看ttl.txt信息
cat ttl.txt
ttl format version: 1
{"columns":[{"name":"code","min":1595917713,"max":1595917713},{"name":"type","min":1595917713,"max":1595917713}],"table":{"min":1596004103,"max":1596004103}}

​ 通过ttl.txt内容可以发现,MergeTree时通过一串JSON配置保存了ttl的相关信息,其中:

  • columns:用于保存列级别ttl的信息
  • table:用于表示表级别ttl的信息
  • min和max:保存了当前数据分区内,TTL指定日期字段的最小值、最大值分别与INVERTAL表达式计算后的时间戳。

将table属性中的min和max时间戳格式化,并分别和create_time最小值与最大值对比:

1
2
3
4
5
6
7
8
9
10
SELECT 
toDateTime('1596004103') AS ttl_min,
toDateTime('1596004103') AS ttl_max,
ttl_min - MIN(create_time) AS expire_min,
ttl_max - MAX(create_time) AS expire_max
FROM ttl_table_t2

┌─────────────ttl_min─┬─────────────ttl_max─┬─expire_min─┬─expire_max─┐
2020-07-29 14:28:232020-07-29 14:28:238640086400
└─────────────────────┴─────────────────────┴────────────┴────────────┘

TTL的处理逻辑:

  • MergeTree以分区目录为单位,通过分区内的ttl.txt文件记录过期时间,并最为后续的判断依据
  • 当写入一批数据时,都会给予INTERVAL表达式的计算结果为这个分区生成对应的ttl.txt文件
  • 在MergeTree进行合并分区时,会触发删除TTL过期数据的逻辑
  • 在选择删除的分区时,使用了贪婪算法:尽可能找到会最早过期,合并次数最多的分区(MaxBlockNum)最大
  • 如果一个分区内某一列数据因为TTL到期被全部删除,在合并之后的新分区目录中,不会包含这个列字段的数据文件(column.bin和column.mrk)

ClickHouse针对列级别和表级别的TTL目前都没有提供删除TTL策略的方法,仅提供了全局开启/关闭TTL的方法:SYSTEM START/STOP TTL MERGES;,而且该配置并不能指定某张表开启或关闭TTL。