ClickHouse 源码阅读——SQL的前世今生

作者:逍凯,阿里云数据库实习开发工程师

注:以下分析基于开源 v19.15.2.2-stable 版本进行,社区最新版本代码改动较大,但是总体思路是不变的。

用户提交一条查询SQL背后发生了什么?

在传统关系型数据库中,SQL处理器的组件主要包括以下几种:

• Query Parsing负责进行词法和语法分析,把程序从人类高可读的格式(即SQL)转化成机器高可读的格式(AST,抽象语法树)。

词法分析指的是把SQL中的字符序列分解成一个个独立的词法单元——Token(<类型,值>)。语法分析指的是从词法分析器输出的token中识别各类短语,并构造出一颗抽象语法树。而按照构造抽象语法树的方向,又可以把语法分析分成自顶向下和自底向上分析两种。而ClickHouse采用的则是手写一个递归下降的语法分析器。

• Query Rewrite即通常我们说的"Logical Optimizer"或基于规则的优化器(Rule-Based Optimizer,即RBO)。

其负责应用一些启发式规则,负责简化和标准化查询,无需改变查询的语义。

常见操作有:谓词和算子下推,视图展开,简化常量运算表达式,谓词逻辑的重写,语义的优化等。

• Query Optimizer即通常我们所说的"Physical Optimizer",负责把内部查询表达转化成一个高效的查询计划,指导DBMS如何去取表,如何进行排序,如何Join。如下图所示,一个查询计划可以被认为是一个数据流图,在这个数据流图中,表数据会像在管道中传输一样,从一个查询操作符(operator)传递到另一个查询操作符。

ClickHouse 源码阅读——SQL的前世今生

 

一个查询计划

• Query Executor查询执行器,负责执行具体的查询计划,从存储引擎中获取数据并且对数据应用查询计划得到结果。执行引擎也分为很多种,如经典的火山模型(Volcano Model),还有ClickHouse采用的向量化执行模型(Vectorization Model)。

ClickHouse 源码阅读——SQL的前世今生

 

(图来自经典论文 Architecture Of Database System)

但不管是传统的关系型数据库,还是非关系型数据库,SQL的解析和生成执行计划过程都是大同小异的,而纵览ClickHouse的源代码,可以把用户提交一条查询SQL背后的过程总结如下:

1.服务端接收客户端发来的SQL请求,具体形式是一个网络包,Server的协议层需要拆包把SQL解析出来

2.Server负责初始化上下文与Network Handler,然后 Parser 对Query做词法和语法分析,解析成AST

3.Interpreter的 SyntaxAnalyzer 会应用一些启发式规则对AST进行优化重写

4.Interpreter的 ExpressionAnalyzer 根据上下文信息以及优化重写后的AST生成物理执行计划

5.物理执行计划分发到本地或者分布式的executor,各自从存储引擎中获取数据,应用执行计划

6.Server把执行后的结果以Block流的形式输出到Socket缓冲区,Client从Socket中读取即可得到结果

ClickHouse 源码阅读——SQL的前世今生

 

接收客户端请求

我们要以服务端的视角来出发,首先来看server.cpp大概做什么事情:

下面只挑选重要的逻辑:

• 初始化上下文

• 初始化Zookeeper(ClickHouse的副本复制机制需要依赖ZooKeeper)

• 常规配置初始化

• 绑定服务端的端口,根据网络协议初始化Handler,对客户端提供服务

int Server::main()
{
    // 初始化上下文
    global_context = std::make_unique<Context>(Context::createGlobal());
    global_context->setApplicationType(Context::ApplicationType::SERVER);
     
    // zk初始化
    zkutil::ZooKeeperNodeCache main_config_zk_node_cache([&] { return global_context->getZooKeeper(); });
    
    //其他config的初始化
    //...
    
    //绑定端口,对外提供服务
    auto address = make_socket_address(host, port);
    socket.bind(address, /* reuseAddress = */ true);

    //根据网络协议建立不同的server类型
    //现在支持的server类型有: HTTP,HTTPS,TCP,Interserver,mysql
    //以TCP版本为例:
    create_server("tcp_port", [&](UInt16 port)
    {
        Poco::Net::ServerSocket socket;
        auto address = socket_bind_listen(socket, listen_host, port);
        servers.emplace_back(std::make_unique<Poco::Net::TCPServer>(
            new TCPHandlerFactory(*this),
            server_pool,
            socket,
            new Poco::Net::TCPServerParams));
     });
    
    //启动server
    for (auto & server : servers)
            server->start();
   
}

客户端发来的请求是由各自网络协议所对应的 Handler 来进行的,server在启动的时候 Handler 会被初始化并绑定在指定端口中。我们以TCPHandler为例,看看服务端是如何处理客户端发来的请求的,重点关注 TCPHandler::runImpl 的函数实现:

• 初始化输入和输出流的缓冲区

• 接受请求报文,拆包

• 执行Query(包括整个词法语法分析,Query重写,物理计划生成和生成结果)

• 把Query结果保存到输出流,然后发送

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值