ClickHouse要点笔记

存储

ck使用稀疏索引,索引所占空间较小。一般每8192行一个索引。

这个单元的单位被称之为“Granule”,是数据按行划分时用到的逻辑概念。关于多少行是一个Granule这个问题,在老版本中这是用参数index_granularity设定的一个常量,也就是每隔确定行就是一个Granule。在当前版本中有另一个参数index_granularity_bytes会影响Granule的行数,它的意义是让每个Granule中所有列的sum size尽量不要超过设定值。老版本中的定长Granule设定主要的问题是MergeTree中的数据是按Granule粒度进行索引的,这种粗糙的索引粒度在分析超级大宽表的场景中,从存储读取的data size会膨胀得非常厉害,需要用户非常谨慎得设定参数。

索引文件是primary.idx,索引到数据的桥梁是col.mrk2文件,数据是col.bin文件。显然除了索引文件是每个分区一个,mrk2和bin都是每列一个文件。我们看一个例子:

3c6e5f037bba12d44ac16f33af2352dd.png

date和city两列是主键,在primary.idx文件中,按date+city排序所有数据,每隔8192行,pick中这行的date+city值记录下来,这行的下标(图中的Mark M值)乘以8192就是行号。primary.idx文件非常小,可以常驻内存。date、city、action三列都各自有自己的mrk2和bin文件。

结合下面两图:
2f00a95f8edd540d8f70c3092e726dd9.png

f83d5201e420552a174e64da7607c1a1.png

比如where条件中有id=12 AND Name=Herman,则通过primary.idx中二分查找确定下标就是1(Block集合,也就是mark number集合),在Id.mrk2(其它非索引列的col.mrk2也是同理)中通过下标知道两个offset,于是在Id.bin中就seek到6的位置,然后继续二分查找很快找到12(如果是非索引列,则顺序查找不超过一个Block的大小也能找到)。

如果SELECT中有非索引列,则在前述步骤确认行号集合后(多索引列条件需要对行号集合求交集),再拿着行号集合到非索引列的mrk2文件找两个offset,再到其bin文件中取对应行号的数据。

导入数据时每批可以生成多个parts,每个part最多1048576行。ClickHouse保证part级别原子生效(但不保证parts级别)。

计算

在executeQueryImpl函数中,返回的BlockIO对象来自于interpreter->execute()调用,interpreter有针对不同类型SQL语句的各种不同实现,比如一般query对应的InterpreterSelectQuery实现类。它的execute函数中,是如下代码:

    BlockIO res;
    QueryPlan query_plan;

    buildQueryPlan(query_plan);

    res.pipeline = QueryPipelineBuilder::getPipeline(std::move(*query_plan.buildQueryPipeline(
        QueryPlanOptimizationSettings::fromContext(context), BuildQueryPipelineSettings::fromContext(context))));
    return res;

通过调用buildQueryPipeline生成queryPipeline。然后QueryPipelineBuilder::getPipeline调用就让代码流程从Interpreter文件夹转移到了QueryPipeline文件夹。其函数内容为:

    QueryPipeline res(std::move(builder.pipe));
    res.setNumThreads(builder.getNumThreads());
    res.setProcessListElement(builder.process_list_element);
    return res;

基本执行流程为AST -> Step (Node) -> Pipeline (Processor) -> graph -> 调度线程。

AST的prefilter阶段,通过调用move_condition将部分列条件下推给存储,默认不超过10%。

Step阶段生成step数组,比如生成包含scan和filter两个成员的数组。

依次调用step数组的updatePipeline函数。

Processor(对应与IProcessor类)相当于Operator,有NeedData、PortFull等多种状态。一般线程池的个数等于CPU的个数。每个线程从Processor的池子中拿出一个执行,输入和输出均称为Trunk。Trunk包含多个column,每个column的内部实现是ColumnVector。

Processor代码注释中有其自身功能的详细解读。

数据exchange的接口是Port,具体实现有InputPort和OutputPort等类。

ExecutingGraph类基于Processor生成表达其上下游关系的图。

PullingPipelineExecutor为单线程,PullingAsyncPipelineExecutor为多线程,都负责Pulling executor for QueryPipeline。它们都包含了QueryPipeline类型的成员,并且是构造函数的唯一参数类型。

PullingAsyncPipelineExecutor::pull函数是驱动执行的环节。它调用了threadFunction,进而调用了data.executor->execute(num_threads),进而调用PipelineExecutor的executeImpl,进而调用线程个数次PipelineExecutor的executeSingleThread,进而调用PipelineExecutor的executeStepImpl。它来真正执行一个step。

Pulling(Async)PipelineExecutor在调用其pull函数时构造新的PipelineExecutor:

data->executor = std::make_shared<PipelineExecutor>(pipeline.processors, pipeline.process_list_element);

比如看一下MergeJoin::mergeInMemoryRightBlocks函数,其内容为:

auto pipeline = QueryPipelineBuilder::getPipeline(std::move(builder));
PullingPipelineExecutor executor(pipeline);
Block block;
while (executor.pull(block))

可以看到基于QueryPipeline构造了Pulling(Async)PipelineExecutor来调用其pull。

线程可以抢Processor来干活,直到消化完Processor所以不容易有长尾。

跨节点数据exchange的Processor称为OutputFormat,一定是单线程的。

据说ClickHouse的llvm的codegen默认不打开,较少使用。另外,开窗函数和不相关子查询可能在规划中或刚刚实现。

TODO

查询总入口、执行计划与优化器

MySQLHandler和其它接口的Handler中会调用executeQuery函数,该函数再调用静态函数executeQueryImpl函数对一个query进行parse、优化、执行,最终返回std::tuple<ASTPtr, BlockIO>。返回的BlockIO也就是一个stream,可以从中读取query返回的数据:

BlockIO executeQuery(
    const String & query,
    ContextMutablePtr context,
    bool internal,
    QueryProcessingStage::Enum stage)
{
    ASTPtr ast;
    BlockIO streams;
    std::tie(ast, streams) = executeQueryImpl(query.data(), query.data() + query.size(), context, internal, stage, nullptr);

    if (const auto * ast_query_with_output = dynamic_cast<const ASTQueryWithOutput *>(ast.get()))
    {
        String format_name = ast_query_with_output->format
                ? getIdentifierName(ast_query_with_output->format)
                : context->getDefaultFormat();

        if (format_name == "Null")
            streams.null_format = true;
    }

    return streams;
}

executeQueryImpl中对query的执行流程参见前述计算部分。

TODO

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值