作者介绍,数据库作为核心基础部件,是需要保护的对象。任何不慎的线上操作都可能导致数据库出现严重故障,给业务造成巨大损失。为了避免这种损失,一般会在管理上下功夫,比如为研发人员制定数据库开发规范;新上线的SQL需要DBA审核;维护操作需要得到领导的批准等等。而如果你想能够有效地管理这些措施,你需要有效的数据库培训,还需要DBA们认真地进行SQL审计。许多中小型初创公司可以通过制定规范、进行培训和改进审核流程来管理数据库。随着美团点评业务的不断壮大,上述措施的实施成本越来越高。如何更多地依靠技术手段来提高效率,越来越受到关注。业界有很多基于MySQL源码开发的SQL审计、优化建议等工具,大大减轻了DBA的SQL审计负担。那么能否继续扩充MySQL的源码,辅助DBA和研发人员进一步提升效率呢?比如更全面的SQL优化功能;多维慢查询分析;辅助故障分析等。要实现以上功能,核心技术之一就是SQL解析。现状与场景SQL解析是一项复杂的技术,一般数据库厂商都掌握。当然也有提供SQL解析API的公司(参考链接:http://sqlparser.com/)。由于近几年MySQL数据库中间件的兴起,需要支持读写分离、分库分表等功能,所以需要提取表名、数据库名和SQL中相关字段的值。所以Java语言写的Druid、C语言写的MaxScale、Go语言写的Kingshard等,都会对SQL进行部分解析。但真正使用SQL分析技术进行数据库维护的产品却很少,主要有:美团点评开源的SQLAdvisor。它基于MySQL原生的词法分析,结合SQL中where条件、聚合条件、多表Join关系的分析,给出索引优化建议。参考链接:https://github.com/Meituan-Dianping/SQLAdvisor去哪儿开源Inception。专注于基于内置规则的SQL审计。参考链接:http://mysql-inception.github.io/inception-document阿里云DBA。根据官方文档,它还提供了SQL优化建议和改写。参考链接:https://yq.aliyun.com/articles/218442以上产品都有非常合适的应用场景,在业界应用广泛。然而,SQL解析的应用场景还远未被充分挖掘,例如:基于表粒度的慢查询报表。例如,一个Schema包含属于不同业务线的数据表。从业务线的角度,希望提供表粒度的慢查询报表。生成SQL特征。将SQL语句中的值替换为问号,方便SQL分类。虽然可以用正则表达式来实现同样的功能,但是BUG很多,可以参考pt-query-digest。例如在pt-query-digest中,所有遇到的数字都会被替换为“?”,从而无法区分不同数字后缀的表。高风险操作的确认和规避。比如DBA不小心丢了数据表,目前还没有有效的工具可以回滚这种操作,尤其是大表,后果不堪设想。SQL合法性判断。出于安全、审计、管控等方面的考虑,美团点评不允许研发人员直接操作数据库,而是提供RDS服务。尤其是数据变更,需要研发人员上级主管进行业务审批。如果研发人员写的SQL有语法错误,RDS无法判断SQL是否合法,就会造成不必要的沟通成本。因此,为了让所有需要它的业务都能方便的使用SQL解析功能,我们认为它应该具备以下特点:直接暴露SQL解析接口,使用尽可能简单。例如:输入SQL,输出表名、特征和优化建议。界面的使用不依赖于特定的语言,否则维护和使用成本太高。例如:以HTTP方式提供服务等。千里之行,始于足下。先介绍一下SQL的解析原理。原理SQL解析优化属于编译器的范畴,与C等其他语言的解析没有本质区别,分为词法分析、句法语义分析、优化、执行代码生成。MySQL对应的部分,如下图所示:SQL解析原理1.词法分析SQL解析包括两部分:词法分析和句法/语义分析。词法分析主要是将输入转化为记号。Token包含Keyword(也称symbol)和non-Keyword。例如:SQL语句selectusernamefromuserinfo,分析后会得到4个Token,包括2个Keywords,分别是select和from:通常使用Flex可以进行词法分析生成。参考链接:https://www.gnu.org/software/flex但是MySQL并没有使用这个工具,而是手写了词法分析部分(据说是为了效率和灵活性,参考:https://yq.aliyun.com/articles/71979)。具体代码在sql/lex.h和sql/sql_lex.cc文件中。MySQL中的Keyword定义在sql/lex.h中,部分Keyword如下:{"&&",SYM(AND_AND_SYM)},{"<",SYM(LT)},{"<=",SYM(LE)},{"<>",SYM(NE)},{"!=",SYM(NE)},{"=",SYM(EQ)},{">",SYM(GT_SYM)},{">=",SYM(GE)},{"<<",SYM(SHIFT_LEFT)},{">>",SYM(SHIFT_RIGHT)},{"<=>",SYM(EQUAL_SYM)},{"ACCESSIBLE",SYM(ACCESSIBLE_SYM)},{"ACTION",SYM(ACTION)},{"ADD",SYM(ADD)},{"AFTER",SYM(AFTER_SYM)},{"AGAINST",SYM(AGAINST)},{"AGGREGATE",SYM(AGGREGATE_SYM)},{"ALL",SYM(ALL)},词法分析的核心代码是sql/sql_lex.c文件中的MySQLLex→lex_one_Token,有兴趣的同学可以下载源代码进行研究。2.语法分析语法分析是生成语法树的过程。这是整个解析过程中最本质也是最复杂的部分,但这部分是由MySQL使用Bison来完成的。即便如此,如何设计合适的数据结构和相关算法来存储和遍历所有信息,还是值得在这里研究的。语法分析树SQL语句:selectusername,ismalfromuserinfowhereage>20andlevel>5and1=1会生成如下语法树:但背后的原理都是编译器的范畴。您可以参考维基百科上的一篇文章和此链接中的参考书。我在学习MySQL源码的过程中也看了部分内容。参考链接:https://en.wikipedia.org/wiki/LR_parser由于编译涉及的内容太多,本人经验和时间有限,就不做过多研究了。从工程的角度来说,学习如何使用Bison构建语法树来解决实际问题,可能对我们的工作更有帮助。下面我将讨论基于Bison的过程。MySQL语法分析树生成过程的全部源码在sql/sql_yacc.yy中,MySQL5.6中约有17K行代码。下面摘录SQL解析过程中涉及的部分代码:selectusername,ismalfromuserinfowhereage>20andlevel>5and1=1。其实有了Bison,SQL解析的难度并没有想象的那么大。特别是这里给出分析上下文之后。代码提示下:select/*select语言入口*/:select_init{LEX*lex=Lex;lex->sql_command=SQLCOM_SELECT;};select_init:SELECT_SYM/*select关键字*/select_init2|'('select_paren')'union_opt;select_init2:select_part2SELECT_LEX*sel=lex->current_select;if(lex->current_select->set_braces(0)){my_parse_error(ER(ER_SYNTAX_ERROR));MYSQL_YYABORT;}if(sel->linkage==UNION_TYPE&&sel->master_unit->first_select->braces)union_clause;select_part2:SELECT_LEX*sel=lex->current_select;if(sel->linkage!=UNION_TYPE)mysql_init_select(lex);lex->current_select->parsing_place=SELECT_LIST;}select_optionsselect_item_list/*解析列表名称*/{Select->parsing_place=NO_MATTER;}select_intoselect_lock_type;select_into:opt_order_clauseopt_limit_clause{}|into|select_from/*from字句*/|intoselect_from|select_frominto;select_from:FROMjoin_table_list/*解析表名*/where_clause/*where字句*/group_clausehaving_clauseopt_order_clauseopt_limit_clauseprocedure_analyse_clause{Select->context.table_list=Select->context.first_name_resolution_table=Select->table_list.first;}|FROMDUAL_SYMwhere_clauseopt_limit_clause/*oraclecompatibility:oraclealwaysrequiresFROMclause,andDUALissystemtablewithoutfields.Is"SELECT1FROMDUAL"anybetterthan"SELECT1"?嗯:)*/;where_clause:/*空*/{Select->where=0;}|WHERE{Select->parsing_place=IN_WHERE;}expr/*各种表达式*/{SELECT_LEX*select=Select;select->where=$3;select->parsing_place=NO_MATTER;if($3)$3->top_level_item;/*allpossibleexpressions*/expr:|exprandexpr%precAND_SYM{/*Seecommentsinruleexpr:exprorexpr*/Item_cond_and*item1;Item_cond_and*item3;if(is_cond_and($1)){item1=(Item_cond_and*)$1;if(is_cond_and($3)){item3=(Item_cond_and*)$3;/*(X1ANDX2)AND(Y1ANDY2)==>AND(X1,X2,Y1,Y2)*/item3->add_at_head(item1->argument_list);$$=$3;}else(X1ANDX2)ANDY==>AND(X1,X2,Y)*/item1->add($3);$$=$1;elseif(is_cond_and($3))XAND(Y1ANDY2)==>AND(X,Y1,Y2)*/item3->add_at_head($1);/*XANDY*/$$=new(YYTHD->mem_root)Item_cond_and($1,$3);if($$==)上下拉查看完整当你浏览上面的代码时,你会发现C++代码是通过C++代码嵌入到Bison中的,并将解析后的信息存储到相关对象中。比如表信息会存放在TABLE_LIST中,order_list会存放信息在orderby子句中,where子句存放在Item中。有了这些信息,就可以借助相应的算法对SQL做进一步的处理。核心数据结构及其关系在SQL解析中,核心结构是SELECT_LEX,定义在sql/sql_lex.h中。下面仅列出与上述示例相关的部分。在上面的SQL解析树结构图解中,item_list中存放的是列名username和ismale,table_list中存放的是表名,where中存放的是条件。其中where条件中的item层级最深,表达也比较复杂,如下图:SQL分析在where条件中的应用为了更深入的理解SQL解析器,这里举两个例子SQL分析的应用:1.UselessConditionalRemoval“Uselessconditionalremoval”属于优化器的逻辑优化范畴,只能根据SQL本身和表结构来完成。有很多优化案例。代码在sql/sql_optimizer.cc文件的remove_eq_conds函数中。为了避免过于繁琐的描述和大段代码的粘贴,这里通过图片分析以下四种情况:1=1and(m>3andn>4)1=2and(m>3andn>4)1=1or(m>3andn>4)1=2or(m>3andn>4)去除无用条件a:去除无用条件b去除无用条件c去除无用条件d如果你对其代码实现感兴趣的同学需要了解MySQL中一个重要的数据结构Item类。由于其复杂性,MySQL官方文档专门介绍了Item类。参考链接:https://dev.mysql.com/doc/internals/en/item-class.html阿里的MySQL团队也有类似的文章。更详细的理解需要查看源码中的sql/item_*等文件。2.SQL特征生成为了保证数据库基础系统组件的稳定高效运行,业界有很多辅助系统。比如慢查询系统,中间件系统。这些系统在收集和接收SQL后,需要对SQL进行分类,以便收集统计信息或应用相关策略。分类时,往往需要获取SQL特征。例如SQL:selectusername,ismalfromuserinfowhereage>20andlevel>5;SQL功能:selectusername,ismalfromuserinfowhereage>?andlevel>?业界著名的慢查询分析工具pt-query-digest通过正则表达式实现了该功能,但这种处理方式bug较多。下面介绍如何使用SQL解析完成SQL特征的生成。SQL特征生成由两部分组成:生成Token数组;基于Token数组生成SQL特征。先回顾一下词法分析那一章,我们在SQL中引入了关键字,每个关键字都有一个16位的整数与之对应,非关键字则统一用ident表示,ident也对应一个16位的整数。下表:SQL转换为特征的过程:在SQL解析过程中,可以轻松完成Token数组的生成。Token数组生成完成后,就可以轻松完成SQL特征的生成。SQL特性广泛应用于各种系统中。比如pt-query-digest需要将SQL按照特性进行分类。但是,它基于正则表达式的实现有很多错误。下面是几个已知的bug:学习建议最近在摸索SQL解析器和优化器的过程中,从一开始的无所适从,到有章可循,也总结了一些心得体会,在这里和大家分享一下:首先,阅读相关书籍,可以让我们对解析器和优化器有一个系统的认识。但是市面上关于MySQL的这类书籍很少。目前,您可以在《数据库查询优化器的艺术:原理解析与SQL性能优化》阅读中文作品;其次,需要阅读源码,但是最好以某个版本为基础,比如MySQL5.6.23,因为SQL分析和优化部分的代码是不断变化的,尤其是跨大版本的时候;再次,使用GDB多调试,验证自己的猜测,测试阅读质量;***,需要自己写相关的代码进行验证,只有写出来才算真正的精通。
