一:导论
最近认真研究了下clickhouse的索引,和兄弟们分享下我的一些小心得。
索引就如同我们的字典或者书的目录一样,就是为了让我们快速找到我们想要的数据,索引索引,就是一个引路人嘛,引领我们快速到达目的地。所以我们clickhouse 中的索引也是这样的作用。
那么,clickhouse 中的索引具体是啥样的呢?有什么分类呢?该怎么使用呢?
二:索引
2.1: 一级索引定义
clickhouse中的索引叫做index,一般我们是通过在创建表的时候指定主键来确定索引,所以现在我们就以一张表来贯穿这个索引的讲述过程,如下所示。
CREATE TABLE hits_UserID_URL
(
UserID
UInt32,
URL
String,
EventTime
DateTime
)
ENGINE = MergeTree
PRIMARY KEY (UserID, URL)
ORDER BY (UserID, URL, EventTime);
这个表中的主键是UserID和URL,我们显示的定义了,然后这个order by 就是定义了联合排序健,在磁盘中按照这样的顺序来确定数据的排列数据,如果没有定义显示的primary key ,那么order by 定义的健就会被称默认为主键,按照上述的主键定义,我们的系统中就会生成一个primary.idx ,就是一个索引文件,如下所示:
这个索引文件是如何生成的呢?首先我们在建表的过程中会有一个值叫做索引粒度,类似于一个标尺的作用,叫做index_granularity, 它的值一般是8192,意思就是每扫描8192行我们就会生成一行索引,然后这个索引数据就是我们表中定义的那个主键UserId和URL,比如索引文件的第一行数据就是我们按照UserID, URL, EventTime升序排列得到的第一行的UserID和URL的值,而第二行数据就是第8193行的UserID和URL的值,依此类推,所以总的索引条数n=total_rows/index_granularty, 向上取整。
2.2: 一级索引查询使用详细逻辑
如上所述,定义了主键并且生成了索引文件之后,我们的查询是如何使用这个索引加快查询的呢?如下所示的这个查询
SELECT URL, count(URL) AS Count
FROM hits_UserID_URL
WHERE UserID = '749.927.693'
GROUP BY URL
ORDER BY Count DESC
LIMIT 10;
当我们的clickhouse在解析这个sql语句的时候,where的这个查询条件命中了我们的主键UserID,所以查询引擎就会走索引,它就会去把primary.idx 文件加载到内存中,然后使用二分法查找,对这个UserID进行比较,最终可以定位得到这个条件的UserID是位于如下的mark176的位置中,
那么得到了这个信息之后,我们如何去获取相应的数据呢,这就是标记文件的作用了,如下所示,我们有了索引之后会对每个列生成一个mark文件,
这个mark文件有两列,第一列是被压缩的数据块的位置,第二个是这个数据颗粒176在解压的数据块里面的位置。可能同学会有疑问,直接有第一个文件的位置不就行了嘛,反正一般这个block 就是对应一个granularity(8192行)数据的压缩块,何必多次一举呢?其实不然,这就要涉及到如下clickhouse的压缩逻辑了。
2.2.1: clickhouse 的数据压缩
在我们往clickhouse 中写入数据的时候,clickhouse一般是每隔index_granularity(默认8192)行生成一条索引记录,同理,这边也是每隔index_granularity 行去决定压缩数据,但是这个数据是否压缩还取决于一个值,就是65536(64kb),假设每个批次(8192行)的数据大小为size,则压缩逻辑就取决于以下的逻辑,也可以见下图:
- 如果size< 64kb, 那么就会等下一批次数据过来,直到累积到64kb就生成一个压缩的数据块,这个压缩的数据块中包含两个颗粒度的数据
- 如果 64kb<size<1MB,则就直接生成一个压缩的数据块,这个数据块里面包含一个颗粒度的数据
- 如果size>1MB,则一部分会压缩成一个数据块,另一部分放到另一个压缩的数据块里面,所以一个颗粒度也就是一批次的数据会被分配到两个数据块甚至多个数据块中
接着回到上文,当我们找到mark位置之后,就如上文的mark 176,我们就要去根据这个block_offset 和granule_offset 分别去UserID.bin 文件和URL.bin 文件把目的数据取出来加载到内存中,然后互相匹配拿到目的数据展现出来。以UserID 为例,假如这个原先的176的grandule 的整体size<64kB, 则这个grandule的数据就会和其他grandule被压缩成一个block,假设放在169的block中当中,则这个block_offset 就是169,这样我们就可以定位到被压缩的数据块,而对应的这个grandule_offset就是176,这就是这个压缩的数据块压缩后这个grandule 的位置,如下所示:
当这个UserID的数据被加载到内存后,根据对应的row_number 和URL的数据row_number 对齐之后,根据条件进行聚合过滤就得到了我们上述查询所需要的结果了。