QueryParser,一个用于解析和分析SQL的开源工具

0.
QueryParser,一个用于解析和分析SQL的开源工具

在2015年初,优步工程将其业务实体从整数标识符迁移到UUID标识符,作为使用多个活动数据中心的主动的一部分。为实现这一目标,我们的数据仓库团队是任务的,识别数据仓库中每个表之间的每个外关关系,以将所有uuids的所有ID列回填

鉴于我们表格的分散所有权,这并不是一个简单的努力。最有希望的解决方案是通过刮擦提交给仓库的所有SQL查询并观察到哪个列连接在一起的信息来遍历信息。为了满足这种需求,我们建造和开放QueryParser.,我们用于解析和分析SQL查询的工具。

在本文中,我们讨论了QueryParser的实现,它解锁的各种应用程序以及沿途遇到的一些问题和局限性。

实施

在内部,QueryParser部署在流架构中,如图1所示,如下所示:

图1:优步数据仓库流式架构通过查询分析器馈送所有查询。盒子表示服务和管道表示数据流。目录信息服务负责跟踪数据仓库中表的模式。

QueryParser将提交到数据仓库的实时查询流,分析每一个单个,并将分析结果发出到单独的流。单个查询以三个步骤处理,如下所述,并在图2中示出。

  • 第1阶段:解析。将查询从一个原始字符串转换为抽象语法树(AST)表示。
  • 第2阶段:解决。扫描原始AST并应用范围规则。通过添加表名来转换纯列名称,并通过添加模式名称来转换普通表名称。要求在每个表中输入每个表中的列和每个模式中的表中的完整列表,否则称为“目录信息”。
  • 第3阶段:分析。扫描已解析的AST,查找是否相等的列。
图2:QueryParser需要三次传递到完全处理查询:解析,解析和分析。顶部流程将此序列概念上显示为数据类型的转换。底部流程明确地说明了真实查询的序列。

它的实现和架构成功地确定了外键关系——这是一个很好的结果,因为原型只覆盖了SQL语法的一部分,目录信息完全是硬编码的,而且我们对外键关系的理解也在不断发展

Haskell的选择

您可能在开源中注意到的第一件事之一QueryParser.存储库是它写入的哈斯克尔。Queryparser最初是由一位Haskell爱好者Uber工程师构想出来的,并很快得到了其他几个工程师的关注。事实上,我们中的许多人专门学习Haskell来开发它。

由于各种原因,Haskell成为原型设计QueryParser的好选择。要开始,Haskell非常成熟图书馆支持对于语言解析器。其富有表现力的系统对于我们的SQL查询的内部模型的频繁和广泛的重构也非常有用。此外,我们倾向于编制的编译器来指导我们通过那些大型可怕的重构。如果我们使用动态类型的语言尝试相同,我们将丢失几周追逐Haskell的编译器可以快速为我们标记的运行时错误。

在Haskell写QueryParser的主要缺点是,没有足够的开发人员知道它。为了向Haskell推出更多的工程师,我们开始了一个每周阅读小组,在午餐时遇到午餐,讨论Haskell书籍和文件。

请注意,对于与所需的非Haskell基础结构的其余部分的互操作性,QueryParser是(并且是)部署在Python代理服务器后面。看看部署Queryparser本文的一部分以获取更多详细信息。

解决方案的多样性

在QueryParser的初始成功之后,我们考虑了该工具可以改善我们的数据仓库运营的其他方式。除了实现连接检测,我们还决定实现几个分析功能:

  • 表访问:在查询中访问了哪些表格
  • 列访问:查询的每个子句中访问了哪些列
  • 表谱系:查询修改了哪些表,哪些输入决定了它们的最终状态

新分析在我们的数据仓库中对访问模式进行了详细的理解,允许以下领域的进步:表管理,有针对性的通信,了解下面概述的数据流,事件响应和防御性操作:

表管理

就表管理而言,福利是三倍。首先,表访问统计信息让我们通过查找不经常访问的表来释放存储和计算资源,然后删除它们。

其次,列访问统计信息让我们通过优化磁盘上的表布局来提高数据库性能,特别是Vertica投影。诀窍是按列将顶部组设置为分片键,并按列作为订单键。

最后,列连接统计信息通过识别经常连接在一起的表集群并将它们替换为单个表,从而提高数据可用性并减少数据库负载尺寸模型表。

有针对性的沟通

表访问统计信息让我们向数据消费者发送目标通信。除了关于表模式或数据质量问题的更新中,我们可以仅通知最近访问该表的数据消费者来爆破整个数据工程邮件列表。

了解数据流

表谱系数据解锁了特殊用例:如果一起分析了一系列查询,则可以聚合表谱系数据以在序列上生成数据流的图形。

例如,考虑下面的图3中的假设SQL,从依赖表B和C产生新版本的模型表A:

查询
drop a_new如果存在
从b中创建a_new作为选择...
从c插入a_new select ...
如果存在,则删除a_old
重命名a到a_old
重命名a_new到a

图3:用于计算模型表A的SQL查询的序列来自依赖表B和C.

在下面的图4中,我们描述了Queryparser将为序列中的每个查询生成的表谱系。此外,我们描述并解释序列中每个查询的累积观测数据流。最后,累积数据流(正确!)记录表A对表B和C有依赖关系:

查询 表谱系的查询 累积观察到的数据流 解释累积数据流量
drop a_new如果存在 a_new没有依赖关系 a_new没有依赖关系
从b中创建a_new作为选择... a_new中的数据由b中的数据专门确定
a_new中的数据由b中的数据专门确定
从c插入a_new select ... a_new中的数据由A_New中的先前数据和C中的数据确定 a_new中的数据由b中的数据确定和c中的数据
如果存在,则删除a_old a_old没有依赖关系 a_new中的数据由B中的数据和C中的数据确定

a_old没有依赖关系

重命名a到a_old a_old中的数据由先前的数据确定

A没有依赖性,不得不

a_new中的数据由B中的数据和C中的数据确定

a_old中的数据由先前的数据确定

A没有依赖性,不得不

重命名a_new到a A中的数据由A_New中的先前数据确定

a_new不再没有依赖性

a中的数据由B中的数据和C中的数据确定

a_old中的数据由先前的数据确定

a_new不再没有依赖性

图4:从图3中的SQL,对于序列中的每个查询的表谱系,以及整个序列的累积表谱系。

我们修改了我们Etl.-框架记录每个ETL中的SQL查询序列,并将它们提交给Queryparser,此时Queryparser以编程方式为仓库中所有建模的表生成数据流图。下面的图5给出了一个例子:

图5:示例数据流图表示四个原始表(A,B,C,D)和三个建模表(E,F,G)描绘了查询如何通过QueryParser处理查询。在实践中,原始表通常来自上游操作系统,例如Kafka主题,刀具数据存储和面向服务的体系结构(SOA)数据库表。所建模的表在数据仓库(Hive)和下游数据集市(Vertica)中暴露。

事件响应

表谱系数据在响应数据质量事件方面非常有用,它通过提供事件影响的战术可见性来减少缓解时间。例如,考虑到表依赖关系在图5中,原始表中如果有一个问题,我们知道,影响的范围包括建模表E, G .我们也知道,一旦问题解决,E和G需要回填。为了解决这个问题,我们可以将沿袭数据与表访问数据结合起来,向E和G的所有用户发送有针对性的通信。

表沿袭数据对于识别事件的根本原因也很有用。例如,如果图5中建模的表E有问题,那只可能是由于原始表A或B。如果建模的表G有问题,那可能是由于原始表A、B、C或D。

防御行动

最后,能够在运行时解锁的防御性操作策略中分析查询,使我们的数据仓库能够更顺利地运行。使用QueryParser,可以将查询拦截到数据仓库,并提交分析。如果QueryParser检测到解析错误或某些查询反模式,则可以拒绝查询,从而减少数据仓库的整体负载。

问题和限制

弗雷德布鲁克斯着名争辩说没有银弹在软件工程中。虽然对我们的存储需求有好处,但Queryparser也不例外。随着项目的展开,它揭示了一些有趣的本质复杂性。

长尾的语言特征

首先,最不令人惊讶的是:在为新的SQL方言添加支持时,有很多常用的语言功能尾部可以实现,这可能需要重大更改QueryParser的查询内部表示。在原型阶段在专门处理的QueryParser时立即显而易见vertica,并在支持时进一步确认蜂巢普拉斯托是补充道。例如,在Vertica中解析TIMESERIES和OFFSET需要向SELECT语句添加新子句。此外,Hive中左半连接的解析需要一种新的连接类型和特殊的作用域规则,而Presto中额外的顶级命名空间“数据库”(该表属于模式属于数据库)的解析需要大量的结构访问解析

跟踪目录状态

其次,跟踪目录状态很难。调用解析列名称和表名时需要目录信息。优步数据仓库支持高度并发的工作负载,包括并发架构更改,通常会创建,丢弃和重命名表,或从现有表中添加或删除列。我们使用QueryParser来跟踪目录状态来简要试验;如果QueryParser已经分析了每个查询,我们会想知道我们是否可以简单地添加报告模式更改的分析,并通过将它们应用于以前的目录状态来生成新的目录状态。最终,由于难以订购整个查询流,这种方法是不成功的。相反,我们的替代(更有效)的方法是将目录状态视为更静态或更少的静态,跟踪通过配置文件的表的模式成员资格和列列表。

Sessionizing查询

第三,难以与QueryParser会话化查询很难。在一个完美的世界中,QueryParser将能够在整个数据库会话中跟踪表谱系,占交易和回滚以及各个级别的交易隔离。然而,在实践中,根据查询日志重构数据库会话非常困难,因此我们决定不为这些特性添加表沿袭支持。相反,Queryparser依赖于Uber的ETL框架来代表它session化ETL查询。

漏水的抽象

最后,Hive是底层文件系统上的泄漏抽象。例如,可以通过几种方式完成插入物:

  1. 插入foo选择...从栏中
  2. ALTER表FOA添加分区...地点'/ HDFS / PATH / TO / PATTITION / IN / BAR'

优步的ETL框架最初使用了第一个方法,但被迁移到使用第二种方法,因为它显示出戏剧性的性能改进。这引起表谱系数据的问题,因为QueryParser与表格栏相对应的QueryParser不会解释为“/ HDFS / PATH / PATE / PATTITION / IN /栏”。这个特殊问题是暂时使用正则表达式扩展,从HDFS路径中推断表名称。但是,通常,如果您选择绕过SQL Authate的SQL抽象,则支持文件系统层操作,然后选择退出QueryParser分析。

部署Queryparser

在Uber的非Haskell基础设施中部署Haskell服务需要一些小的创造力,但从来不会产生实质性的问题。

安装Haskell本身很简单。优步的标准基础架构模式是在Docker容器中运行每个服务。通过配置文件管理容器级依赖项,因此添加Haskell支持并作为添加堆栈到所需包列表。

QueryParser内部部署为Haskell工件,在Python Service包装后面运行,用于与其他Uber基础架构的互操作性。Python Wrapper充当代理服务器,并只需将哈克尔后端服务器转发到同一Docker容器中的请求。Haskell服务器由一个主线程组成,该主题侦听对a的请求UNIX-域插座;当新的请求到达时,主线程会产生一个工作线程来处理请求。

Python Wrapper还代表Haskell后端处理公制发射。度量数据通过第二个UNIX域套接字传递,数据以反向方向流动:a守护线程在Python层中侦听来自Haskell图层的指标。

为了在Python和Haskell图层之间共享配置,我们在Haskell实现了一个微型配置解析器,它将Uber的标准Python惯例描述为分层配置文件。

最后,为了定义服务接口,我们使用节约。这是Uber的标准选择,自节俭以来,Haskell Server从框中工作。编写Python代码以透明地前进请求需要潜入二进制协议,并且是最困难的操作步骤。

概要

QueryParser解锁了一种多样性的解决方案并具有一些有趣的限制。从其卑微的起源作为迁移工具,它成为洞察大规模数据访问模式的车辆。

如果您有兴趣在类似的项目上工作,请联系到za@uber.com.和/或通过UBER职业页面申请与我们在一起并告诉您的优步招聘人员您想在数据知识平台团队上工作。

终点:

剧透警告:最终有几十个主键需要迁移。每个主键可以在不同的别名下有许多外键。最厉害的罪犯有50多个化名。

²异关关系从明显的“选择*从foo加入bar上选择* foo.a = bar.b”到较少明显的明显“folfo.a,其中foo.a中的foo.a in(从栏中选择b)”的较小显而易见喜欢“从Foo Union选择B栏中选择A”。我们在自由主义方面遇到了我们被视为一种关系,因为无论如何将手动检查产出。

给定SQL“w.x.y。”z ",哪个标识符是列名?根据目录状态和作用域中的内容,它可以是带有“x.y”的“w”。“z”指嵌套的结构字段,或者它可以是“z”和“w.x”。y”指的是“database.schema”。或介于两者之间的任何东西。

评论