ClickHouse和他的朋友们(6)MergeTree存储结构

Stella981
• 阅读 730

上篇的 存储引擎技术进化与MergeTree 介绍了存储算法的演进。

存储引擎是一个数据库的底盘,一定要稳和动力澎湃。

接下来我们将一起来探索下 ClickHouse MergeTree 列式存储引擎,解构下这台“跑车”最重要的部件。

所有的存储引擎,无论精良与粗制滥造,最终都是要把数据回写到磁盘,来满足存储和索引目的。

磁盘文件的构造可以说是算法的物理体现,我们甚至可以通过这些存储结构反推出其算法实现。

所以,要想深入了解一个存储引擎,最好的入手点是它的磁盘存储结构,然后再反观它的读、写机制就会有一种水到渠成的感觉。

如果这个分析顺序搞反了,会有一种生硬的感觉,网上大部分教程都是这种“生硬”式教学,本文将直击灵魂从最底层谈起,彻底搞明白4个问题:

  1. MergeTree 有哪些文件?

  2. MergeTree 数据如何分布?

  3. MergeTree 索引如何组织?

  4. MergeTree 如何利用索引加速?

话不多说,上表:

CREATE TABLE default.mt(    `a` Int32,    `b` Int32,    `c` Int32,    INDEX `idx_c` (c) TYPE minmax GRANULARITY 1)ENGINE = MergeTreePARTITION BY a ORDER BY bSETTINGS index_granularity=3

造点数据:

insert into default.mt(a,b,c) values(1,1,1);insert into default.mt(a,b,c) values(5,2,2),(5,3,3);insert into default.mt(a,b,c) values(3,10,4),(3,9,5),(3,8,6),(3,7,7),(3,6,8),(3,5,9),(3,4,10);

磁盘文件

ls ckdatas/data/default/mt/1_4_4_0  3_6_6_0  5_5_5_0  detached  format_version.txt

可以看到,生成了 3 个数据目录,每个目录在 ClickHouse 里称作一个分区(part),目录名的前缀正是我们写入时字段 a 的值: 1,3,5,因为表分区是这样定位的:PARTITION BY a

现在我们看看 a=3 分区:

ls ckdatas/data/default/mt/3_6_6_0/a.bin  a.mrk2  b.bin  b.mrk2  c.bin  checksums.txt  c.mrk2  columns.txt  count.txt  minmax_a.idx  partition.dat  primary.idx  skp_idx_idx_c.idx  skp_idx_idx_c.mrk2
  • *.bin 是列数据文件,按主键排序(ORDER BY),这里是按照字段 b 进行排序

  • *.mrk2 mark 文件,目的是快速定位 bin 文件数据位置

  • minmax_a.idx 分区键 min-max 索引文件,目的是加速分区键 a 查找

  • primay.idx 主键索引文件,目的是加速主键 b 查找

  • skp_idx_idx_c.* 字段 c 索引文件,目的是加速 c 的查找

在磁盘上,MergeTree 只有一种物理排序,就是 ORDER BY 的主键序,其他文件(比如 .mrk/.idx)是一种逻辑加速,围绕仅有的一份物理排序,要解决的问题是:

在以字段 b 物理排序上,如何实现字段 a、字段 c 的快速查找?

MergeTree 引擎概括起来很简单:整个数据集通过分区字段被划分为多个物理分区,每个分区內又通过逻辑文件围绕仅有的一种物理排序进行加速查找。

存储结构

数据文件

对于单个物理分区內的存储结构,首先要明确一点,MergeTree 的数据只有一份:*.bin。

a.bin 是字段 a 的数据,b.bin 是字段 b 的数据,c.bin 是字段 c 的数据,也就是大家熟悉的列存储。

各个 bin 文件以 b.bin排序对齐(b 是排序键),如图:

ClickHouse和他的朋友们(6)MergeTree存储结构

这样会有一个比较严重的问题:如果 *.bin 文件较大,即使读取一行数据,也要加载整个 bin 文件,浪费了大量的 IO,没法忍。

granule

高、黑科技来了,ClickHouse MergeTree 把 bin 文件根据颗粒度(GRANULARITY)划分为多个颗粒(granule),每个 granule 单独压缩存储。

SETTINGS index_granularity=3 表示每 3 行数据为一个 granule,分区目前只有 7 条数据,所以被划分成 3 个 granule(三个色块):

ClickHouse和他的朋友们(6)MergeTree存储结构

为方便读取某个 granule,使用 *.mrk 文件记录每个 granule 的 offset,每个 granule 的 header 里会记录一些元信息,用于读取解析:

ClickHouse和他的朋友们(6)MergeTree存储结构

这样,我们就可以根据 mark 文件,直接定位到想要的 granule,然后对这个单独的 granule 进行读取、校验。

目前,我们还有缺少一种映射:每个 mark 与字段值之间的对应,哪些值区间落在 mark0,哪些落在 mark1 ...?

有了这个映射,就可以实现最小化读取 granule 来加速查询:

  1. 根据查询条件确定需要哪些 mark

  2. 根据 mark 读取相应的 granule

存储排序

在了解 MergeTree 索引机制之前,需要明白以下两点:

  1. 只有一份全量数据,存储在 *.bin 文件

  2. *.bin 按照 ORDER BY 字段降序存储

  3. ClickHouse和他的朋友们(6)MergeTree存储结构

稀疏索引

因为数据只有一份且只有一种物理排序,MergeTree在索引设计上选择了简单、高效的稀疏索引模式。

什么是稀疏索引呢?就是从已经排序的全量数据里,间隔性的选取一些点,并记录这些点属于哪个 mark。

1. primary index

主键索引,可通过[PRIMARY KEY expr]指定,默认是 ORDER BY 字段值。

注意 ClickHouse primary index 跟 MySQL primary key 不是一个概念。

在稀疏点的选择上,取每个 granule 最小值:

ClickHouse和他的朋友们(6)MergeTree存储结构

2. skipping index

普通索引。

INDEXidx_c(c) TYPE minmax GRANULARITY 1 针对字段 c 创建一个 minmax 模式索引。

GRANULARITY 是稀疏点选择上的 granule 颗粒度,GRANULARITY 1 表示每 1 个 granule 选取一个:

ClickHouse和他的朋友们(6)MergeTree存储结构

如果定义为GRANULARITY 2 ,则 2 个 granule 选取一个:

ClickHouse和他的朋友们(6)MergeTree存储结构

3. partition minmax index

针对分区键,MergeTree 还会创建一个 min/max 索引,来加速分区选择。

ClickHouse和他的朋友们(6)MergeTree存储结构

4. 全景图

ClickHouse和他的朋友们(6)MergeTree存储结构

查询优化

现在熟悉了 MergeTree 的存储结构,我们通过几个查询来体验下。

1. 分区键查询

语句:

select * from default.mt where a=3

查询会直接根据 a=3 定位到单个分区:

<Debug> InterpreterSelectQuery: MergeTreeWhereOptimizer: condition "a = 3" moved to PREWHERE<Debug> default.mt (SelectExecutor): Key condition: unknown<Debug> default.mt (SelectExecutor): MinMax index condition: (column 0 in [3, 3])<Debug> default.mt (SelectExecutor): Selected 1 parts by a, 1 parts by key, 3 marks by primary key, 3 marks to read from 1 ranges┌─a─┬──b─┬──c─┐│ 3 │  4 │ 10 ││ 3 │  5 │  9 ││ 3 │  6 │  8 ││ 3 │  7 │  7 ││ 3 │  8 │  6 ││ 3 │  9 │  5 ││ 3 │ 10 │  4 │└───┴────┴────┘

2. 主键索引查询

语句:

select * from default.mt where b=5

查询会先从 3 个分区读取 prmary.idx,然后定位到只有一个分区符合条件,找到要读取的 mark:

<Debug> default.mt (SelectExecutor): Key condition: (column 0 in [5, 5])<Debug> default.mt (SelectExecutor): MinMax index condition: unknown<Debug> default.mt (SelectExecutor): Selected 3 parts by a, 1 parts by key, 1 marks by primary key, 1 marks to read from 1 ranges┌─a─┬─b─┬─c─┐│ 3 │ 5 │ 9 │└───┴───┴───┘

3. 索引查询

语句:

select * from default.mt where b=5

查询会先从 3 个分区读取 prmary.idx 和 skp_idx_idx_c.idx 进行 granule 过滤(没用的 drop 掉),然后定位到只有 3_x_x_x 分区的一个 granule 符合条件:

<Debug> InterpreterSelectQuery: MergeTreeWhereOptimizer: condition "c = 5" moved to PREWHERE<Debug> default.mt (SelectExecutor): Key condition: unknown<Debug> default.mt (SelectExecutor): MinMax index condition: unknown<Debug> default.mt (SelectExecutor): Index `idx_c` has dropped 1 / 1 granules.<Debug> default.mt (SelectExecutor): Index `idx_c` has dropped 1 / 1 granules.<Debug> default.mt (SelectExecutor): Index `idx_c` has dropped 2 / 3 granules.<Debug> default.mt (SelectExecutor): Selected 3 parts by a, 1 parts by key, 5 marks by primary key, 1 marks to read from 1 ranges┌─a─┬─b─┬─c─┐│ 3 │ 9 │ 5 │└───┴───┴───┘

总结

本文从磁盘存储结构入手,分析 ClickHouse MergeTree 的存储、索引设计。

只有了解了这些底层机制,我们才好对自己的 SQL 和表结构进行优化,使其执行更加高效。

ClickHouse MergeTree 设计简单、高效,它首要解决的问题是:在一种物理排序上,如何实现快速查找。

针对这个问题,ClickHouse使用稀疏索引来解决。

在官方 roadmap 上,列举了一个有意思的索引方向:Z-Order Indexing,目的是把多个维度编码到一维存储,当我们给出多维度条件的时候,可以快速定位到这个条件点集的空间位置,目前 ClickHouse 针对这个索引设计暂无进展。

延伸阅读

全文完。

Enjoy ClickHouse :)

叶老师的「MySQL核心优化」大课已升级到MySQL 8.0,扫码开启MySQL 8.0修行之旅吧

ClickHouse和他的朋友们(6)MergeTree存储结构

本文分享自微信公众号 - 老叶茶馆(iMySQL_WX)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这