分析完成了聚合以及向量化过滤,向量化的函数计算之后。本篇,笔者将分析数据库的一个重要算子:排序。让我们从源码的角度来剖析ClickHouse作为列式存储系统是如何实现排序的。

本系列文章的源码分析基于ClickHouse v19.16.2.2的版本。

1.执行计划

老规矩,咱们还是先从一个简单的查询出发,通过一步步的通过执行计划按图索骥ClickHouse的执行逻辑。

select * from test order by k1;

咱们先尝试打开ClickHouse的Debug日志看一下具体的执行的pipeline

这里分为了5个流,而咱们所需要关注的流已经呼之欲出了MergeSortingPartialSorting,ClickHouse先从存储引擎的数据读取数据,并且执行函数运算,并对数据先进行部分的排序,然后对于已经有序的数据在进行MergeSort,得出最终有序的数据。

2. 实现流程的梳理

那咱们接下来要梳理的代码也很明确了,就是PartialSortingBlockInputStreamMergingSortedBlockInputStream

  • PartialSortingBlockInputStream的实现

    PartialSortingBlockInputStream的实现很简单,咱们直接看代码吧:
Block PartialSortingBlockInputStream::readImpl()
{
Block res = children.back()->read();
sortBlock(res, description, limit);
return res;
}

它从底层的流读取数据Block,Block可以理解为Doris之中的Batch,相当一批行的数据,然后根据自身的成员变量SortDescription来对单个Block进行排序,并根据limit进行长度截断。

SortDescription是一个vector,每个成员描述了单个排序列的排序规则。比如

: null值的排序规则,是否进行逆序排序等。

/// Description of the sorting rule for several columns.
using SortDescription = std::vector<SortColumnDescription>;
  • sortBlock的函数实现

接下来,我们来看看sortBlock函数的实现,看看列式的执行系统是如何利用上述信息进行数据排序的。

void sortBlock(Block & block, const SortDescription & description, UInt64 limit)
{
/// If only one column to sort by
if (description.size() == 1)
{
bool reverse = description[0].direction == -1; const IColumn * column = !description[0].column_name.empty()
? block.getByName(description[0].column_name).column.get()
: block.safeGetByPosition(description[0].column_number).column.get(); IColumn::Permutation perm;
if (needCollation(column, description[0]))
{
const ColumnString & column_string = typeid_cast<const ColumnString &>(*column);
column_string.getPermutationWithCollation(*description[0].collator, reverse, limit, perm);
}
else
column->getPermutation(reverse, limit, description[0].nulls_direction, perm); size_t columns = block.columns();
for (size_t i = 0; i < columns; ++i)
block.getByPosition(i).column = block.getByPosition(i).column->permute(perm, limit);
}

这里需要分为两种情况讨论:1. 单列排序。2.多列排序。多列排序与单列的实现大同小异,所以我们先从单列排序的代码开始庖丁解牛。它的核心代码就是下面的这四行:

    column->getPermutation(reverse, limit, description[0].nulls_direction, perm);
size_t columns = block.columns();
for (size_t i = 0; i < columns; ++i)
block.getByPosition(i).column = block.getByPosition(i).column->permute(perm, limit);

先通过单列排序,拿到每一列在排序之后的IColumn::Permutation perm;。然后Block之中的每一列都利用这个perm, 生成一个新的排序列,替换旧的列之后,就完成Block的排序了。

如上图所示,Permutation是一个长度为limitPodArray, 它标识了根据排序列排序之后的排序位置。后续就按照这个perm规则利用函数permute生成新的列,就是排序已经完成的列了。

ColumnPtr ColumnVector<T>::permute(const IColumn::Permutation & perm, size_t limit) const
{
typename Self::Container & res_data = res->getData();
for (size_t i = 0; i < limit; ++i)
res_data[i] = data[perm[i]]; return res;
}

这里细心的朋友会发现,String列在sortBlock函数之中做了一些额外的判断

  if (needCollation(column, description[0])) {
const ColumnString & column_string = typeid_cast<const ColumnString &>(*column);
column_string.getPermutationWithCollation(*description[0].collator, reverse, limit, perm);
}

这部分是一个特殊的字符串生成perm的逻辑,ClickHouse支持用不同的编码进行字符串列的排序。比如通过GBK编码进行排序的话,那么中文的排序顺序将是基于拼音顺序的。

  • getPermutation的实现

    所以,在ClickHouse的排序过程之中。getPermutation是整个排序算子实现的重中之重, 它是Column类的一个虚函数,也就是说每一个不同的数据类型的列都可以实现自己的排序逻辑。我们通过ColumnVector的实现,来管中规豹一把。
template <typename T>
void ColumnVector<T>::getPermutation(bool reverse, size_t limit, int nan_direction_hint, IColumn::Permutation & res) const
{
if (reverse)
std::partial_sort(res.begin(), res.begin() + limit, res.end(), greater(*this, nan_direction_hint));
else
std::partial_sort(res.begin(), res.begin() + limit, res.end(), less(*this, nan_direction_hint));
}
else
{
/// A case for radix sort
if constexpr (std::is_arithmetic_v<T> && !std::is_same_v<T, UInt128>)
{
return;
}
} /// Default sorting algorithm.
for (size_t i = 0; i < s; ++i)
res[i] = i; pdqsort(res.begin(), res.end(), less(*this, nan_direction_hint));
}
}

这部分代码较多,笔者简化了一下这部分的逻辑。

  • 如果存在limit条件,并且列的长度大于limit,采用std::partial_sort进行perm的排序。
  • 如果为数字类型,并且不为UInt128类型时,则采用Radix Sort计数排序来对perm进行排序。
  • 如不满足前二者的条件,则使用快速排序作为最终的默认实现。

好的,看到这里。已经完整的梳理了PartialSortingBlockInputStream,得到了每一个输出的Block已经按照我们的排序规则进行排序了。接下来就要请出MergeSortingBlockInputStream来进行最终的排序工作。

  • MergeSortingBlockInputStream的实现

    从名字上也能看出来,这里需要完成一次归并排序,来得到最终有序的排序结果。至于排序的对象,自然上面通过PartialSortingBlockInputStream输出的Block了。

直接定位到readImpl()的实现,ClickHouse这里实现了Spill to disk的外部排序逻辑,这里为了简化,笔者先暂时拿掉这部分外部排序的逻辑。

Block MergeSortingBlockInputStream::readImpl()
{
/** Algorithm:
* - read to memory blocks from source stream;
*/ /// If has not read source blocks.
if (!impl)
{
while (Block block = children.back()->read())
{
blocks.push_back(block);
sum_rows_in_blocks += block.rows();
sum_bytes_in_blocks += block.allocatedBytes(); /** If significant amount of data was accumulated, perform preliminary merging step.
*/
if (blocks.size() > 1
&& limit
&& limit * 2 < sum_rows_in_blocks /// 2 is just a guess.
&& remerge_is_useful
&& max_bytes_before_remerge
&& sum_bytes_in_blocks > max_bytes_before_remerge)
{
remerge();
} if ((blocks.empty() && temporary_files.empty()) || isCancelledOrThrowIfKilled())
return Block(); if (temporary_files.empty())
{
impl = std::make_unique<MergeSortingBlocksBlockInputStream>(blocks, description, max_merged_block_size, limit);
} Block res = impl->read();
return res;
}

由上面代码可以看到,MergeSortingBlockInputStream这部分就是不断从底层的PartialSortingBlockInputStream读取出来,并存储全部存储下来。最终读取完成之后,利用MergeSortingBlocksBlockInputStream类,完成所有Blocks的归并排序工作。而MergeSortingBlocksBlockInputStream类就是简单完成利用堆进行多路归并排序的过程代码,笔者在这里就不再展开了,感兴趣的同学可以自行参考MergeSortingBlockInputStream.cpp部分的实现。

3.要点梳理

第二小节梳理完ClickHouse的排序算子的实现流程,这里进行一些简单的要点小结:

  1. ClickHouse的排序实现需要利用排序列生成对应的perm,最终利用perm完成每一个Block的排序。

  2. 所以每一个不同数据类型的列,都需要实现getPermutationpermute来实现排序。并且可以根据数据类型,选择不同的排序实现。比如radix sort的时间复杂度为O(n),相对快速排序的时间复杂度就存在了明显的优势。

  3. 排序算法存在大量的数据依赖,所以是很难发挥SIMD的优势的。只有在radix sort下才些微有些部分可以向量化,所以相对于非向量化的实现,不存在太多性能上的优势。

4. 小结

OK,到此为止,咱们可以从Clickhouse的源码实现之中梳理完成列式的存储系统是如何实现排序的。

当然,这部分跳过了一部分重要的实现:Spill to disk。这个是确保在一定的内存限制之下,对海量数据进行排序时,可以利用磁盘来缓存排序的中间结果。这部分的实现也很有意思,感兴趣的朋友,可以进一步展开来看这部分的实现。

笔者是一个ClickHouse的初学者,对ClickHouse有兴趣的同学,欢迎多多指教,交流。

5. 参考资料

官方文档

ClickHouse源代码

ClickHouse源码笔记6:探究列式存储系统的排序的更多相关文章

  1. ClickHouse源码笔记4:FilterBlockInputStream, 探寻where,having的实现

    书接上文,本篇继续分享ClickHouse源码中一个重要的流,FilterBlockInputStream的实现,重点在于分析Clickhouse是如何在执行引擎实现向量化的Filter操作符,而利用 ...

  2. ClickHouse源码笔记5:聚合函数的源码再梳理

    笔者在源码笔记1之中分析过ClickHouse的聚合函数的实现,但是对于各个接口函数的实际如何共同工作的源码,回头看并没有那么明晰,主要原因是没有结合Aggregator的类来一起分析聚合函数的是如果 ...

  3. ClickHouse源码笔记3:函数调用的向量化实现

    分享一下笔者研读ClickHouse源码时分析函数调用的实现,重点在于分析Clickhouse查询层实现的接口,以及Clickhouse是如何利用这些接口更好的实现向量化的.本文的源码分析基于Clic ...

  4. ClickHouse源码笔记1:聚合函数的实现

    由于工作的需求,后续笔者工作需要和开源的OLAP数据库ClickHouse打交道.ClickHouse是Yandex在2016年6月15日开源了一个分析型数据库,以强悍的单机处理能力被称道. 笔者在实 ...

  5. ClickHouse源码笔记2:聚合流程的实现

    上篇笔记讲到了聚合函数的实现并且带大家看了聚合函数是如何注册到ClickHouse之中的并被调用使用的.这篇笔记,笔者会续上上篇的内容,将剖析一把ClickHouse聚合流程的整体实现. 第二篇文章, ...

  6. clickhouse源码Redhat系列机单机版安装踩坑笔记

    前情概要 由于工作需要用到clickhouse, 这里暂不介绍概念,应用场景,谷歌,百度一大把. 将安装过程踩下的坑记录下来备用 ClickHouse源码 git clone安装(直接下载源码包安装失 ...

  7. Alink漫谈(十八) :源码解析 之 多列字符串编码MultiStringIndexer

    Alink漫谈(十八) :源码解析 之 多列字符串编码MultiStringIndexer 目录 Alink漫谈(十八) :源码解析 之 多列字符串编码MultiStringIndexer 0x00 ...

  8. 厉害!这份阿里面试官 甩出的Spring源码笔记,GitHub上已经爆火

    前言 时至今日,Spring 在 Java 生态系统与就业市场上,面试出镜率之高,投产规模之广,无出其右.随着技术的发展,Spring 从往日的 IoC 框架,已发展成 Cloud Native 基础 ...

  9. Zepto源码笔记(一)

    最近在研究Zepto的源码,这是第一篇分析,欢迎大家继续关注,第一次写源码笔记,希望大家多指点指点,第一篇文章由于首次分析原因不会有太多干货,希望后面的文章能成为各位大大心目中的干货. Zepto是一 ...

随机推荐

  1. OOP第三章博客

    OO第三单元博客 • (1)梳理JML语言的理论基础.应用工具链情况: 理论基础: 网络资料上面介绍JML有两种主要的用法: 开展规格化设计.这样交给代码实现人员的将不是可能带有内在模糊性.二义性的自 ...

  2. 进程间IPC通信-stop waiting for thing to happen,go out and make them happen!!!

    进程间通信: System V IPC对象: ipcs -q:查看消息队列   ipcs -m:查看共享内存 ipcs -s:查看信号灯集 ipcrm -q:删除消息队列   ipcrm -m:删除共 ...

  3. golang:协程安全

    多路复用 Go语言中提供了一个关键字select,通过select可以监听channel上的数据流动.select的用法与switch语法类似,由select开始一个新的选择块,每个选择条件由case ...

  4. 029. Python多态介绍

    多态:不同的子类对象,调用相同的父类方法,产生不同的结果 继承 重写 在不改变原有代码的前提下,实现了不同的效果 class Soldier(): # 攻击 def attack(self): pas ...

  5. JavaEE 三层架构模式

    什么是架构模式 要理解三层架构模式,我们得先搞清楚什么是架构模式.(这里说的架构模式是针对后端开发) 所谓架构就是系统最高级别的设计,一个系统特别复杂时才需要架构设计,如果只是开发一个很小程序,就谈不 ...

  6. cp1 项目管理概述

    项目不成功:问题很多 chapter__1 ① 任务不明确 ② 变更 ③ 新技术 ④ 成本 ⑤ 进度 ⑥ 质量问题 ⑦ 开发混乱 ⑧ 用户 项目定义 项目 (Project) 是为了创造一个唯一的产品 ...

  7. IDEA 配置 Tomcat(详细)(Day_12)

    如果这世界上真有奇迹,那只是努力的另一个名字.生命中最难的阶段,不是没有人懂你,而是你不懂你自己. 运行环境 AND 版本 JDK8 + IntelliJ IDEA 2018.3  +   Tomca ...

  8. 路由信息相关 route 网卡

    目录 route命令 1.查看路由表 2.管理路由 基本网络配置 添加网卡地址 修改网卡UUID route命令 路由表管理命令,路由表主要构成: Destination: 目标网络ID,表示可以到达 ...

  9. [leetcode] 90. 子集 II.md

    90. 子集 II 78. 子集题的扩展,其中的元素可能会出现重复了 我们仍沿用78题的代码,稍作改动即可: 此时需要对nums先排个序,方便我们后面跳过选取相同的子集. 跳过选取相同的子集.当选取完 ...

  10. Django(47)drf请求生命周期分析

    前言   一般我们写完序列化以后,我们就会开始写视图了,drf中我们一般使用CBV的方式,也就是类视图的方式,最基础的我们会使用from rest_framework.views import API ...