如何自己开发一个大数据SQL引擎?

我们先回顾一下前面讨论过的大数据仓库 Hive。作为一个成功的大数据仓库,它将 SQL 语句转换成 MapReduce 执行过程,并把大数据应用的门槛下降到普通数据分析师和工程师就可以很快上手的地步。

但是 Hive 也有自己的问题,由于它使用自己定义的 Hive QL 语法,这对已经熟悉 Oracle 等传统数据仓库的分析师来说,还是有一定的上手难度。特别是很多企业使用传统数据仓库进行数据分析已经由来已久,沉淀了大量的 SQL 语句,并且这些 SQL 经过多年的修改打磨,非常庞大也非常复杂。我曾经见过某银行的一条统计报表 SQL,打印出来足足两张 A4 纸。这样的 SQL 光是完全理解可能就要花很长时间,再转化成 Hive QL 就更加费力,还不说转化修改过程中可能引入的 bug。

如果你们团队决定开发一款能够支持标准数据库 SQL 的大数据仓库引擎,希望让那些在 Oracle 上运行良好的 SQL 可以直接运行在 Hadoop 上,而不需要重写成 Hive QL。我们称之为Panthera

在开发 前,我们分析一下 Hive 的主要处理过程,大体上分成三步:

  1. 将输入的 Hive QL 经过语法解析器转换成 Hive 抽象语法树(Hive AST)。

  2. 将 Hive AST 经过语义分析器转换成 MapReduce 执行计划。

  3. 将生成的 MapReduce 执行计划和 Hive 执行函数代码提交到 Hadoop 上执行。

Panthera的设计思路是保留 Hive 语义分析器不动,替换 Hive 语法解析器,使其将标准 SQL 语句转换成 Hive 语义分析器能够处理的 Hive 抽象语法树。用图形来表示的话,是用红框内的部分代替黑框内原来 Hive 的部分。

image.png

红框内的组件我们重新开发过,浅蓝色的是我们使用的一个开源的 SQL 语法解析器,将标准 SQL 解析成标准 SQL 抽象语法树(SQL AST),后面深蓝色的就是团队自己开发的 SQL 抽象语法树分析与转换器,将 SQL AST 转换成 Hive AST。

那么标准 SQL 和 Hive QL 的差别在哪里呢?

标准 SQL 和 Hive QL 的差别主要有两个方面,一个是语法表达方式,Hive QL 语法和标准 SQL 语法略有不同;另一个是 Hive QL 支持的语法元素比标准 SQL 要少很多,比如,数据仓库领域主要的测试集TPC-H所有的 SQL 语句 Hive 都不支持。尤其是 Hive 不支持复杂的嵌套子查询,而对于数据仓库分析而言,嵌套子查询几乎是无处不在的。比如下面这样的 SQL,在 where 查询条件 existes 里面包含了另一条 SQL 语句。


select o_orderpriority, count(*) as order_count 
from orders 
where o_orderdate >= date '[DATE]' 
and o_orderdate < date '[DATE]' + interval '3' month 
and exists 
( select * from lineitem 
where l_orderkey = o_orderkey and l_commitdate < l_receiptdate ) 
group by o_orderpriority order by o_orderpriority;

所以开发支持标准 SQL 语法的 SQL 引擎的难点,就变成如何将复杂的嵌套子查询消除掉,也就是 where 条件里不包含 select。

SQL 的理论基础是关系代数,而关系代数的主要操作只有 5 种,分别是并、差、积、选择、投影。所有的 SQL 语句最后都能用这 5 种操作组合完成。而一个嵌套子查询可以等价转换成一个连接(join)操作。

比如这条 SQL

select s_grade from staff where s_city not in (select p_city from proj where s_empname=p_pname)

这是一个在 where 条件里嵌套了 not in 子查询的 SQL 语句,它可以用 left outer join 和 left semi join 进行等价转换,示例如下,这是 Panthera 自动转换完成得到的等价 SQL。这条 SQL 语句不再包含嵌套子查询,


select panthera_10.panthera_1 as s_grade from (select panthera_1, panthera_4, panthera_6, s_empname, s_city from (select s_grade as panthera_1, s_city as panthera_4, s_empname as panthera_6, s_empname as s_empname, s_city as s_city from staff) panthera_14 left outer join (select panthera_16.panthera_7 as panthera_7, panthera_16.panthera_8 as panthera_8, panthera_16.panthera_9 as panthera_9, panthera_16.panthera_12 as panthera_12, panthera_16.panthera_13 as panthera_13 from (select panthera_0.panthera_1 as panthera_7, panthera_0.panthera_4 as panthera_8, panthera_0.panthera_6 as panthera_9, panthera_0.s_empname as panthera_12, panthera_0.s_city as panthera_13 from (select s_grade as panthera_1, s_city as panthera_4, s_empname as panthera_6, s_empname, s_city from staff) panthera_0 left semi join (select p_city as panthera_3, p_pname as panthera_5 from proj) panthera_2 on (panthera_0.panthera_4 = panthera_2.panthera_3) and (panthera_0.panthera_6 = panthera_2.panthera_5) where true) panthera_16 group by panthera_16.panthera_7, panthera_16.panthera_8, panthera_16.panthera_9, panthera_16.panthera_12, panthera_16.panthera_13) panthera_15 on ((((panthera_14.panthera_1 <=> panthera_15.panthera_7) and (panthera_14.panthera_4 <=> panthera_15.panthera_8)) and (panthera_14.panthera_6 <=> panthera_15.panthera_9)) and (panthera_14.s_empname <=> panthera_15.panthera_12)) and (panthera_14.s_city <=> panthera_15.panthera_13) where ((((panthera_15.panthera_7 is null) and (panthera_15.panthera_8 is null)) and (panthera_15.panthera_9 is null)) and (panthera_15.panthera_12 is null)) and (panthera_15.panthera_13 is null)) panthera_10 ;

通过可视化工具将上面两条 SQL 的语法树展示出来,是这样的。

image.png

这是原始的 SQL 抽象语法树。

image.png

这是等价转换后的抽象语法树,内容太多被压缩得无法看清,不过你可以感受一下

那么,在程序设计上如何实现这样复杂的语法转换呢?当时 Panthera 项目组合使用了几种经典的设计模式,每个语法点被封装到一个类里去处理,每个类通常不过几十行代码,这样整个程序非常简单、清爽。如果在测试过程中遇到不支持的语法点,只需为这个语法点新增加一个类即可,团队协作与代码维护非常容易。

使用装饰模式的语法等价转换类的构造,Panthera 每增加一种新的语法转换能力,只需要开发一个新的 Transformer 类,然后添加到下面的构造函数代码里即可。


   private static SqlASTTransformer tf =
      new RedundantSelectGroupItemTransformer(
      new DistinctTransformer(
      new GroupElementNormalizeTransformer(
      new PrepareQueryInfoTransformer(
      new OrderByTransformer(
      new OrderByFunctionTransformer(
      new MinusIntersectTransformer(
      new PrepareQueryInfoTransformer(
      new UnionTransformer(
      new Leftsemi2LeftJoinTransformer(
      new CountAsteriskPositionTransformer(
      new FilterInwardTransformer(
      //use leftJoin method to handle not exists for correlated
      new CrossJoinTransformer(
      new PrepareQueryInfoTransformer(
      new SubQUnnestTransformer(
      new PrepareFilterBlockTransformer(
      new PrepareQueryInfoTransformer(
      new TopLevelUnionTransformer(
      new FilterBlockAdjustTransformer(
      new PrepareFilterBlockTransformer(
      new ExpandAsteriskTransformer(
      new PrepareQueryInfoTransformer(
      new CrossJoinTransformer(
      new PrepareQueryInfoTransformer(
      new ConditionStructTransformer(
      new MultipleTableSelectTransformer(
      new WhereConditionOptimizationTransformer(
      new PrepareQueryInfoTransformer(
      new InTransformer(
      new TopLevelUnionTransformer(
      new MinusIntersectTransformer(
      new NaturalJoinTransformer(
      new OrderByNotInSelectListTransformer(
      new RowNumTransformer(
      new BetweenTransformer(
      new UsingTransformer(
      new SchemaDotTableTransformer(
      new NothingTransformer())))))))))))))))))))))))))))))))))))));

而在具体的 Transformer 类中,则使用组合模式对抽象语法树 AST 进行遍历,以下为 Between 语法节点的遍历。我们看到使用组合模式进行树的遍历不需要用递归算法,因为递归的特性已经隐藏在树的结构里面了。


  @Override
  protected void transform(CommonTree tree, TranslateContext context) throws SqlXlateException {
    tf.transformAST(tree, context);
    trans(tree, context);
  }

  void trans(CommonTree tree, TranslateContext context) {
    // deep firstly
    for (int i = 0; i < tree.getChildCount(); i++) {
      trans((CommonTree) (tree.getChild(i)), context);
    }
    if (tree.getType() == PantheraExpParser.SQL92_RESERVED_BETWEEN) {
      transBetween(false, tree, context);
    }
    if (tree.getType() == PantheraExpParser.NOT_BETWEEN) {
      transBetween(true, tree, context);
    }
  }

将等价转换后的抽象语法树 AST 再进一步转换成 Hive 格式的抽象语法树,就可以交给 Hive 的语义分析器去处理了,从而也就实现了对标准 SQL 的支持。

当时 Facebook 为了证明 Hive 对数据仓库的支持,Facebook 的工程师手工将 TPC-H 的测试 SQL 转换成 Hive QL,我们将这些手工 Hive QL 和 Panthera 进行对比测试,两者性能各有所长,总体上不相上下,这说明 Panthera 自动进行语法分析和转换的效率还是不错的。

事实上,标准 SQL 语法集的语法点非常多,我们经过近两年的努力,绞尽脑汁进行各种关系代数等价变形,依然没有全部适配所有的标准 SQL 语法。

思考题

SQL 注入是一种常见的 Web 攻击手段,如下图所示,攻击者在 HTTP 请求中注入恶意 SQL 命令(drop table users;),服务器用请求参数构造数据库 SQL 命令时,恶意 SQL 被一起构造,并在数据库中执行。

image.png

但是 JDBC 的 PrepareStatement 可以阻止 SQL 注入攻击,MyBatis 之类的 ORM 框架也可以阻止 SQL 注入,请从数据库引擎的工作机制解释 PrepareStatement 和 MyBatis 的防注入攻击的原理。

因为SQL语句在程序运行前已经进行了预编译,在程序运行时第一次操作数据库之前,SQL语句已经被数据库分析,编译和优化,对应的执行计划也会缓存下来并允许数据库已参数化的形式进行查询,当运行时动态地把参数传给PreprareStatement时,即使参数里有敏感字符如 or '1=1'也数据库会作为一个参数一个字段的属性值来处理而不会作为一个SQL指令,如此,就起到了SQL注入的作用了

如果用statement jdbc会简单拼接字符串然后作为sql执行
preparedstatement就会进行预编译 对其中的换行符等字符做转义 对注入的sql会起到混淆的作用
mybatis这些orm框架也是基于preparedstatement mybatis尽量使用#占位符

正常情况下,当http请求参数带有sql语句的条件时,在 SQL 注入中,参数值作为 SQL 指令的一部分,会被数据库进行编译/解释执行。当使用了 PreparedStatement,带占位符 ( ? ) 的 sql 语句只会被编译一次,之后执行只是将占位符替换为参数值,并不会再次编译/解释,因此从根本上防止了 SQL 注入问题。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341