查询解析器是 Postgres 的核心组件:数据库需要了解您要求的数据才能返回正确的结果。但是此功能对于使用 Postgres 查询的各种其他工具也很有用。几年前,我们发布了 pg_query以在独立的 C 库中支持此功能。
pganalyze 使用 pg_query 来解析和分析在 Postgres 数据库上运行的每个 SQL 查询。我们最初的动机是创建 pg_query 来检查查询引用了哪些表,或者它是哪种语句。从那时起,我们扩大了它在 pganalyze 中的使用。pganalyze 现在可以在查询概览中以智能方式截断查询文本。该pganalyze集电极支持收集EXPLAIN计划,并采用pg_query到基于日志的支持EXPLAIN。而我们连在一起pg_stat_statements,并auto_explain使用查询指纹在pganalyze数据(另一pg_query功能,我们将详细在后面的章节讨论)。
Postgres 社区工具建立在 pg_query 之上
但是,我们当时没想到的是,我们从社区中看到了巨大的兴趣。仅 Ruby 库在其生命周期中就获得了超过 350 万次下载。
感谢许多贡献者,pg_query 现在可以绑定 Ruby 和 Go 之外的其他语言,例如 Python(pglast,由Lele Gaifax维护)、Node.js(pgsql-parser,由Dan Lynch维护)甚至 OCaml。还有许多著名的第三方项目使用 pg_query 来解析 Postgres 查询。这里有一些我们的最爱:
- sqlc在 Go 中提供类型安全的基于 SQL 的数据库访问
- pgdebug可让您调试复杂的 CTE 并将部分作为独立查询执行
- Google 的 HarbourBridge使用 pg_query 帮助客户试用 Postgres 来源的 Spanner
- DuckDB使用 pg_query 的分叉版本作为解析层
- GitLab使用 pg_query 在其内部错误报告中规范化查询
- Splitgraph通过 pglast Python 绑定使用 pg_query 来解析 Splitfiles 中的 SQL 语句
- sqlint 会检查您的 SQL 文件的正确性
今天,是时候将 pg_query 提升到一个新的水平了。
发布 pg_query 2.0:更好更快的解析,支持 Postgres 13
我们很高兴地宣布 pg_query 的下一个主要版本,pg_query 2.0。
在此版本中,您将找到对以下方面的支持:
- 解析 PostgreSQL 13 查询语法
- 解析器作为核心 C 库的一部分,将修改后的解析树转换回 SQL
- 基于Protocol Buffers (Protobuf) 的新解析树格式
- 改进的、更快的查询指纹识别机制
- 以及更多!
pg_query 如何将 Postgres 语句转换为解析树
解析 SQL 的方法有很多,但是 pg_query 的作用域非常具体。也就是说,为了能够像 Postgres 一样解析完整的 Postgres 查询语法。唯一可靠的方法是使用 Postgres 解析器本身。
pg_query 不是第一个这样做的项目,例如 pgpool 也有 Postgres 解析器的副本。但是我们需要一个独立的 C 库中的解析器的易于维护、自包含的版本。这将使我们和 Postgres 社区通过编写一个简单的包装器来使用几乎任何语言的解析器。
我们是怎么做到的?我们首先查看 Postgres 源代码。查看源代码,您会发现调用的函数raw_parser:
/* * raw_parser * Given a query in string form, do lexical and grammatical analysis. * * Returns a list of raw (un-analyzed) parse trees. The immediate elements * of the list are always RawStmt nodes. */ List * raw_parser(const char *str) { ...
复制
原始解析后,Postgres 进入解析分析。在那个阶段,Postgres 识别列的类型,将表名映射到模式等等。之后,Postgres 进行规划(参见我们对Postgres 查询规划的介绍),然后根据查询计划执行查询。
对于 pg_query,我们只需要原始解析器。查看代码,我们发现了一个问题。解析器代码仍然依赖于很多 Postgres 代码,例如用于内存管理或错误处理。我们需要一种可重复的方式来提取足够的源代码来编译和运行解析器。
因此,自动提取 Postgres 解析器代码及其依赖项的想法诞生了。
使用 LibClang 从 Postgres 中提取 C 源代码
我们的目标:一组代表 Postgresraw_parser函数副本的自包含 C 文件。但是我们不想手动复制代码。幸运的是,我们可以使用LibClang来解析 C 代码,并了解其依赖关系。
这方面的细节可能会填满很多页,但这里是其工作原理的简化版本:
1. 源代码中的每个翻译单元(.c 文件)都通过 LibClang 的 Ruby 绑定进行分析:
require 'ffi/clang' index = FFI::Clang::Index.new(true, true) translation_unit = index.parse_translation_unit(file, ['... CFLAGS ...'])
复制
2. 分析遍历文件并标记每个 C 方法,以及它引用的符号:
translation_unit.cursor.visit_children do |cursor, parent| @file_to_symbol_positions[cursor.location.file] ||= {} @file_to_symbol_positions[cursor.location.file][cursor.spelling] = [cursor.extent.start.offset, cursor.extent.end.offset] cursor.visit_children do |child_cursor, parent| if child_cursor.kind == :cursor_decl_ref_expr || child_cursor.kind == :cursor_call_expr @references[cursor.spelling] ||= [] (@references[cursor.spelling] << child_cursor.spelling).uniq! end :recurse end end
复制
3. 我们根据我们正在寻找的顶级方法解析所需的 C 方法及其代码:
def deep_resolve(method_name, depth: 0, trail: [], global_resolved_by_parent: [], static_resolved_by_parent: [], static_base_filename: nil) ... global_dependents = (@references[method_name] || [] global_dependents.each do |symbol| deep_resolve(symbol, depth: depth + 1, trail: trail + [method_name], global_resolved_by_parent: global_resolved_by_parent + global_dependents) end ... end deep_resolve('raw_parser')
复制
4. 我们只写出需要的 C 代码部分(请参阅此处的详细信息)
有了这个,我们就有了一个可以工作的 Postgres 解析器!
您可以在pg_query 源中找到完整的详细信息。
一旦我们可以在我们的独立库中调用 Postgres 解析器,我们就可以得到解析树的结果,表示为 Postgres 解析器 C 结构。但是现在我们需要让它在其他语言中有用,比如 Ruby 或 Go。
将 Postgres 解析器 C 结构体转换为 JSON 和 Protobufs
这是一个鲜为人知的事实,但 Postgres 实际上有一个查询解析树的文本表示。它很少直接使用,保留用于内部通信和调试。查看示例的最简单方法是查看pg_attref中的adbin字段,它显示了列默认值表达式的内部表示(相比之下,显示了 SQL 中的表达式):pg_get_expr
SELECT adbin, pg_get_expr(adbin, adrelid) FROM pg_attrdef WHERE adrelid = 'mytable'::regclass AND adnum = 1; -[ RECORD 1 ]------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- adbin | {FUNCEXPR :funcid 480 :funcresulttype 23 :funcretset false :funcvariadic false :funcformat 2 :funccollid 0 :inputcollid 0 :args ({FUNCEXPR :funcid 1574 :funcresulttype 20 :funcretset false :funcvariadic false :funcformat 0 :funccollid 0 :inputcollid 0 :args ({CONST :consttype 2205 :consttypmod -1 :constcollid 0 :constlen 4 :constbyval true :constisnull false :location 68 :constvalue 4 [ -27 10 -122 1 0 0 0 0 ]}) :location 60}) :location -1} pg_get_expr | nextval('mytable_id_seq'::regclass)
复制
这种文本格式对于使用其他语言的解析树没有用处。因此,我们需要一种更可移植的格式来从 C 中导出解析树,并以另一种语言(例如 Ruby)导入它。
pg_query 的初始版本为此使用了 JSON。JSON 很棒,因为您几乎可以用任何编程语言解析它。因此,在这个新的 pg_query 版本中,我们仍然支持 JSON。
我们还引入了对新的基于模式的格式的支持,使用协议缓冲区 (Protobuf)。
为什么 pg_query 2.0 增加了对 Protocol Buffers 的支持
虽然 JSON 便于传递解析树,但它有一些问题:
- JSON 的解析速度比二进制格式慢
- 内存使用可能成为复杂解析树的问题
- 围绕 JSON 数据树构建逻辑容易出错,因为需要添加大量检查来识别每个节点及其支持的字段
- 很难实例化新的解析树节点,例如用于解析回 SQL 语句
在 pg_query 1.0 中,使用 Ruby 绑定访问“SELECT 1”的值看起来像这样:
result = PgQuery.parse("SELECT 1") result.tree[0]['RawStmt']['stmt']['SelectStmt']['targetList'][0]['ResTarget']['val'] # => {"A_Const"=>{"val"=>{"Integer"=>{"ival"=>1}}, "location"=>7}} 以下是 Protobuf 如何改进 Ruby 中的解析树处理: result = PgQuery.parse("SELECT 1") result.tree.stmts[0].stmt.select_stmt.target_list[0].res_target.val.a_const # => <PgQuery::A_Const: val: <PgQuery::Node: integer: <PgQuery::Integer: ival: 1>>, location: 7>
复制
请注意我们如何为每个解析树节点类型提供完整的类定义,从而使与树节点的交互变得更加容易。
现在,假设我想更改解析树并将其转换回 SQL 语句。为此,我需要一个解析器。
使用解析器将解析树转回 SQL
将 Postgres 解析树解析回 SELECT 语句的图示
Postgres 本身在很多地方都有解析器逻辑。例如 postgres_fdw 有一个解析器来生成要发送到远程服务器的查询。但是,Postgres 中的解析器代码需要一个解析后分析树(直接引用关系 OID 等)。这意味着我们不能在 pg_query 中使用它,它适用于原始解析树。
多年来,Ruby pg_query 库已经有了一个解析器。多年来,我们已经有许多社区贡献来完成它。Python 和 Node.js 的第三方库也有自己的解析器。这些工作都是并行完成的,没有共享代码。Go 库完全缺少解析器。
**我们如何减少社区中的重复工作?**通过为原始解析树创建一个新的便携式解析器。这避免了对每个基于 pg_query 的库进行重复工作。
覆盖所有 Postgres 回归测试的 pg_query 解析器
pg_query 2.0 有一个新的解析器,用 C 编写。这是这个新版本迄今为止最大的任务。新的解析器能够生成 Postgres 回归测试中使用的所有 SQL 查询(pg_query 解析器当然可以解析)等等。
它是这样工作的,这里以 Go 库为例,它之前没有解析器:
package main import ( "fmt" pg_query "github.com/pganalyze/pg_query_go/v2" ) func main() { // Parse a query result, err := pg_query.Parse("SELECT 42") if err != nil { panic(err) } // Modify the parse tree result.Stmts[0].Stmt.GetSelectStmt().GetTargetList()[0].GetResTarget().Val = pg_query.MakeAConstStrNode("Hello World", -1) // Deparse back into a query stmt, err := pg_query.Deparse(result) if err != nil { panic(err) } fmt.Printf("%s\n", stmt) }
复制
这将输出以下内容:
SELECT 'Hello World'
复制
首先,解析步骤将 Go 结构编码为新的 Protobuf 格式。然后,C 库将其解码为 Postgres 解析树 C 结构。最后但并非最不重要的是,C 库的新解析器将 C 结构转换为 SQL 查询文本。
远离解析,让我们来看看新的指纹识别机制:
pg_query 中的指纹:检查两个查询是否相同的更好方法
让我们从查询指纹的动机开始。pganalyze 需要将不同数据源的 Postgres 统计数据链接在一起。例如来自pg_stat_statements
Postgres auto_explain
日志的查询。在查询详情页可以看到pganalyze
中的指纹:
pganalyze Query Details 页面显示查询及其关联的指纹值
此查询可以根据您查看的 Postgres 的哪个部分以不同的方式表示:
- pg_stat_statements: SELECT “abalance” FROM “pgbench_accounts” WHERE “aid” = ?
- 自动解释: SELECT abalance FROM pgbench_accounts WHERE aid = 4674588
简单的文本比较不足以确定这些查询是否相同。
为什么我们要创建自己的查询指纹概念?
Postgres 已经有了“queryid”的概念,它是基于解析后的分析树计算出来的。它用于诸如pg_stat_statements
区分不同查询条目的地方。
但是,这个queryid 在今天并非随处可用,例如您无法通过auto_explain
计划获得它。它也不能在数据库之间移植,因为它依赖于特定的关系 OID。即使您在登台和生产系统上有完全相同的查询,它们也会有不同的 queryid 值。并且 queryid 不能在 Postgres 服务器的上下文之外生成。因此,pganalyze 有自己的机制,称为查询指纹。
指纹仅根据原始解析树来识别 Postgres 查询。我们在 pg_query 中开源了这个机制:
PgQuery.fingerprint('SELECT a, b FROM c') # => "fb1f305bea85c2f6" PgQuery.fingerprint('SELECT b, a FROM c') # => "fb1f305bea85c2f6"
复制
这种机制不需要正在运行的服务器,因此您需要的只是一个有效的 Postgres 查询作为输入。
在 pg_query 2.0 中,我们对指纹功能做了一些改进:
- 使用更快的 XXH3 哈希函数,而不是 SHA-1。pg_query 1.0 使用过时的加密哈希函数 SHA-1。此用例不需要加密保证,而 XXH3 快得多。
- 将指纹包含在 64 位值中,而不是 136 位。我们已经确定 64 位精度足以查询指纹。Postgres 本身也这么认为,因为它使用 64 位作为 Postgres queryid。我们经常使用来自 的数据pg_stat_statements,因此更多的位几乎没有什么好处。使用较小的数据类型也意味着 pganalyze 的性能更好。
- 修复两个几乎相同的查询具有不同指纹的边缘情况。当指纹导致相同的查询意图时,指纹应该忽略查询差异。我们已经解决了一些无法按预期工作的情况。您可以查看相应的wiki 页面以更详细地了解这些更改。
- pg_query 2.0 的其他更改
关于新版本的其他一些事情:
pg_query 库现在位于 GitHub 上的 pganalyze 组织中。这清楚地表明谁维护和资助核心开发。我们将继续在 BSD 3-clause 许可下使 pg_query 可用。
pg_query 有一个用于拆分查询的新方法。当您想将多语句字符串拆分为其组件语句时,这会很有用,例如SELECT ';'; SELECT 'foo'
拆分为SELECT ';'
和SELECT 'foo'
有一个新功能可用于访问 Postgres 扫描程序。这包括查询文本中注释的位置。可以设想基于此构建一个语法高亮器。或者从查询中提取评论,同时忽略常量值中的类似评论的标记。
结论
新的 pg_query 2.0 现已推出,可以开始使用Go和Ruby 的绑定。我们还在开发一个新的 pganalyze 维护的 Rust 绑定,我们很快就会有消息。
文章来源:https://pganalyze.com/blog/pg-query-2-0-postgres-query-parser