news 2026/9/24 1:14:28

PRQL 语言设计哲学:从“翻译成 SQL“走向“以关系为中心“的思考方式

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
PRQL 语言设计哲学:从“翻译成 SQL“走向“以关系为中心“的思考方式
  • 后端

【免费下载链接】prql

PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement

项目地址:https://gitcode.com/gh_mirrors/pr/prql
点击查看免费下载

PRQL 是一门面向数据转换的现代语言,其编译器本质上是把 PRQL 翻译成 SQL 的转译器;但官方语言设计指南明确指出,语言设计绝不能以"这个特性怎么翻译成 SQL"为出发点,而应以"这个特性如何作用于关系(relation)"为中心。本文以 language-design.md 为核心骨架,结合 prqlc 编译器的真实源码与测试,剖析这一设计原则的来龙去脉,帮助你建立正确的 PRQL 语言设计视角——无论你是想为 PRQL 提交新特性提案,还是想深入理解编译器内部如何把一条条流水线翻译成最终 SQL。

一、背景:PRQL 首先是一个"转译器"

PRQL 在项目中的定位是"SQL 的流水线式替代品"(a simple, powerful, pipelined SQL replacement)。在实现层面,它确实是一个把 PRQL 文本编译成 SQL 文本的转译器:prqlc提供的顶层入口 compile 函数 接收一段 PRQL 字符串,依次经过prql_to_pl(解析成 PL AST)、pl_to_rq(语义分析与降级)、rq_to_sql(生成 SQL)三个阶段后返回 SQL 字符串。

正因为"最终要产出 SQL"这一事实如此显眼,语言设计很容易滑向一种以 SQL 为中心的思维惯性,把每个 PRQL 特性都先想象成"它对应哪段 SQL":

PRQL feature -> SQL feature -> relational result

官方语言设计指南明确指出这种思路是有缺陷的,原因有两点:

  1. 它无法很好地建模特性之间的交互。两个特性单独看都能翻译成合理的 SQL,但当它们组合在一起时,翻译结果可能出现语义偏差,而"逐个特性翻译"的思维框架根本看不到这一点;
  2. SQL 行为本身有时具有误导性,甚至在不同方言之间不一致。例如子查询中的排序不会在父查询中保留;集合运算(UNION、INTERSECT、EXCEPT)在方言之间也存在行为差异。

二、为什么"以 SQL 为中心"是错的:来自源码的佐证

2.1 特性交互:sortgroupjoin的组合陷阱

官方文档指出"它不能很好地建模特性之间的交互"。仓库里恰好有一个回归测试可以当作活教材。在 lib.rs 的test_sort_not_propagated_after_join测试中,注释直接引用了 PRQL 规范:

Per PRQL spec,groupresets the order. Thesortinside a group is for row selection (which row to keep), not output ordering. After the group, there is no defined order, so it should not appear in the outer query.

翻译过来:按照 PRQL 语义,group会重置排序;group内部的sort只用于"选哪一行保留"(即行选择),而不是输出排序;group之后没有定义好的顺序,所以它不应该泄漏到外层查询

看这条测试对应的 PRQL 与生成 SQL 的对照:

prql target:sql.postgres from tracks group media_type_id ( sort name take 1 ) join media_types (== media_type_id) select { tracks.track_id, media_types.name }

生成的 SQL 中,sort name只出现在 CTE 内部的DISTINCT ON行选择逻辑里,外层JOIN之后的 SELECT 没有任何ORDER BY

WITH table_0 AS ( SELECT DISTINCT ON (media_type_id) track_id, media_type_id, name FROM tracks ORDER BY media_type_id, name ) SELECT table_0.track_id, media_types.name FROM table_0 INNER JOIN media_types ON table_0.media_type_id = media_types.media_type_id

而 同文件中的另一个测试test_explicit_sort_after_distinct_on_preserved则验证了相反的情况:如果用户在group之后显式sort media_type_id,这个新引入的顺序就应该穿透join一直传播到最终输出。这两条测试一正一反,正是"特性交互必须用关系语义来定义、而不能靠逐个翻译 SQL 来凑"的绝佳证据——如果设计者脑子里只有"sort → ORDER BY"这一条映射,就根本无法回答"group内的 sort 和group外的 sort 到底该落在生成的哪一层"。

2.2 SQL 行为的误导性:子查询的顺序不会保留

官方文档指出"SQL 行为有时会误导人:子查询中的顺序不会在父查询中保留"。这可以从编译器的 RQ(Relational Query)中间表示中得到印证:在 RQ 层面,排序是关系的一种显式属性Sort是关系变换列表中的一个独立变换;而在 SQL 层面,子查询里的ORDER BY只有在LIMIT/OFFSET伴随下才有确定语义,一旦进入父查询,子查询的排序约定就会丢失。因此编译器不能在"翻译到最后一步"之前就依赖 SQL 子查询的排序行为,而必须在语义层先把顺序表达为关系上的确定属性,再由 SQL 生成阶段决定把它物化到哪里(CTE、窗口函数还是最外层ORDER BY)。

2.3 方言差异:集合运算与DISTINCT ON

官方文档还指出 SQL 行为"在不同方言之间不一致(集合运算)"。prqlc 对此的应对是:把方言相关的决策尽量推迟到编译管线最末端的 SQL 后端阶段,而不是让语言语义去迁就某个方言。仓库中的 sql/dialect.rs 定义了DialectSupportLevel枚举,SQL 后端针对不同方言采用不同的生成策略;例如上面测试里出现的DISTINCT ON就是 PostgreSQL 方言特有的写法,它只在target:sql.postgres下才会被生成,其他方言会走各自的等价实现。语言本身不关心这些差异——关系语义只有一个,方言差异是"最后一公里"的事。

三、正确的视角:PRQL 特性作用于关系

官方文档给出了替代方案:我们应该思考 PRQL 特性如何影响 PRQL 表达式——在绝大多数情况下,也就是如何影响关系

PRQL feature -> relation | v PRQL feature -> relation | v PRQL feature -> relation | v relational result

这条流水线式的图示与 PRQL 语言本身的形态完全同构:PRQL 的一条查询就是由fromselectfiltergroupjoin等变换(transform)构成的管线,每一个变换都接收一个关系、产出一个新关系,最终得到关系结果。语言特性(比如sorttakewindow)首先应该被理解为"对当前关系的某种操作",而不是"对 SQL 的某种翻译"。

3.1 源码中的"关系":RQ AST

这条原则在编译器中间表示里体现得极为直接。RQ(Relational Query)是编译器在语义分析后得到的"严格类型化、用于描述关系查询"的 AST(见 ir/rq/mod.rs)。其核心结构是:

  • RelationalQuery:由def(查询定义)、tables(表声明集合)与relation(主关系)组成;
  • Relation:包含kindcolumns(列定义,即该关系对外暴露的接口);
  • RelationKind:关系的种类,其中Pipeline(Vec<Transform>)表示一条由多个变换组成的关系管线。

而变换本身是一组封闭的、与 SQL 无关的语义操作,见 ir/rq/transform.rs 中的Transform枚举:

pub enum Transform { From(TableRef), Compute(Compute), Select(Vec<CId>), Filter(Expr), Aggregate { partition: Vec<CId>, compute: Vec<CId> }, Sort(Vec<ColumnSort<CId>>), Take(Take), Join { side: JoinSide, with: TableRef, filter: Expr }, Append(TableRef), Loop(Vec<Transform>), }

注意这里没有任何"SELECT 关键字""ORDER BY 子句"之类的 SQL 词汇——FilterSortTakeAggregateJoin描述的都是对关系的变换,这正是"PRQL 特性 → 关系"原则在实现层的直接体现。

3.2 语义层的关系状态:Lineage(谱系)

在语义分析阶段(PL 层面),编译器为每一个变换维护"当前关系"的完整描述,这就是 ir/pl/lineage.rs 中定义的Lineage。它的文档注释写得很直白:

Represents the object that is manipulated by the pipeline transforms. Similar to a view in a database or a data frame.

——"表示被管线变换所操作的对象,类似于数据库中的视图或一个数据框。"Lineage 记录了当前关系的列集合:每个列要么是Single(单个列,带有名字与定义它的表达式 id),要么是All(来自某个输入的整组列,支持except排除集,对应foo_table.*语法)。语义解析器(semantic/mod.rs 中的resolve)在逐条处理语句时会同步更新这个 Lineage——一个变换作用于关系、产生新关系,然后下一个变换再作用于这个新关系,这与官方文档第二张图的链条完全一一对应。

3.3 从源码注释到设计共识

在 lowering.rs 的模块注释中,编译器明确写下了降级(PL → RQ)过程要保证的语义不变量:

  • transforms 不会被嵌套(每个变换都是关系层面的一层操作);
  • transforms 具有正确的 partition、window 与 sort 设置;
  • 不存在未解析的表达式。

这组不变量本质上就是"以关系为中心"的工程化落地:语言特性必须在语义层就被规范化成对关系的确定操作,而不是把问题留给 SQL 生成阶段去"碰运气"。

四、SQL 只在最后一步介入

官方文档的结论是:"只有在最后一步——当关系(或者说关系表达式)被翻译成 SQL 表达式时——才需要开始思考 SQL。"这一论断与 prqlc 的编译架构(ARCHITECTURE.md)完全吻合。编译器划分为三大阶段:

阶段子阶段使用的 AST
parselexer / parserLR(Lexer Representation)→ PR(Parser Representation)
semanticast_expand / resolver / flatten / loweringPR → PL → RQ
sqlpreprocess / pq-compiler / postprocess / sql-compiler / codegenRQ → PQ →sqlparser::ast→ string

三个阶段里,只有最后一个sql阶段才接触 SQL。semantic阶段的全部工作(名称解析、声明提取、类型推断、Lineage 计算、PL 降级为 RQ)都不依赖任何 SQL 概念;RQ 之后,sql/mod.rs 的compile函数才接手:把 RQ 中的每个关系转换为一个 SQL 查询,把管线在合适位置切分成可以用单个 SELECT 表达的小段(即"AtomicPipelines"),最后按方言生成文本。

这带来了一个重要的实践含义:语言设计者在思考新特性时,应当先在"关系"这一层把语义定义清楚,SQL 只是最后的表达手段。只要关系语义是确定的、自洽的,无论最终要适配哪个 SQL 方言、是生成 CTE 还是嵌套子查询,都只是sql阶段的技术决策,而不会反过来推翻语言设计。

4.1 一个例子:名称解析的"最后一公里"

参考文档 name-resolution.md 的 "Translating to SQL" 一节是这条原则的绝佳注脚:PRQL 的作用域规则完全是关系层面的——fromjoin引入的表会作为命名空间注入作用域,命名空间里是该表的所有列,此外还有一个无名的特殊命名空间("frame")装着当前变换正在操作的关系的列。

至于"生成的 SQL 里列名要不要带表前缀",完全是 SQL 翻译层的决定:当查询只引用一张表时,不需要前缀(select first_name直接生成SELECT first_name);当存在多张表且无法确知所有列归属时,才给所有列名加上表前缀以防歧义。也就是说,"有没有前缀"是 SQL 生成阶段根据歧义风险做出的工程取舍,并不改变 PRQL 语言本身的语义——语言层面,first_name始终通过作用域解析到唯一确定的列。

五、把原则落到实践:评估一个新 PRQL 特性

把官方设计指南转化为可操作的设计流程,大致如下:

  1. 问关系层面的问题:这个特性输入一个关系时,输出什么关系?它改变列(如deriveselect)、改变行(如filtertake)、改变分组与聚合状态(如groupaggregate),还是改变顺序(如sort)?它和前后相邻的变换如何组合?
  2. 用 PRQL 语义先定义清楚,例如groupsort只用于行选择、不决定输出顺序,这是关系语义层的规定(详见 group 标准库文档 与相关回归测试);
  3. 最后才考虑 SQL 表达:这个关系语义在目标方言下如何落地?是直接映射、用 CTE,还是需要拆分管线(如窗口函数在 WHERE 子句中无法物化时触发拆分,见 ARCHITECTURE.md 中关于 anchoring 的说明);
  4. 用反例检验:如果按"逐特性翻译成 SQL"的思路会得出错误组合结果,说明这个特性还没有被定义清楚,应该回到关系层面继续推敲。

六、参与 PRQL 语言设计的正确姿势

如果你希望亲自参与 PRQL 的语言设计,仓库的贡献指南给出了一套与本文原则呼应的做法:

  • 新语言特性在 GitHub issue 中决策,通常挂有language-design标签;讨论的核心就是"这个特性如何作用于关系"这类语义问题,而不是 SQL 怎么写;
  • 发现编译器产生错误结果时提交 bug 报告,可以使用仓库内的 playground 前端(web/playground目录)快速复现查询并导出生成 SQL;
  • 收集难以用 PRQL 表达的查询示例(尤其是比 SQL 更难表达的情况),并贴到相关 issue 中——官方强调,脱离示例的建议很难被认真对待,任何建议都应当锚定在具体示例上
  • 想动手写编译器代码的话,可以沿 development.md 与 ARCHITECTURE.md 入手,编译器(prqlccrate,位于 prqlc/prqlc)用 Rust 编写,其 PL/RQ 中间表示正体现了本文所述的设计思想。

七、总结

PRQL 语言设计指南的核心只有一句话:把每个特性想成"对关系的操作",而不是"对 SQL 的翻译"。以 SQL 为中心的视角会遮蔽特性间的交互、被 SQL 本身的误导性行为带偏、并且无法统一处理方言差异;而以关系为中心的视角让语言语义保持自洽,SQL 只在编译管线的最末端作为表达手段登场。prqlc 的架构(PL 语义层维护 Lineage、RQ 用Pipeline(Vec<Transform>)描述关系变换、SQL 后端在最后阶段才处理方言与物化策略)正是这一设计哲学的工程实现。带着这个视角去阅读 language-design.md、去 review 新的语言特性提案,你会发现 PRQL 的设计讨论始终围绕一个清晰的问题展开:这个特性,让关系发生了怎样的变化?

  • 后端

【免费下载链接】prql

PRQL is a modern language for transforming data — a simple, powerful, pipelined SQL replacement

项目地址:https://gitcode.com/gh_mirrors/pr/prql
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/24 1:09:09

腾讯云轻量服务器免费升配技术解析与实操指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/24 1:08:38

Python机器学习文本分类器实战:从数据清洗到模型部署全流程

简介&#xff1a;这份资源是面向NLP入门者与机器学习实践者的Python文本分类项目包&#xff0c;围绕文本自动归类这一核心任务&#xff0c;覆盖从数据预处理、特征工程到模型训练与评估的完整链路。包内共30个文件&#xff0c;以27个py脚本为主体&#xff0c;辅以1个md说明、1个…

作者头像 李华
网站建设 2026/9/24 1:07:27

Python股票K线数据校验与清洗:量化回测避坑指南

说实话&#xff0c;我用 Python 折腾股票历史 K 线的这几年&#xff0c;最深的感触是&#xff1a;大多数量化策略跑出来的结果很漂亮&#xff0c;但一模拟盘就露馅&#xff0c;问题往往不在策略逻辑&#xff0c;而在最底层的 K 线数据压根没洗干净。很多人以为从 AkShare、Tush…

作者头像 李华
网站建设 2026/9/24 0:58:45

2026年长春高性价比空气能采暖及热水工程专业公司实力与用户口碑

在长春气能采暖及热水工程&#xff0c;怎么选靠谱的服务商? 吉林高寒地区气能项目&#xff0c;容易踩哪些选型坑? 需要同时覆盖采暖、制冷和热水&#xff0c;能不能只用一套系统? 长春本地气能工程&#xff0c;售后响应速度能保障吗?很多吉林地区的业主、企业负责人找空气能…

作者头像 李华