源码分析 | ClickHouse和他的朋友们(8)纯手工打造的SQL解析器

Clickhouse 计算引擎架构 —— Clickhouse 架构篇(三) 相比较于存储引擎的精妙设计,ClickHouse的计算引擎一直是一个争议非常大的话题。对ClickHouse计算引擎的各种评价都有,两极分化很严重。有人认为ClickHouse计算引擎的向量化设计得巧夺天工,也有很多人认为ClickHouse的计算引擎缺乏优化和对分布式的支持,就是个半成品。这些对ClickHouse计算引擎的评价都在一定程度上反映了ClickHouse计算引擎的某些方面,如果从这些方面来看待ClickHouse的计算引擎,难免陷入盲人摸象的状态。 阅读详情

本文首发于 2020-07-26 21:55:10

《ClickHouse和他的朋友们》系列文章转载自圈内好友 BohuTANG 的博客,原文链接:
https://bohutang.me/2020/07/25/clickhouse-and-friends-parser/
以下为正文。

现实生活中的物品一旦被标记为“纯手工打造”,给人的第一感觉就是“上乘之品”,一个字“贵”,比如北京老布鞋。

但是在计算机世界里,如果有人告诉你 ClickHouse 的 SQL 解析器是纯手工打造的,是不是很惊讶!

这个问题引起了不少网友的关注,所以本篇聊聊 ClickHouse 的纯手工解析器,看看它们的底层工作机制及优缺点。

枯燥先从一个 SQL 开始:

EXPLAIN SELECT a,b FROM t1

token

首先对 SQL 里的字符逐个做判断,然后根据其关联性做 token 分割:

比如连续的 WordChar,那它就是 BareWord,解析函数在 Lexer::nextTokenImpl(),解析调用栈:

DB::Lexer::nextTokenImpl() Lexer.cpp:63
DB::Lexer::nextToken() Lexer.cpp:52
DB::Tokens::operator[](unsigned long) TokenIterator.h:36
DB::TokenIterator::get() TokenIterator.h:62
DB::TokenIterator::operator->() TokenIterator.h:64
DB::tryParseQuery(DB::IParser&, char const*&, char const*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >&, bool, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, bool, unsigned long, unsigned long) parseQuery.cpp:224
DB::parseQueryAndMovePosition(DB::IParser&, char const*&, char const*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, bool, unsigned long, unsigned long) parseQuery.cpp:314
DB::parseQuery(DB::IParser&, char const*, char const*, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, unsigned long, unsigned long) parseQuery.cpp:332
DB::executeQueryImpl(const char *, const char *, DB::Context &, bool, DB::QueryProcessingStage::Enum, bool, DB::ReadBuffer *) executeQuery.cpp:272
DB::executeQuery(DB::ReadBuffer&, DB::WriteBuffer&, bool, DB::Context&, std::__1::function<void (std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&, std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > const&)>) executeQuery.cpp:731
DB::MySQLHandler::comQuery(DB::ReadBuffer&) MySQLHandler.cpp:313
DB::MySQLHandler::run() MySQLHandler.cpp:150

ast

token 是最基础的元组,他们之间没有任何关联,只是一堆生冷的词组与符号,所以我们还需对其进行语法解析,让这些 token 之间建立一定的关系,达到一个可描述的活力。

ClickHouse 在解每一个 token 的时候,会根据当前的 token 进行状态空间进行预判(parse 返回 true 则进入子状态空间继续),然后决定状态跳转,比如:

EXPLAIN  -- TokenType::BareWord

逻辑首先会进入Parsers/ParserQuery.cpp 的 ParserQuery::parseImpl 方法:

bool res = query_with_output_p.parse(pos, node, expected)
    || insert_p.parse(pos, node, expected)
    || use_p.parse(pos, node, expected)
    || set_role_p.parse(pos, node, expected)
    || set_p.parse(pos, node, expected)
    || system_p.parse(pos, node, expected)
    || create_user_p.parse(pos, node, expected)
    || create_role_p.parse(pos, node, expected)
    || create_quota_p.parse(pos, node, expected)
    || create_row_policy_p.parse(pos, node, expected)
    || create_settings_profile_p.parse(pos, node, expected)
    || drop_access_entity_p.parse(pos, node, expected)
    || grant_p.parse(pos, node, expected);

这里会对所有 query 类型进行 parse 方法的调用,直到有分支返回 true。

我们来看第一层 query_with_output_p.parse Parsers/ParserQueryWithOutput.cpp:

bool parsed =
       explain_p.parse(pos, query, expected)
    || select_p.parse(pos, query, expected)
    || show_create_access_entity_p.parse(pos, query, expected)
    || show_tables_p.parse(pos, query, expected)
    || table_p.parse(pos, query, expected)
    || describe_table_p.parse(pos, query, expected)
    || show_processlist_p.parse(pos, query, expected)
    || create_p.parse(pos, query, expected)
    || alter_p.parse(pos, query, expected)
    || rename_p.parse(pos, query, expected)
    || drop_p.parse(pos, query, expected)
    || check_p.parse(pos, query, expected)
    || kill_query_p.parse(pos, query, expected)
    || optimize_p.parse(pos, query, expected)
    || watch_p.parse(pos, query, expected)
    || show_access_p.parse(pos, query, expected)
    || show_access_entities_p.parse(pos, query, expected)
    || show_grants_p.parse(pos, query, expected)
    || show_privileges_p.parse(pos, query, expected

跳进第二层 explain_p.parse ParserExplainQuery::parseImpl状态空间:

bool ParserExplainQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected)
{
    ASTExplainQuery::ExplainKind kind;
    bool old_syntax = false;

    ParserKeyword s_ast("AST");
    ParserKeyword s_analyze("ANALYZE");
    ParserKeyword s_explain("EXPLAIN");
    ParserKeyword s_syntax("SYNTAX");
    ParserKeyword s_pipeline("PIPELINE");
    ParserKeyword s_plan("PLAN");

    ... ...
    else if (s_explain.ignore(pos, expected))
    {
       ... ...
    }
    
    ... ...
    
    ParserSelectWithUnionQuery select_p;
    ASTPtr query;
    if (!select_p.parse(pos, query, expected))
        return false;
    ... ...

s_explain.ignore 方法会进行一个 keyword 解析,解析出 ast node:

EXPLAIN -- keyword

跃进第三层 select_p.parse ParserSelectWithUnionQuery::parseImpl状态空间:

bool ParserSelectWithUnionQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected)
{
    ASTPtr list_node;

    ParserList parser(std::make_unique<ParserUnionQueryElement>(), std::make_unique<ParserKeyword>("UNION ALL"), false);
    if (!parser.parse(pos, list_node, expected))
        return false;
...

parser.parse 里又调用第四层 ParserSelectQuery::parseImpl 状态空间:

bool ParserSelectQuery::parseImpl(Pos & pos, ASTPtr & node, Expected & expected)
{
    auto select_query = std::make_shared<ASTSelectQuery>();
    node = select_query;

    ParserKeyword s_select("SELECT");
    ParserKeyword s_distinct("DISTINCT");
    ParserKeyword s_from("FROM");
    ParserKeyword s_prewhere("PREWHERE");
    ParserKeyword s_where("WHERE");
    ParserKeyword s_group_by("GROUP BY");
    ParserKeyword s_with("WITH");
    ParserKeyword s_totals("TOTALS");
    ParserKeyword s_having("HAVING");
    ParserKeyword s_order_by("ORDER BY");
    ParserKeyword s_limit("LIMIT");
    ParserKeyword s_settings("SETTINGS");
    ParserKeyword s_by("BY");
    ParserKeyword s_rollup("ROLLUP");
    ParserKeyword s_cube("CUBE");
    ParserKeyword s_top("TOP");
    ParserKeyword s_with_ties("WITH TIES");
    ParserKeyword s_offset("OFFSET");

    ParserNotEmptyExpressionList exp_list(false);
    ParserNotEmptyExpressionList exp_list_for_with_clause(false);
    ParserNotEmptyExpressionList exp_list_for_select_clause(true);  
    ...
    
            if (!exp_list_for_select_clause.parse(pos, select_expression_list, expected))
            return false;

第五层 exp_list_for_select_clause.parse ParserExpressionList::parseImpl状态空间继续:

bool ParserExpressionList::parseImpl(Pos & pos, ASTPtr & node, Expected & expected)
{
    return ParserList(
        std::make_unique<ParserExpressionWithOptionalAlias>(allow_alias_without_as_keyword),
        std::make_unique<ParserToken>(TokenType::Comma))
        .parse(pos, node, expected);
}

… … 写不下去个鸟!

可以发现,ast parser 的时候,预先构造好状态空间,比如 select 的状态空间:

  1. expression list
  2. from tables
  3. where
  4. group by
  5. with …
  6. order by
  7. limit

在一个状态空间內,还可以根据 parse 返回的 bool 判断是否继续进入子状态空间,一直递归解析出整个 ast。

总结

手工 parser 的好处是代码清晰简洁,每个细节可防可控,以及友好的错误处理,改动起来不会一发动全身。

缺点是手工成本太高,需要大量的测试来保证其正确性,还需要一些fuzz来保证可靠性。

好在ClickHouse 已经实现的比较全面,即使有新的需求,在现有基础上修修补补即可。


欢迎关注我的微信公众号【数据库内核】:分享主流开源数据库和存储引擎相关技术。

标题网址
GitHubhttps://dbkernel.github.io
知乎https://www.zhihu.com/people/…
思否(SegmentFault)https://segmentfault.com/u/db…
掘金https://juejin.im/user/5e9d3e…
开源中国(oschina)https://my.oschina.net/dbkernel
博客园(cnblogs)https://www.cnblogs.com/dbkernel
【clickhouse】clickhouse 解析器 1.概述 ClickHouse 中有两种类型的解析器, full parser 和 data format parser ,前者是一个完整的 SQL 解析器,后者是一个高性能的流解析器。当语句被发到 ClickHouse 时,默认配置下前 1 MB 字节的数据会使用 full parser 来处理,剩下的数据就交给 data format parser了,所以,像 insert 这类语句,即使整个语句再长,也不会有问题。 语法细节,整体上跟 MySQL 是一样的,当然, ClickHouse 在一些地方有自 阅读详情

相关推荐

【ClickHouse 实战】怎样使用ClickHouse SQL进行数据分析

使用 ClickHouse SQL 进行数据分析主要包括以下几个方面:数据查询、聚合和分组、数据过滤和排序、时间序列和窗口函数以及使用内置函数。

AI天才研究院 1万+

ClickHouse源码分析——SQL解析

SQL解析函数 ClickHouse的SQL解析入口函数:parseQuery, 函数原型: ASTPtr parseQuery(     IParser &amp; parser,     const char * begin,     const char * end,     const std::string &amp; query_description,     size...

Hello!Eden 5868

别让PEX和后仿真拖后腿:Calibre与Virtuoso ADE联调的那些‘坑’与最佳实践

本文深入探讨了Calibre与Virtuoso ADE在PEX寄生参数提取和后仿真联调中的常见问题与解决方案。针对DRC/LVS验证通过后出现的网表导入错误和仿真波形异常,提供了七大典型场景的实战经验,包括PEX提取模式选择、CalibreView生成细节、环境变量配置等关键环节的最佳实践,助力工程师高效完成芯片设计验证。

weixin_33695450的博客 393

clickhouse实时大数据分析引擎的SQL写法详解

  ClickHouse的优点 单个查询的并行处理(利用多个核心) 多个服务器上的分布式处理 非常快速的扫描(参见下面的基准测试),可用于实时查询 列存储非常适合使用“宽”/“非规范化”表(许多列) 压缩性好 SQL支持(有限制) 良好的功能集,包括支持近似计算 不同的存储引擎(磁盘存储格式) 非常适合结构日志/事件数据以及时间序列数据(引擎MergeTree需要日期字段) ...

Alice_qixin的博客 1万+

LangChain4j-针对性操作Function-call+系统预设角色

如果我们想要对一些敏感信息进行针对化的操作,不仅仅是接口大模型去进行回复。比如我们现在数据库里面有一些菜品,那么用户提问,店里有多少套餐,那么云端大模型肯定是不知道的,我们希望捕获这些关键字然后进行自己的业务查询或者处理,这个时候就可以使用function-call。

DDDDDouble的博客 454

uniapp - web-view嵌套H5网页白屏问题,<web-view>页面跳转出现白屏情况,强力解决uniapp嵌套webview页面时出现无法加载并白屏(无论本地还是线上H5网页链接都能解决)

uniapp微信小程序,uniapp APP端,uni-app web-view嵌套H5项目白屏问题分析及解决,uniapp嵌套webview页面时,出现白屏,uniapp <web-view>页面跳转出现白屏解决方法,uniapp <web-view>页面跳转出现白屏解决方法,解决uniapp的web-view打开网页在老手机上出现白屏的问题,uni-app web-view白屏处理,引入本地网页白屏,WebView 加载不出网页,一片空白webview白屏加载不出来,使用webview加载链接后白屏,u

🏆 前端领域优质创作者 1万+

探索DorisParser: 强大的SQL解析器,助力数据处理新高度

探索DorisParser: 强大的SQL解析器,助力数据处理新高度 在大数据时代,高效、准确地解析SQL查询对于任何数据处理系统都至关重要。今天,我们要向您推荐一个开源项目——,这是一个由Java编写的高性能SQL解析库,它提供了丰富的功能,以满足对复杂SQL语句的解析需求。 项目简介 DorisParser是专为大数据场景设计的SQL解析引擎,能够解析多种常见的SQL方言,包括MySQL、Hi...

gitblog_00074的博客 848

海康VisionMaster与西门子Smart 200进行S7通信

海康VisionMaster与西门子Smart 200进行S7通信

haier123888的博客 8184

ClickHouse SQL 操作语句详解

ClickHouse SQL 语句详解

深圳市多克创新科技有限公司 3791

高通SDX35:SGMII接口PHY RTL8211FSI-CG驱动适配

高通SDX35 RTL8211F SGMII

不秃头的程序猿Gabriel的博客 787

【微服务】Java 对接飞书多维表格使用详解

Java 对接飞书多维表格使用详解

congge_study的博客 1万+

基于Calcite自定义SQL解析器

最近项目组要求写个SQL脚本相关的解析工具能自定义校验规则,开始一直研究基于C#的Irony框架学习;由于本人基础薄弱,网上这个框架的资源不是很多。最终放弃改使用Apache Calcite框架。借用了网上的一些资源和博客,终于把自定义规则的demo弄好了。现把学习过程和其中出现的问题整理如下,希望能帮到也想学习Calcite新伙伴。 1>下载IDE并配置maven环境 我的demo基于eclipse开发的,其它IDE环境可自行钻研学习 1.1、maven下载 http://maven.apach

weixin_38381496的博客 1970

【2026华为OD新规机考】最新套题持续更新【完全原创题解 | 详细考点分类 | 不断更新题目 | 六种主流语言Py+Java+Cpp+C+Js+Go】

可上练习华子OD、大厂真题绿色聊天软件戳od1441了解算法冲刺训练(备注【优快云】否则不通过)

weixin_48157259的博客 511

探索高效SQL解析——Node-SQL-Parser

在这个大数据和云计算的时代,SQL作为数据查询与管理的主要语言,其重要性不言而喻。而`Node-SQL-Parser`()是一个基于JavaScript的开源库,专门用于解析SQL语句,它为开发者提供了一个简单而强大的工具,以方便地处理和操作SQL。 ## 项目简介 `Node-SQL-Parser`是由GitHub用户@taozhi8833998开发的一个Node.js模块,它的主要功能是将...

gitblog_00060的博客 3458

某Z节验证码

某Z节验证码、captchaBody、滑块、旋转、空间推理

mamenqi_csdn的博客 773

复分析 阿尔福斯(Ahlfors) 第三版答案 英文

答案分为十章,有部分习题没有答案,全英文复分析阿尔福斯(Ahlfors) 第三版答案

node封装sql分页函数(动态注入查询条件)

【代码】node封装sql分页函数(动态注入查询条件)

creatlh的博客 463

数学建模模型总结

这是本人在两年内准备参加数学建模竞赛搜集到的模型资料,已经经过了本人的筛选和整理,希望能够对准备参加数学建模竞赛的学弟学妹们提供一些帮助!

大数据之ClickHouse

首先,MergeTree的分区目录并不是在数据表被创建之后就存在的,而是在数据写入过程中被创建的,也就是说,如果一张数据表没有任何数据,那么也不会有任何分区目录存在。MergeTree按照index_granularity的间隔粒度,将一段完整的数据划分成了多个小的间隔数据段,一个具体的数据段即是一个MarkRange。通过与start及end对应的索引编号的取值,即能够得到它所对应的数值区间。MergeTree数据分区的规则由分区ID决定,而具体到每个数据分区所对应的ID,则是由分区键的取值决定的。

IT枫斗者的博客 938

jre9下载 jdk9

(Java SE Runtime Environment 9.0) 9.0u175 官方最新版 【x86|x64】

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

ClickHouse 的 Parser 分析器

ClickHouse的Parser分析器是其SQL查询处理的核心组件之一,主要功能是将SQL语句解析为抽象语法树(AST)。Parser 采用递归方式解析SQL语句,生成AST语法树结构,不同类型的SQL语句会调用对应的parse实现类。作为查询处理流程的第一阶段,Parser与分布式处理、列式存储等特性共同构成ClickHouse的高性能分析能力。生成的AST会被后续的Interpreter解释器用于创建查询执行计划。其设计充分考虑了OLAP场景对复杂查询解析的需求。使用C++编写,性能优化显著。

weixin_43346403的博客 259

ClickHouse 语法详解

ClickHouse 语法详解

深圳市多克创新科技有限公司 2357

ClickHouse SQL 在 Java 中的校验方法

涵盖官方 JDBC 驱动、第三方库以及自定义实现方案。

hopsky的博客 268

ClickHouse SQL语法详解:从基础到高级特性

ClickHouse SQL语法详解:从基础到高级特性 【免费下载链接】ClickHouse ClickHouse® 是一个免费的大数据分析型数据库管理系统。 项目地址: https://gitcode.com/GitHub_Tr...

gitblog_00267的博客 569

ClickHouse的介绍(基本sql操作,以及数据库引擎、表引擎、分片、副本、explain、优化、物化视图等)

ClickHouse是俄罗斯的Yandex于2016年开源的列式存储数据库(DBMS),使用C++语言编写,主要用于在线分析处理查询(OLAP),能够使用SQL查询实时生成分析数据报告。列式存储以下面的表为例:1)采用行式存储时,数据在磁盘上的组织结构为:好处是想查某个人所有的属性时,可以通过一次磁盘查找加顺序读取就可以。但是当想查所有人的年龄时,需要不停的查找,或者全表扫描才行,遍历的很多数据都是不需要的。2)采用列式存储时,数据在磁盘上的组织结构为:这时想查所有人的年龄只需把年龄那一列拿出来就可以了。3

迷雾总会解 4万+

clickhouse常用sql

1、慢查询监控 SELECT user, formatDateTime(query_start_time, '%Y%m%d %T') AS start_time, query_duration_ms / 1000 AS query_duration_s, query, memory_usage / 1024 / 1024 / 1024 AS memory_usage_g, result_rows , formatReadableSize(resu

麻辣香蕉 1643

ClickHouse 入门(二)【基础SQL操作】

clickhouse 基本语法、多维分析、副本引擎、分片副本机制

功不唐捐,玉汝于成 2251

ClickHouse中的SQL语法和函数详解

– with语法 with '1' as v select * from tb_user where id=v; --求平均年龄 with (select count(1) from tb_user) as cnt select (sum(age)/cnt) from tb_user; array join 语法 array join 相当于explode+lateral view select id,arr from tb_arr_join array join arr; --会覆盖原先的数组

weixin_46011754的博客 3397
上一篇: 源码分析 | ClickHouse和他的朋友们(6)MergeTree存储结构
下一篇: 源码分析 | ClickHouse和他的朋友们(10)MergeTree Write-Ahead Log
dbkernel
博客等级 码龄4年 60粉丝 42原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值