一、背景
随着物联网的发展,时序数据库的需求越来越多,比如水文监控、工厂的设备监控、国家安全相关的数据监控、通讯监控、金融行业指标数据、传感器数据等。
在互联网行业中,也有着非常多的时序数据,例如用户访问网站的行为轨迹,应用程序产生的日志数据等等。
时序数据有几个特点:
- 基本上都是插入,没有更新的需求。
- 数据基本上都有时间属性,随着时间的推移不断产生新的数据,旧的数据不需要保存太久。
业务方对时序数据通常有几个查询需求。
- 获取最新状态,查询最近的数据(例如传感器最新的状态)。
- 展示区间统计,指定时间范围,查询统计信息,例如平均值,最大值,最小值,计数等。
- 获取异常数据,根据指定条件,筛选异常数据。
二、时序数据库应该具备的特点
-
压缩能力
通常用得上时序数据库的业务,传感器产生的数据量都是非常庞大的,数据压缩可以降低存储成本。 -
自动rotate
时序数据通常对历史数据的保留时间间隔是有规定的,例如一个线上时序数据业务,可能只需要保留最近1周的数据。为了方便使用,时序数据库必须有数据自动rotate的能力。 -
支持分片,水平扩展
因为涉及的传感器可能很多,单个节点可能比较容易成为瓶颈,所以时序数据库应该具备水平扩展的能力,例如分表应该支持水平分区。 -
自动扩展分区,
业务对时序数据的查询,往往都会带上对时间区间进行过滤,因此时序数据通常在分区时,一定会有一个时间分区的概念。时序数据库务必能够支持自动扩展分区,减少用户的管理量,不需要人为的干预自动扩展分区。例如1月份月末,自动创建2月份的分区。 -
插入性能
时序数据,插入是一个强需求。对于插入性能要求较高。 -
分区可删除
分区可以被删除,例如保留1个月的数据,1个月以前的分区都可以删除掉。 -
易用性(SQL接口)
SQL是目前最通用的数据库访问语言,如果时序数据库能支持SQL是最好的。 -
类型丰富
物联网的终端各异,会有越来越多的非标准类型的支持需求。例如采集图像的传感器,数据库中至少要能够存取图像的特征值。而对于其他垂直行业也是如此,为了最大程度的诠释业务,必须要有精准的数据类型来支撑。 -
索引接口
支持索引,毫无疑问是为了加速查询而引入的。 -
高效分析能力
时序数据,除了单条的查询,更多的是报表分析或者其他的分析类需求。这对时序数据库的统计能力也是一个挑战。 -
其他特色
(1)支持丰富的数据类型,数组、范围类型、JSON类型、K-V类型、GIS类型、图类型等。满足更多的工业化需求,例如传感器的位置信息、传感器上传的数据值的范围,批量以数组或JSON的形式上传,传感器甚至可能上传图片特征值,便于图片的分析。(例如国家安全相关),轨迹数据的上层则带有GIS属性。
这个世界需要的是支持类型丰富的时序数据库,而不是仅仅支持简单类型的时序数据库。
(2)支持丰富的索引接口,因为类型丰富了,普通的B-TREE索引可能无法满足快速的检索需求,需要更多的索引来支持 数组、JSON、GIS、图特征值、K-V、范围类型等。 (例如PostgreSQL的gin, gist, sp-gist, brin, rum, bloom, hash索引接口)。
这两点可以继承PostgreSQL数据库的已有功能,已完全满足。
三、TimescaleDB介绍
TimescaleDB是基于PostgreSQL数据库打造的一款时序数据库,插件化的形式,随着PostgreSQL的版本升级而升级,不会因为另立分支带来麻烦。
图 1 timescaleDB结构数据自动按时间和空间分片(chunk)
- partition key(分区键)
是由一个表上的一个列或者多个列组成,用于确定某一行特定数据分布在哪个分区上,用create table 语句来定义。 - chunks
分区在TimescaleDB中被称为chunk。 - query across time
跨时间查询 - query across space
跨空间查询
透明自动分区特性
在时序数据的应用场景下,其记录数往往是非常庞大的,很容易就达到 数以亿计 。而对于PG来说,由于大量的还是使用B+tree索引,所以当数据量到达一定量级后其写入性能就会出现明显的下降(这通常是由于索引本身变得非常庞大且复杂)。这样的性能下降对于时序数据的应用场景而言是不能忍受的,而TimescaleDB最核心的自动分区特性解决的就是这个问题。这个特性希望达到的目标如下:
- 随着数据写入的不断增加,将时序数据表的数据分区存放,保证每一个分区的索引维持在一个较小规模,从而维持住写入性能。
- 基于时序数据的查询场景,自动分区时以时序数据的时间戳为分区键,从而确保查询时可以快速定位到所需的数据分区,保证查询性能。
- 分区过程对用户透明,从而达到自动扩展的效果。
上图是PG10声明式分区示例,我们创建了一个表树,以主表为根,接下来是下一个子级别的四个设备表,然后是任意数量的时间表第三个子层次。上图显示了PG库多维表分区的概念视图。
PostgresSQL分区的意思是把逻辑上的一个大表分割成物理上的几块儿。在PG里表分区是通过表继承来实现的,一般都是建立一个主表,里面是空,然后每个分区都去继承它。无论何时,都应保证主表里面是空的。
PostgreSQL 10.x 之前的版本提供了一种“手动”方式使用分区表的方式,需要使用继承 + 触发器的来实现分区表,步骤较为繁琐,需要定义附表、子表、子表的约束、创建子表索引,创建分区删除、修改,触发器等。PostgreSQL分区表(Table Partitioning)应用
PostgreSQL 10.x 开始提供了内置分区表(内置是相对于 10.x 之前的手动方式)。内置分区简化了操作,将部分操作内置,最终简单三步就能够创建分区表。但是只支持范围分区(RANGE)和列表分区(LIST),11.x 版本添加了对 HASH 分区。PostgreSQL 10分区表性能测试
从上图可以看出来,需要每个时间间隔创建四个新表,每个设备一个。额外的设备将使分区策略进一步复杂化,所有这些额外的表都可能对性能和可伸缩性产生负面影响,因为需要为插入和查询处理更多的表。
TimescaleDB也依赖于表继承,但避免了深度嵌套的多级继承树。相反,它直接在根创建一个浅层叶块树,而不管分区维度的数量,如下所示。
图 3 TimescaleDB分区彩色框代表不同的”空间”键(例如,我们示例中的设备)。这样子的设计减少了树中表的数量并避免了嵌套。通过这样做,TimescaleDB 提高了插入和查询性能,简化了重新分区,并使表管理(和保留)更容易。
四、性能测试
环境:
- 操作系统:CentOS 7.8(虚拟机)
- Postgresql版本:12.1
- TimescaleDB版本:2.2.0
- CPU:Intel Core i7 9xx 1核心 4线程
- 内存:4G
- 硬盘:100G
- 时序库安装参考 离线安装Postgres、时序库(timescaledb)
安装工具
1、golang环境安装详见centos 7 安装golang
2、TSBS 安装
TSBS用于对批量加载性能和查询执行性能进行基准测试。(目前不测量并发插入和查询性能)github上的tsbs
cd $GOPATH
yum install git
git clone https://gitee.com/mirrors_toddlipcon/tsbs.git
cd tsbs/cmd
go get ./...
3、安装postgres缓存清理扩展pg_dropcache,
#进入contrib目录下,下载pg_dropcache
git clone git://github.com/zilder/pg_dropcache
cd pg_dropcache
make
make install USE_PGXS=1
生成测试数据
tsbs_generate_data --use-case="cpu-only" --seed=123 --scale=4000 --timestamp-start="2021-01-01T00:00:00Z" --timestamp-end="2021-01-04T00:00:00Z" --log-interval="10s" --format="timescaledb" | gzip > /tmp/timescaledb-data.gz
需要的变量:
1.--use-case:分为iot、cpu-only和devops,例如 cpu-only
2.--seed:一种用于确定性生成的PRNG种子,例如 123
3.--scale:需要生成的设备数量,例如 4000
4.--timestamp-start:数据生成的开始时间,例如 2016-01-01T00:00:00Z
5.--timestamp-end:数据生成的结束时间,例如 2016-01-04T00:00:00Z
6.--log-interval:以秒为单位读取每个设备之间的时间间隔,例如10
7.--format:需要生成的数据库, 例如timescaledb (cassandra、clickhouse、cratedb、influx、mongo、siridb、timescaledb)
上面的示例将生成一个伪CSV文件,可用于将数据批量加载到TimescaleDB中。上面的配置将生成超过1亿行。时间段每增加一天将增加大约3300万行,30天将产生10亿行。
生成查询脚本
本测试没有使用这些测试脚本,因为生成的脚本使用的time_bucket()是timescaledb专属方法。不过借鉴了相关的查询方法。
[root@panghu tsbs]# FORMATS="timescaledb" SCALE=4000 SEED=123 TS_START="2021-01-01T00:00:00Z" TS_END="2021-01-04T00:00:01Z" QUERIES=1000 QUERY_TYPES="single-groupby-1-1-1 single-groupby-1-1-12 single-groupby-1-8-1 single-groupby-5-1-1 single-groupby-5-1-12 single-groupby-5-8-1 cpu-max-all-1 cpu-max-all-8 double-groupby-1 double-groupby-5 double-groupby-all high-cpu-all high-cpu-1 lastpoint groupby-orderby-limit" BULK_DATA_DIR="/tmp/queries" scripts/generate_queries.sh
写入数据
-- 普通表
在tsbs目录下执行一下命令,其参数表示是单线程执行,创建普通表,每30s输出一次报告,并将报告输出到normal.log
NUM_WORKERS=1 USE_HYPERTABLE=false REPORTING_PERIOD=30s BULK_DATA_DIR=/tmp \
DATABASE_NAME=normal scripts/load_timescaledb.sh > normal.log
-- 超表
其创建的是以12小时分块的超表
NUM_WORKERS=1 CHUNK_TIME=12h USE_HYPERTABLE=true REPORTING_PERIOD=30s \
DATABASE_NAME=hyper BULK_DATA_DIR=/tmp scripts/load_timescaledb.sh > hypertable.log
经过1亿数据量的测试,并没有发现timescaledb在insert方面的优势。可能是机器配置或数据库配置问题,也可能是测试数据不够大。相比来说pg速度比timescaledb快,但timescaledb的插入速度一直都比较稳定。timescaledb官网上也只有一张图,并没有详细的测试场景介绍。
图4 官网测试按照官网的解释,当向 PostgreSQL 中插入新的数据行时,数据库需要更新表的每个索引列的索引(一般来说是BTREE)。一旦索引太大而无法放入内存——根据分配的内存,当表每10s内插入上百万行数据时,通常会发生这种情况——这需要从磁盘交换一页或多页。正是这种磁盘交换造成了插入瓶颈。在这个问题上投入更多的内存不可避免地造成延迟。
TimescaleDB通过大量利用自动时空分区解决了这一问题,即使是运行在一台机器上。从本质上讲,所有在最近时间间隔内的写入都只对保留在内存中的表进行。这使得在大规模插入数据时,插入性能比PostgreSQL提高了20倍。
查询性能测试
首先在两个库中引入pg_dropcache
[root@panghu pg_dropcache]# su - postgres -c psql
postgres=# \c normal
normal=# create extension pg_dropcache;
normal=# \c hypertable
hypertable=# create extension pg_dropcache;
每次查询前都执行
select pg_dropcache();
简单查询
1、1台设备在1小时内每1分钟的一个指标(一列)最大值
explain analyze select date_trunc('minute', time) as minute, max(usage_user) as max_usage_user from cpu where tags_id in (select id from tags where hostname in('host_249')) and time >='2021-01-02 00:00:00' and time < '2021-01-02 01:00:00' group by minute order by minute asc;
-- 普通表
GroupAggregate (cost=1529.58..1536.98 rows=370 width=16) (actual time=5.159..5.396 rows=60 loops=1)
Group Key: (date_trunc('minute'::text, cpu."time"))
-> Sort (cost=1529.58..1530.51 rows=370 width=16) (actual time=5.136..5.213 rows=360 loops=1)
Sort Key: (date_trunc('minute'::text, cpu."time"))
Sort Method: quicksort Memory: 41kB
-> Nested Loop (cost=0.85..1513.80 rows=370 width=16) (actual time=0.140..4.806 rows=360 loops=1)
-> Index Scan using tags_hostname_idx on tags (cost=0.28..8.30 rows=1 width=4) (actual time=0.060..0.062 rows=1 l
oops=1)
Index Cond: (hostname = 'host_249'::text)
-> Index Scan using cpu_tags_id_time_idx on cpu (cost=0.57..1500.88 rows=370 width=20) (actual time=0.063..4.039
rows=360 loops=1)
Index Cond: ((tags_id = tags.id) AND ("time" >= '2021-01-02 00:00:00+08'::timestamp with time zone) AND ("tim
e" < '2021-01-02 01:00:00+08'::timestamp with time zone))
Planning Time: 1.079 ms
Execution Time: 5.498 ms
-- 超表
Sort (cost=1477.89..1478.39 rows=200 width=16) (actual time=5.172..5.179 rows=60 loops=1)
Sort Key: (date_trunc('minute'::text, _hyper_1_2_chunk."time"))
Sort Method: quicksort Memory: 27kB
-> HashAggregate (cost=1467.74..1470.24 rows=200 width=16) (actual time=5.110..5.134 rows=60 loops=1)
Group Key: date_trunc('minute'::text, _hyper_1_2_chunk."time")
-> Nested Loop (cost=17.52..1465.91 rows=367 width=16) (actual time=0.416..4.783 rows=360 loops=1)
-> Index Scan using tags_hostname_idx on tags (cost=0.28..8.30 rows=1 width=4) (actual time=0.193..0.199 rows=1 l
oops=1)
Index Cond: (hostname = 'host_249'::text)
-> Bitmap Heap Scan on _hyper_1_2_chunk (cost=17.24..1453.02 rows=367 width=20) (actual time=0.209..4.104 rows=36
0 loops=1)
Recheck Cond: ((tags_id = tags.id) AND ("time" >= '2021-01-02 00:00:00+08'::timestamp with time zone) AND ("t
ime" < '2021-01-02 01:00:00+08'::timestamp with time zone))
Heap Blocks: exact=360
-> Bitmap Index Scan on _hyper_1_2_chunk_cpu_tags_id_time_idx (cost=0.00..17.15 rows=367 width=0) (actual t
ime=0.149..0.149 rows=360 loops=1)
Index Cond: ((tags_id = tags.id) AND ("time" >= '2021-01-02 00:00:00+08'::timestamp with time zone) AND
("time" < '2021-01-02 01:00:00+08'::timestamp with time zone))
Planning Time: 1.235 ms
Execution Time: 5.264 ms
2、扩大查询时间,12小时内,每分钟的数据最大值
explain analyze select date_trunc('minute', time) as minute, max(usage_user) as max_usage_user from cpu where tags_id in (select id from tags where hostname in('host_555')) and time >='2021-01-02 00:00:00' and time < '2021-01-02 12:00:00' group by minute order by minute asc;
-- 普通表
GroupAggregate (cost=18034.27..18124.93 rows=4533 width=16) (actual time=54.607..57.942 rows=720 loops=1)
Group Key: (date_trunc('minute'::text, cpu."time"))
-> Sort (cost=18034.27..18045.61 rows=4533 width=16) (actual time=54.586..55.601 rows=4320 loops=1)
Sort Key: (date_trunc('minute'::text, cpu."time"))
Sort Method: quicksort Memory: 395kB
-> Nested Loop (cost=186.64..17758.98 rows=4533 width=16) (actual time=3.339..51.775 rows=4320 loops=1)
-> Index Scan using tags_hostname_idx on tags (cost=0.28..8.30 rows=1 width=4) (actual time=0.059..0.066 rows=1 loops=1)
Index Cond: (hostname = 'host_555'::text)
-> Bitmap Heap Scan on cpu (cost=186.36..17694.02 rows=4533 width=20) (actual time=3.260..45.543 rows=4320 loops=1)
Recheck Cond: ((tags_id = tags.id) AND ("time" >= '2021-01-02 00:00:00+08'::timestamp with time zone) AND ("time" < '2021-01-02 12:00:00+08'::timestamp with time zone))
Heap Blocks: exact=4320
-> Bitmap Index Scan on cpu_tags_id_time_idx (cost=0.00..185.23 rows=4533 width=0) (actual time=2.229..2.229 rows=4320 loops=1)
Index Cond: ((tags_id = tags.id) AND ("time" >= '2021-01-02 00:00:00+08'::timestamp with time zone) AND ("time" < '2021-01-02 12:00:00+08'::timestamp with time zone))
Planning Time: 0.883 ms
Execution Time: 58.365 ms
-- 超表
Sort (cost=17181.49..17181.99 rows=200 width=16) (actual time=55.692..55.797 rows=720 loops=1)
Sort Key: (date_trunc('minute'::text, _hyper_1_2_chunk."time"))
Sort Method: quicksort Memory: 58kB
-> HashAggregate (cost=17171.34..17173.84 rows=200 width=16) (actual time=55.070..55.374 rows=720 loops=1)
Group Key: date_trunc('minute'::text, _hyper_1_2_chunk."time")
-> Nested Loop (cost=116.85..17148.76 rows=4517 width=16) (actual time=2.083..51.022 rows=4320 loops=1)
-> Index Scan using tags_hostname_idx on tags (cost=0.28..8.30 rows=1 width=4) (actual time=0.081..0.087 rows=1 loops=1)
Index Cond: (hostname = 'host_555'::text)
-> Append (cost=116.57..17084.01 rows=4516 width=20) (actual time=1.989..45.468 rows=4320 loops=1)
-> Bitmap Heap Scan on _hyper_1_2_chunk (cost=116.57..10590.86 rows=2824 width=20) (actual time=1.983..30.447 rows=2880 loops=1)
Recheck Cond: ((tags_id = tags.id) AND ("time" >= '2021-01-02 00:00:00+08'::timestamp with time zone) AND ("time" < '2021-01-02 12:00:00+08'::timestamp with time zone))
Heap Blocks: exact=2880
-> Bitmap Index Scan on _hyper_1_2_chunk_cpu_tags_id_time_idx (cost=0.00..115.86 rows=2824 width=0) (actual time=1.323..1.323 rows=2880 loops=1)
Index Cond: ((tags_id = tags.id) AND ("time" >= '2021-01-02 00:00:00+08'::timestamp with time zone) AND ("time" < '2021-01-02 12:00:00+08'::timestamp with time zone))
-> Bitmap Heap Scan on _hyper_1_3_chunk (cost=70.14..6470.57 rows=1692 width=20) (actual time=0.866..13.523 rows=1440 loops=1)
Recheck Cond: ((tags_id = tags.id) AND ("time" >= '2021-01-02 00:00:00+08'::timestamp with time zone) AND ("time" < '2021-01-02 12:00:00+08'::timestamp with time zone))
Heap Blocks: exact=1440
-> Bitmap Index Scan on _hyper_1_3_chunk_cpu_tags_id_time_idx (cost=0.00..69.71 rows=1692 width=0) (actual time=0.588..0.589 rows=1440 loops=1)
Index Cond: ((tags_id = tags.id) AND ("time" >= '2021-01-02 00:00:00+08'::timestamp with time zone) AND ("time" < '2021-01-02 12:00:00+08'::timestamp with time zone))
Planning Time: 1.978 ms
Execution Time: 56.013 ms
在单磁盘机器上,许多只执行索引查找或表扫描的简单查询在 TimescaleDB 上会慢 1-3 毫秒。例如,在我们带有索引时间、主机名和 cpu 使用信息的 1亿 行表中,每个数据库将花费不到 3ms,但在 TimescaleDB 上需要额外 1ms。由于计划时间开销稍大,大多数通常需要 <20 毫秒的简单查询(例如,索引查找)在 TimescaleDB 上会慢几毫秒。
基于时间的查询
查询在半天里CPU某个指标超过 90 的数据
explain analyze select * from cpu where usage_user > 90.0 and time >='2021-01-02 00:00:00' and time < '2021-01-02 12:00:00';
-- 普通表
Index Scan using cpu_time_idx on cpu (cost=0.57..921587.58 rows=1661968 width=133) (actual time=93.180..157431.245 rows=1654719 loops=1)
Index Cond: (("time" >= '2021-01-02 00:00:00+08'::timestamp with time zone) AND ("time" < '2021-01-02 12:00:00+08'::timestamp with time zone))
Filter: (usage_user > '90'::double precision)
Rows Removed by Filter: 15625281
Planning Time: 0.259 ms
Execution Time: 157719.448 ms
-- 超表
Gather (cost=1000.00..902892.82 rows=1795682 width=133) (actual time=10107.044..111039.684 rows=1654719 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Append (cost=0.00..722324.62 rows=748201 width=133) (actual time=6100.415..106707.848 rows=551573 loops=3)
-> Parallel Index Scan using _hyper_1_3_chunk_cpu_time_idx on _hyper_1_3_chunk (cost=0.44..294651.21 rows=275748 width=133) (actual time=55.335..81420.184 rows=180439 loops=3)
Index Cond: (("time" >= '2021-01-02 00:00:00+08'::timestamp with time zone) AND ("time" < '2021-01-02 12:00:00+08'::timestamp with time zone))
Filter: (usage_user > '90'::double precision)
Rows Removed by Filter: 1739561
-> Parallel Seq Scan on _hyper_1_2_chunk (cost=0.00..423932.41 rows=472453 width=133) (actual time=9092.177..37698.496 rows=556702 loops=2)
Filter: ((usage_user > '90'::double precision) AND ("time" >= '2021-01-02 00:00:00+08'::timestamp with time zone) AND ("time" < '2021-01-02 12:00:00+08'::timestamp with time zone))
Rows Removed by Filter: 8083298
Planning Time: 97.874 ms
Execution Time: 111278.058 ms
图5 两者查询数据量较大时查询时间对比
基于时间的查询,特别是在数据量较大时,timescaleDB通常可以达到postgre大约 1.2 到 5 倍的性能。
基于时间的聚合
查询一天内每个设备没小时某个测值平均值
explain analyze select date_trunc('hour', time) as hour, hostname, avg(usage_user) as mean_usage_user from cpu where time >= '2021-01-01 00:00:00' and time < '2021-01-02 00:00:00' group by hour, hostname order by hour;
-- 普通表
Finalize GroupAggregate (cost=2805761.31..5565569.39 rows=9714638 width=25) (actual time=297183.377..302860.122 rows=64000 loops=1)
Group Key: (date_trunc('hour'::text, "time")), hostname
-> Gather Merge (cost=2805761.31..5274130.25 rows=19429276 width=49) (actual time=297183.100..303492.096 rows=192000 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial GroupAggregate (cost=2804761.28..3030509.69 rows=9714638 width=49) (actual time=296854.664..302114.907 rows=64000 loops=3)
Group Key: (date_trunc('hour'::text, "time")), hostname
-> Sort (cost=2804761.28..2830840.14 rows=10431543 width=25) (actual time=296854.443..299567.644 rows=7680000 loops=3)
Sort Key: (date_trunc('hour'::text, "time")), hostname
Sort Method: external merge Disk: 310456kB
Worker 0: Sort Method: external merge Disk: 317048kB
Worker 1: Sort Method: external merge Disk: 315008kB
-> Parallel Index Scan using cpu_time_idx on cpu (cost=0.57..1089562.88 rows=10431543 width=25) (actual time=100.705..210288.786 rows=7680000 loops=3)
Index Cond: (("time" >= '2021-01-01 00:00:00+08'::timestamp with time zone) AND ("time" < '2021-01-02 00:00:00+08'::timestamp with time zone))
Planning Time: 2.233 ms
Execution Time: 303763.514 ms
-- 超表
Finalize GroupAggregate (cost=2419133.87..2526990.01 rows=40000 width=25) (actual time=188301.739..196638.338 rows=64000 loops=1)
Group Key: (date_trunc('hour'::text, _hyper_1_2_chunk."time")), _hyper_1_2_chunk.hostname
-> Gather Merge (cost=2419133.87..2525790.01 rows=80000 width=49) (actual time=188301.571..196564.337 rows=136000 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Partial GroupAggregate (cost=2418133.85..2515556.00 rows=40000 width=49) (actual time=176953.907..181966.568 rows=45333 loops=3)
Group Key: (date_trunc('hour'::text, _hyper_1_2_chunk."time")), _hyper_1_2_chunk.hostname
-> Sort (cost=2418133.85..2442364.39 rows=9692215 width=25) (actual time=176919.752..179504.723 rows=7680000 loops=3)
Sort Key: (date_trunc('hour'::text, _hyper_1_2_chunk."time")), _hyper_1_2_chunk.hostname
Sort Method: external merge Disk: 394720kB
Worker 0: Sort Method: external merge Disk: 150496kB
Worker 1: Sort Method: external merge Disk: 397320kB
-> Result (cost=0.00..829638.08 rows=9692215 width=25) (actual time=116.879..94302.311 rows=7680000 loops=3)
-> Parallel Append (cost=0.00..708485.39 rows=9692215 width=25) (actual time=116.857..88726.340 rows=7680000 loops=3)
-> Parallel Index Scan using _hyper_1_2_chunk_cpu_time_idx on _hyper_1_2_chunk (cost=0.44..254091.97 rows=2492192 width=25) (actual time=208.773..59973.717 rows=1920000 loops=3)
Index Cond: (("time" >= '2021-01-01 00:00:00+08'::timestamp with time zone) AND ("time" < '2021-01-02 00:00:00+08'::timestamp with time zone))
-> Parallel Seq Scan on _hyper_1_1_chunk (cost=0.00..405932.35 rows=7200023 width=25) (actual time=19.255..40519.150 rows=8640000 loops=2)
Filter: (("time" >= '2021-01-01 00:00:00+08'::timestamp with time zone) AND ("time" < '2021-01-02 00:00:00+08'::timestamp with time zone))
Planning Time: 276.695 ms
Execution Time: 196843.845 ms
图6 基于时间聚合的对比
由于 TimescaleDB 的时空分区,我们的数据集自然地被划分成块,每个块都有自己的索引。这在使用基于时间的聚合处理查询时有几个好处。在各种块上使用约束排除,TimescaleDB 可以扫描更少的数据,允许数据库选择更合适的计划:例如,完全在内存中执行计算与溢出到磁盘,使用更小的索引(因此可以遍历更少的索引值),使用更好的算法,如 HashAggregates 与 GroupAggregates 等。
基于时间排序的查询
1、分组查询后limit获取
explain analyze select date_trunc('minute', time) as minute, max(usage_user) from cpu where time < '2021-01-01 19:00:00' group by minute order by minute desc limit 5;
-- 普通表
Limit (cost=743688.63..743689.91 rows=5 width=16) (actual time=160970.872..161024.248 rows=5 loops=1)
-> Finalize GroupAggregate (cost=743688.63..747817.53 rows=16138 width=16) (actual time=160970.866..160970.876 rows=5 loops=1
)
Group Key: (date_trunc('minute'::text, "time"))
-> Gather Merge (cost=743688.63..747454.42 rows=32276 width=16) (actual time=160970.840..161024.208 rows=16 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Sort (cost=742688.60..742728.95 rows=16138 width=16) (actual time=160904.119..160904.186 rows=442 loops=3)
Sort Key: (date_trunc('minute'::text, "time")) DESC
Sort Method: quicksort Memory: 55kB
Worker 0: Sort Method: quicksort Memory: 55kB
Worker 1: Sort Method: quicksort Memory: 55kB
-> Partial HashAggregate (cost=741358.98..741560.70 rows=16138 width=16) (actual time=160903.347..160903.77
6 rows=660 loops=3)
Group Key: date_trunc('minute'::text, "time")
-> Parallel Index Scan using cpu_time_idx on cpu (cost=0.57..705526.81 rows=7166434 width=16) (actual
time=43.759..158098.601 rows=5280000 loops=3)
Index Cond: ("time" < '2021-01-01 19:00:00+08'::timestamp with time zone)
Planning Time: 136.133 ms
Execution Time: 161025.264 ms
-- 超表
Limit (cost=0.44..22819.45 rows=5 width=16) (actual time=296.532..1300.620 rows=5 loops=1)
-> GroupAggregate (cost=0.44..912761.09 rows=200 width=16) (actual time=296.527..1300.604 rows=5 loops=1)
Group Key: (date_trunc('minute'::text, cpu."time"))
-> Custom Scan (ChunkAppend) on cpu (cost=0.44..827155.96 rows=17120526 width=16) (actual time=32.262..1270.585 rows=12
0001 loops=1)
Order: date_trunc('minute'::text, cpu."time") DESC
-> Index Scan using _hyper_1_1_chunk_cpu_time_idx on _hyper_1_1_chunk (cost=0.44..784354.64 rows=17120526 width=1
6) (actual time=32.257..1230.772 rows=120001 loops=1)
Index Cond: ("time" < '2021-01-01 19:00:00+08'::timestamp with time zone)
Planning Time: 261.787 ms
Execution Time: 1300.790 ms
图7 分组查询后limit获取
数据库数据量越大,这个时间差会拉开得越大
删除数据库数据
时间序列数据通常建立得非常快,因此需要数据保留策略,例如“仅将原始数据存储一周”。TimescaleDB 提供了数据管理工具,使实施数据保留策略变得容易,而且性能明显高于 PostgreSQL。
事实上,将数据保留策略与聚合的使用相结合是很常见的,因此有时可能保留两个超级表:一个包含原始数据,另一个包含数据汇总到每分钟或每小时的聚合表中。然后,可以在两个超级表上定义不同的保留策略,例如,将聚合数据存储更长时间,而只保留原始数据一个月。
TimescaleDB 通过drop_chunks() 功能在块级别而不是行级别有效删除旧数据。比如之前我们这1亿数据,在超表中以12h一个块存储,会有6个块。下面将比较删除一天数据(因为前面数据是以UTC为准,所以超表向后推8小时)
-- 普通表
normal=# explain analyze delete from cpu where time < '2021-01-02 00:00:00';
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
----------------
Delete on cpu (cost=0.57..1146936.37 rows=25035703 width=6) (actual time=388605.156..388605.157 rows=0 loops=1)
-> Index Scan using cpu_time_idx on cpu (cost=0.57..1146936.37 rows=25035703 width=6) (actual time=65.239..343970.066 rows=23
040000 loops=1)
Index Cond: ("time" < '2021-01-02 00:00:00+08'::timestamp with time zone)
Planning Time: 82.617 ms
Execution Time: 388693.189 ms
-- 超表
hypertable=# explain analyse select drop_chunks('cpu','2021-01-02 00:00:00');
QUERY PLAN
--------------------------------------------------------------------------------------------------
ProjectSet (cost=0.00..5.02 rows=1000 width=32) (actual time=1107.801..1107.812 rows=1 loops=1)
-> Result (cost=0.00..0.01 rows=1 width=0) (actual time=0.007..0.007 rows=1 loops=1)
Planning Time: 0.168 ms
Execution Time: 1119.857 ms
图8 清理数据
TimescaleDB 的 drop_chunks 从超表中删除所有只包含早于指定持续时间的数据的块。因为块是单独的表,删除的结果是简单地从文件系统中删除一个文件,因此非常快,在 10 毫秒内完成。同时,PostgreSQL 需要几分钟才能完成,因为它必须删除表中的单数据行。
TimescaleDB 的方法还避免了底层数据库文件中的碎片,从而避免了在非常大的表中可能额外昂贵(即耗时)的清理需要。
五、用于时间序列操作的新功能
在单磁盘机器上,许多只执行索引查找或表扫描的简单查询在 PostgreSQL 和 TimescaleDB 之间的性能相似。例如上述性能测试的简单查询。
TimescaleDB 除了支持完整的 SQL外,还有许多定制的SQL函数,可以帮助我们以更少的代码行执行时间序列分析。
1、time_bucket() - 用于分析任意时间间隔的数据
可以将 time_bucket 看作 PostgreSQL date_trunc 函数的增强版。time_bucket 允许任意时间间隔(例如 10 秒、15 分钟、6 小时),而 date_trunc 仅提供标准日、分、小时。
示例:每个host在1小时内每 5 分钟的某个cpu指标平均值
这是postgres中用于时间段的解决方法之一 Stack Overflow
SELECT
c.hostname, i.start_time, AVG ( C.usage_user )
FROM cpu AS C
RIGHT JOIN (
SELECT
( SELECT MIN ( TIME ) :: DATE FROM cpu ) + ( n || ' minutes' ) :: INTERVAL start_time,
( SELECT MIN ( TIME ) :: DATE FROM cpu ) + ( ( n + 5 ) || ' minutes' ) :: INTERVAL end_time
FROM
generate_series ( 0, ( 24 * 60 ), 5 ) n
) AS i ON C.TIME >= i.start_time AND C.TIME < i.end_time
WHERE
C.TIME >= '2021-01-02 12:00:00' AND C.TIME < '2021-01-02 13:00:00'
GROUP BY
i.start_time, C.hostname
ORDER BY
c.hostname asc, i.start_time ASC;
下面是timescaleDB解决方法:
SELECT
hostname,
time_bucket ( '5 minute', TIME ) AS fifteen,
AVG ( usage_user ) AS avg_usage_user
FROM
cpu
WHERE
TIME >= '2021-01-02 12:00:00'
AND TIME < '2021-01-02 13:00:00'
GROUP BY
fifteen,
hostname
ORDER BY
hostname ASC, fifteen ASC
不仅更简洁,而且查询速度也差了几十倍。
2、last(),first() - 用于分析聚合中的数据
last()和first()一般用于聚合函数中,可用于查询组中的最后一条/最后一条。
示例:1小时内,每个host每分钟最后一条数据。
postgres
SELECT
hostname, time, dt, usage_user
FROM
(
SELECT ROW_NUMBER ( ) OVER ( PARTITION BY dt, hostname ORDER BY TIME DESC ) row_id,
dt, hostname, time, usage_user
FROM
( SELECT hostname, time, usage_user, date_trunc( 'minute', TIME ) dt FROM cpu WHERE time> '2021-01-03 00:00:00' AND time< '2021-01-03 01:00:00' ) AS t
) AS rs
WHERE
rs.row_id = 1
ORDER BY
hostname,
TIME
timescaleDB
SELECT
time_bucket ( '1 minute', time) one_min, hostname, LAST ( usage_user, time)
FROM cpu
WHERE
time>= '2021-01-03 00:00:00' AND time< '2021-01-03 01:00:00'
GROUP BY
one_min, hostname
ORDER BY
hostname, one_min
3、time_bucket_gapfill() - 填充间隙
有时我们的数据中间丢失了一段,会导致数据不连续,如果有这方面的需求,就必须通过一些手段生成缺失数据。
例如:获取上周每个设备每天的温度,插入缺失的读数:
SELECT
time_bucket_gapfill('1 day', time, now() - INTERVAL '1 week', now()) AS day,
device_id,
avg(temperature) AS value,
interpolate(avg(temperature))
FROM metrics
WHERE time > now () - INTERVAL '1 week'
GROUP BY day, device_id
ORDER BY day;
day | device_id | value | interpolate
------------------------+-----------+-------+-------------
2019-01-10 01:00:00+01 | 1 | |
2019-01-11 01:00:00+01 | 1 | 5.0 | 5.0
2019-01-12 01:00:00+01 | 1 | | 6.0
2019-01-13 01:00:00+01 | 1 | 7.0 | 7.0
2019-01-14 01:00:00+01 | 1 | | 7.5
2019-01-15 01:00:00+01 | 1 | 8.0 | 8.0
2019-01-16 01:00:00+01 | 1 | 9.0 | 9.0
(7 row)
一般来说,单独使用time_bucket_gapfill()只会填充缺失时间点,但是不会填充数据,要配合locf()和interpolate()来使用。
- locf():可让将空值填充成聚合分组中前一个分组的值。
- interpolate():对缺失值进行线性插值。
timescaleDB还提供了很多聚合、统计、计算等API,可前往官网查阅,此处不一一赘述。TimescaleDB API参考
网友评论