openGemini大解析:预聚合

范祥 | October 27, 2023 | 171

​在openGemini中,在数据持久化到文件时,以时间线为维度,计算好了min,max,sum,count 并记录在文件元数据中,我们称为预聚合。 这些预聚合的数据,可以对部分查询场景起到加速的作用(减少磁盘读取和数据解码的开销)。

在openGemini中,在数据持久化到文件时,以时间线为维度,计算好了min,max,sum,count 并记录在文件元数据中,我们称为预聚合。

这些预聚合的数据,可以对部分查询场景起到加速的作用(减少磁盘读取和数据解码的开销)。

数据布局

在介绍预聚合之前,先简单说一下openGemini的数据布局。

数据在文件中是按时间线维度进行了划分的,每个时间线有一个独立的数据块(ChunkData),使用 ChunkMeta 来描述。

如下图所示,水平方向,每个 ChunkData 包含多列;垂直方向,每列按1000行为单位切割为 Segment 。

ChunkMeta

ChunkMeta 相关数据结构如下,本次我们重点关注timeRange 和 ColumnMeta.preAgg

type ChunkMeta struct {
    sid         uint64
    offset      int64
    size        uint32
    columnCount uint32
    segCount    uint32
    timeRange   []SegmentRange // 存放每个 segment 的时间范围
    colMeta     []ColumnMeta // 每列的元数据信息
}

type SegmentRange [2]int64 // min/max

type ColumnMeta struct {
    name    string
    ty      byte
    preAgg  []byte  // 序列化后的预聚合数据
    entries []Segment  // 存放 segment 的偏移量
}

预聚合数据

  • openGemini在数据存储时,按时间线维度,提前计算了每列的 min,max,sum,count 的结果,存放在 ColumnMeta 中
  • boolean 和 string 类型的数据仅记录count, 本次我们以 float,int 类型数据为例

查询加速

如果一个查询仅通过预聚合数据就能得到结果,那么可以减少磁盘读取, 数据解压等开销,极大的提升查询性能。

写入样例数据

  • 写入如下样例数据并刷盘,此时数据库中仅1个文件,2000行数据,1条时间线,1列,2个 segment
insert foo,host=127.0.0.1 val=1 1694654000000000001
insert foo,host=127.0.0.1 val=2 1694654000000000002
insert foo,host=127.0.0.1 val=3 1694654000000000003
... ...
insert foo,host=127.0.0.1 val=1999 1694654000000001999
insert foo,host=127.0.0.1 val=2000 1694654000000002000

加速 sum, count, min, max 查询

查询条件中的时间范围包含了数据的时间范围

SELECT sum(val) from foo where time>1694654000000000000 and time<1694654300000003000
SELECT count(val) from foo where time>1694654000000000000 and time<1694654300000003000 
SELECT min(val),max(val) from foo where time>1694654000000000000 and time<1694654300000003000

加速 first和last 算子 (适用于部分查询场景)

  • 有的场景,存储的数据是单调递增的,一段时间范围内满足:min==firstmax==last,则可以使用预聚合数据进行加速查询
  • 在ChunkMeta中有存放每个 segment的最小和最大时间
  • 由于数据是按时间有序排列的,因此最小值对应的时间为第一行数据的时间,最大对应的时间为最后一行数据的时间
  • 在满足下列条件时,可以直接取chunkmeta 中的时间,不需要读取磁盘上的数据
first 查询,数据没有空值,且时间范围满足:
query time range:    --------------
segment time range:      ---------------
此时取 segment 的最小时间即可
例如:SELECT first(val) from foo where time>1694654000000000000

last查询,数据没有空值,且时间范围满足:
 query time range:         ----------------
 segment time range: ---------------
 此时取 segment 的最大时间即可
例如:SELECT last(val) from foo where time<1694654000000002001
  • 了解更多

https://github.com/openGemini/openGemini/blob/main/engine/immutable/first_last_reader.go

补充:

除了时间范围限制外,还有以下场景不支持预聚合:

  1. field 过滤
  2. group by time

对比测试

  • 特别说明:本测试仅供参考,数据模型和查询模型的差异,测试结果差别可能非常大
  • 使用DevOps模型中的 redis 表进行测试,参考:
  • http://opengemini.org/116.html
  • 精确查询:
SELECT /*+ Exact_Statistic_Query */ count(*) FROM redis GROUP BY hostname
  • 预聚合查询:
SELECT count(*) FROM redis GROUP BY hostname

 

总结

预聚合可以有效提升查询性能效率,但目前来说应用范围有待扩展。欢迎大家到社区一起探讨扩展预聚合数据的应用场景,社区将不断推动技术研发,优化用户体验,在未来提供更加丰富、多元的功能。

 


更多资讯可关注openGemini公众号和视频号。如果您对 openGemini 相关技术感兴趣,欢迎到社区与大家进行相关技术讨论。

Related posts

微信扫一扫:分享

Scan me!

微信里点“发现”,扫一下

二维码便可将本文分享至朋友圈。

Copy Link Moments Facebook Twitter