2021年7月1日星期四

ClickHouse源码笔记6:探究列式存储系统的排序

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

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

1.执行计划

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

select * from test order by k1;

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

这里分为了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的排序了。
生成Perm

如上图所示,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(bl......

原文转载:http://www.shaoqun.com/a/839212.html

跨境电商:https://www.ikjzd.com/

yiqu:https://www.ikjzd.com/w/210

预售:https://www.ikjzd.com/w/889

王惟:https://www.ikjzd.com/w/1744


分析完成了聚合以及向量化过滤,向量化的函数计算之后。本篇,笔者将分析数据库的一个重要算子:排序。让我们从源码的角度来剖析ClickHouse作为列式存储系统是如何实现排序的。本系列文章的源码分析基于ClickHousev19.16.2.2的版本。1.执行计划老规矩,咱们还是先从一个简单的查询出发,通过一步步的通过执行计划按图索骥ClickHouse的执行逻辑。select*fromtestorde
笨鸟转运:https://www.ikjzd.com/w/1550
免费工具推荐:独立站如何做好谷歌SEO排名?:https://www.ikjzd.com/articles/106947
2019亚马逊Q4即将来临,卖家要做好这些准备!:https://www.ikjzd.com/articles/106949
旺季Wish小卖家如何强势逆袭,瓜分平台流量?:https://www.ikjzd.com/articles/106950
网易考拉PC端和APP均正式更名"考拉海购"!:https://www.ikjzd.com/articles/106951
办公室里 男上司撕扯我的衣服:http://lady.shaoqun.com/a/106145.html
老外一个接一个的上 又粗又长我被老外玩晕了:http://lady.shaoqun.com/m/a/247267.html
我被外国黑人3p过程 黑人巨大两根一起挤进:http://lady.shaoqun.com/m/a/247941.html
亚马逊商家只会卖货不赚钱:https://www.ikjzd.com/articles/146274
520、睡前三个超级甜的故事哄女朋友:http://lady.shaoqun.com/a/394662.html
不要轻举妄动:一时冲动会发生男女性交:http://lady.shaoqun.com/a/394663.html
WISH宣布重新开放旧金山总部,提供灵活的工作选择:https://www.ikjzd.com/articles/146288

没有评论:

发表评论