这是我参与「第五届青训营 」伴学笔记创作活动的第 14 天
索引设计
hashIndex
不适合做范围查询
B-tree
每个节点都保存数据
B+tree
中间节点只保存索引,不保存实际数据,这样中间节点可以保存更多的值,这样大数据量时可以尽量降低树的高度
clickhouse不使用常规索引原因
向b树都是排序树,大数据量写入,还要排序,速度没有直接写入磁盘快
Lsm Tree
SSTable
是磁盘存储的文件,在磁盘存储以segment为粒度存储,每个segment内部数据是有序的,不同segment里面无法保证有序性
segment唯一修改的方式就是重新写入一个新的segment然后把原先的segment删除掉
MemTable
如果大量数据写入就要频繁访问磁盘,效率就不高,因此先写入内存,当内存数据达到一定程度再批量写入磁盘
数据查询
合并
当segment数量太多时,索引的数量也就增加,为了提高数据访问效率因此会对segment进行合并,当合并后segment中有序数量也就会增多,可以提高压缩效率,也就保证读取更少的数据
具体实现
- 主键索引
CREATE TABLE hits_UserID_URL
(
`UserID` UInt32,
`URL` String,
`EventTime` DateTime
)
ENGINE = MergeTree
PRIMARY KEY (UserID, URL)
ORDER BY (UserID, URL, EventTime)
SETTINGS index_granularity = 8192, index_granularity_bytes = 0;
-
数据按照主键顺序一次排序
- UserID首先做排序,
- 然后是URL,
- 最后是EventTime
-
数据被组织成granule
- granule是引擎做数据处理的最小数据单位,引擎读数据的时候不是按照一行一行读取的,而是最少读取一个granule
- 方便构建稀疏索引
- 方便并行计算
每个granule都对应primary.idx里面的一行
- 默认每8192行记录主键的一行值,primary.idx需要被全部加载到内存里面
含义就是每8192行建立一条索引,索引会全部加载到内存
- 每个主键的一行数据被称为一个mark
-
每个列都有这样一个mark文件,mark文件存储所有granule在物理文件里面的地址,每一列都有一个mark文件
-
mark文件里面的每一行存储两个地址
- 第一个地址称为block_offset,用于定位一个granule的压缩数据在物理文件中的位置,压缩数据会以一个block为单位解压到内存中。
- 第二个地址称为granule_offset,用于定位一个granule在解压之后的block中的位置。
分两个地址原因是,granule只是我们的查询单元而不是最小写入单元,实际上可能多个granule压缩为一个block写入磁盘
索引的缺陷
- 缺陷:数据按照key的顺序做排序,因此只有第一个key的过滤效果好,后面的key过滤效果依赖第一个key的基数大小(假设第一个key大多数相同,那么第二个key则基本都是有序的,否则第一个key大多不同,也就意味着排序以第一个key为准,而第二个key无序,查询效果差)
索引的优化
尝试建立二级索引
- 在URL列上构建二级索引
构建多个主键索引
- 再建一个表(数据需要同步两份,查询需要用户判断查哪张表)
建物化视图
- 建一个物化视图(数据自动同步到隐式表,查询需要用户判断查哪张表)
使用Projection
- 使用Projection(数据自动同步到隐式表,查询自动路由到最优的表)
总结
- 主键包含的数据顺序写入
- 主键构造一个主键索引
- 每个列构建一个稀疏索引
- 通过mark的选择让主键索可以定位到每一列的索
- 可以通过多种手段优化非主键列的索引
数据合并
合并后数据更大范围是有序的,查找时可以查找更少的part,提高压缩率,减少索引数量
-
数据的可见性
数据合并过程中,未被合并的数据对查询可见
数据合并完成后,新part可见,被合并的part被标记删除
- part的合并发生在一个分区内
此时可以看到1_6表示原先1-6的part进行了合并,合并次数为1次
数据查询
- 对于查询
SELECT
URL,
count(URL) AS Count
FROM hits_UserID_URL
WHERE UserID = 749927693
GROUP BY URL
ORDER BY Count DESC
LIMIT 10
- 通过主键找到需要读的mark
- 切分marks,然后并发的调度reader
- Reader 通过mark block_offset得到需要读的数据文件的偏移量
- Reader 通过mark granule_offset得到解压之后数据的偏移量
- 构建列式filter做数据过滤