1.简介

SQLGlot 可以让你对代码库中的 SQL 实现程序化理解。它能够创建测试和检查,以深入分析你的 SQL,并在源头识别错误或反模式。SQLGlot 提供了标准化不同数据仓库中 SQL 结构的 API,使它成为开发者的多用途工具。

目前已经支持24种不同的sql语法了,就凭这个,也太值得用了。

这是它的git地址 tobymao/sqlglot: Python SQL Parser and Transpiler ,目前已经6.8K stars。

2.功能

stars那么多,功能上也十分强大。

  • 格式化和翻译

  • 元数据

  • 解析器错误

  • 不支持的错误

  • 构建和修改 SQL

  • SQL 优化器

  • AST 自检

  • AST Diff

  • 自定义方言

  • SQL 执行

最近在研究元数据做血缘,因此接下来结合一些案例来聊聊元数据相关内容,避免使用者踩坑。

3. 细说元数据

3.1 浅尝元数据解析

根据sql解析库、表信息 参考链接2

from sqlglot import parse_one, exp
query = """
SELECT
col1
,col2
,col3
FROM db1.table1
"""
for table in parse_one(query).find_all(exp.Table):
  print(f"Table => {table.name} | DB => {table.db}")

执行上述python代码,很容易获取库表关系。当然 如果使用了别名,会被忽略,获取真正的库表名称。

根据sql解析字段

from sqlglot import parse_one, exp
query = """
SELECT
col1
,col2
,col3
FROM db1.table1
"""
for column in parse_one(query).find_all(exp.Column):
  print(f"Column => {column.name}")

上述代码,能过获取所有字段级别的信息。

这些案例也可以在sqlglot的api文档中获取, sqlglot API documentation

不过我想吐槽一句,文档写的过于简单,且不容易理解。

3.2 复杂场景

以下案例只针对于表级别,实际应用中库表级别是有意义的。

场景一:复杂sql,多join

这种场景用上一张的案例依然是可以解决的,因此不展开讲

场景二:insert加复杂sql,多join

这种场景虽然可是使用第一个案例来解析,但是无法区分source表和target表。

# 获取target表
sql = '''   '''
target_tables = set()
for insert in parse_one(sql, read=db_type).find_all(exp.Insert):
    for table in insert.find_all(exp.Table):
        target_tables.add(table.name)

如果说要同时获取source和target 那就需要麻烦一点

def get_sqllineage_select(sql,db_type):
    db_type = 'mysql'
    source_tables = set()
    target_tables = set()
    for insert in parse_one(sql, read=db_type).find_all(exp.Insert):
        for table in insert.find_all(exp.Table):
            target_tables.add(table.name)
    for select in parse_one(sql, read=db_type).find_all(exp.Select):
        for table in select.find_all(exp.Table):
            if table.name in target_tables: target_tables.remove(table.name)
            source_tables.add(table.name)

    return {'source_tables':list(source_tables),'target_tables':list(target_tables)}

为了方便使用,这里写成了一个方法。 db_type 是可以指定数据库类型的,如mysql,doris等数据库。

场景三:CTE+复杂多join查询

好在sqlglot有针对cte类型查询的类型

query = """
with tab1 as
(
  select a,b from db1.table1
)
,tab2 as
(
  select a from tab1
)
,tab3 as
(
  select
  t1.a
  ,t2.b
  from tab1 t1
  join tab2 t2
  on t1.a = t2.a
)
select
*
from tab3
"""

dependencies = {}

for cte in parse_one(query).find_all(exp.CTE):
  dependencies[cte.alias_or_name] = []

  cte_query = cte.this.sql()
  for table in parse_one(cte_query).find_all(exp.Table):
    dependencies[cte.alias_or_name].append(table.name)
print(dependencies)

-- Output: {'tab1': ['table1'], 'tab2': ['tab1'], 'tab3': ['tab1', 'tab2']}

这样输出就会获取字典类型的表,他把所有的临时查询作为一个表名,输出临时表和临表里的表名。

输出结果:

{'tab1': ['table1'], 
 'tab2': ['tab1'], 
 'tab3': ['tab1', 'tab2']}

当然这不是我想要的,我想要实体表。其实很简单把上面的字典修改一下即可。

source_tables = set()
for key in dependencies:
    for table in dependencies[key]:
        if table not in dependencies: source_tables.add(table)

这样就能获取实体表。

场景四:insert+CTE+复杂多join查询

这种场景的sql,大部分的样子是insert在最外层,然后里面套了cte的复杂查询。

因此需要结合上面几种方式来实现。

  • 解决insert的target表
  • 按照CTE的当时再解析一次
  • 综合以上分析分解出target和source
def get_sqllineage_cte(sql,db_type):
    dependencies = {}
    for cte in parse_one(sql, read="doris").find_all(exp.CTE):
        dependencies[cte.alias_or_name] = []

        cte_query = cte.this.sql()
        for table in parse_one(sql, read="doris").find_all(exp.Table):
            dependencies[cte.alias_or_name].append(table.name)
    target_tables = set()
    for insert in parse_one(sql, read="doris").find_all(exp.Insert):
        for table in insert.find_all(exp.Table):
            target_tables.add(table.name)

    source_tables = set()
    for select in parse_one(sql, read="doris").find_all(exp.Select):
        for table in select.find_all(exp.Table):
            if table.name in target_tables: target_tables.remove(table.name)
    for key in dependencies:
        for table in dependencies[key]:
            if table not in dependencies: source_tables.add(table)
    source_tables = source_tables - target_tables
    return {'source_tables':list(source_tables),'target_tables':list(target_tables)}

以上场景根据我个人使用来设想的,如果你有别的场景或者发现代码问题,请多多指教。

4.总结

个人感觉sqlglot比sqlparse、sqllinage要好用一些,实际使用中遇到过在sqlparse、sqlineage中无法解析的sql,在sqlglot中依然能够解决。不过sqlglot的文档写的有点麻烦,且国内资料不多,实际使用中可能会些门槛。目前很多大项目已经在使用了,如datahub ,所以还是值得学习的。

▼关注「DataSpeed」了解更多大数据知识▼

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐