结构化查询语言 (SQL)可能是工程师与数据交互最常用的方式。几乎所有数据服务都提供 SQL 接口。它通常被认为是数据科学家必备的技能,对数据工程师而言更是如此。SQL 具有可移植性、广泛应用、高度标准化和强大的功能。

尽管 SQL 优势显著,数据科学家通常更喜欢使用称为“数据框(dataframes)”(例如 pandasPolars )的工具来进行数据分析。这不仅是因为dataframes工具的生态系统非常丰富,还因为dataframes允许他们以一种通常比 SQL 更自然的方式表达数据分析任务。

鉴于 Polars 和 SQL 的流行程度,我们将探讨它们之间的转换效果。给定一段 Polars 代码,是否可以将其转换为 SQL?转换后的输出是否相同?本文并非旨在进行全面的基准测试,而是着重探讨以下几个方面:

  • LLM的回答是否存在某种模式,这种模式是否具有一定的可预测性?
  • 我们可以采取哪些措施来提高 LLM 的回答质量?
  • 我们可以使用开源的 LLM 来完成这项任务吗?
  • 是否有非 LLM 解决方案?

提示词(Prompt)

我们将提出一个提示词,该提示词涉及将 Polars 语法转换为 SQL 的几个方面:聚合、空值处理和广播。提示词如下:

给定一个包含值的表 df

{'price': [1, 4, None, 4]}

你能把这段 Polar 代码翻译成 SQL 吗?

print(df.select(pl.col('price') - pl.col('price').mean())) # Task 1
print(df.select(pl.col('price').n_unique()))               # Task 2
print(df.select(pl.col('price').rank('dense')))            # Task 3

预期结果

让我们来看一些正确翻译的例子。第一个例子要求我们将聚合值( AVG(price) )与列( price )进行比较,我们可以通过使用 OVER () 来广播聚合值来实现这一点:

SELECT
     price - AVG(price) OVER () AS price_centered
FROM df;

对于第二个问题, COUNT(DISTINCT price) 结果比较接近,但需要注意的关键细节是,Polars 默认将空值包含在 n_unique 中,而 COUNT(DISTINCT ...) 则不会。因此,需要进行一些额外的后处理——一种方法是使用 CASE-WHEN 表达式:

SELECT 
    COUNT(DISTINCT price) 
    + MAX(
        CASE 
            WHEN price IS NULL THEN 1 
            ELSE 0 
        END
      ) AS price
FROM df;

对于最后一个问题,Polars 会保留空值,只对非空值进行排名,因此我们正在寻找类似这样的解决方案:

SELECT
    price,
    CASE
        WHEN ((NOT (price IS NULL))) THEN (dense_rank() OVER (ORDER BY price ASC NULLS LAST))
        ELSE NULL
    END AS d
FROM df;

这些是模型答案的示例。你认为LLM的答案能与这些示例有多接近?

认识一下LLM

为此,我们将比较三款免费模型:

  • 我们将使用 OpenAI 的 GPT-5.1(专有)版本在 ChatGPT 上运行。
  • DeepSeek V3.1(开源,MIT 许可证)。我们将在 OpenRouter 上运行它。
  • 阿里巴巴的 Qwen3 Coder 480B A35B(开源,Apache 2.0 许可证)。我们也会在 OpenRouter 上运行它。

第一个是大家都知道的。可惜的是,对很多人来说,他们对 LLM 的了解也就止步于此了。很多人甚至不知道还有开源的替代方案——让我们改变这种现状!

对 LLM 进行测试

以下是截至 2025 年 12 月 26 日 LLM 项目结果的总结:

任务1

  • GPT:正确!
  • Deepseek:正确!
  • Qwen:答对了!好吧,这道题对他们来说太简单了。我们来做更难的题吧。

任务 2

  • GPT:错误!
  • Deepseek:错误!
  • Qwen:不对!他们都犯了同样的错误,生成的查询语句大致如下:
SELECT 
    COUNT(DISTINCT price) AS n_unique
FROM df;

错误之处在于它会丢弃空值,而 Polars 则会包含空值。盲目信任 LLM 的转换结果而不进行检查可能会导致生产故障或错误的业务决策!

任务3

  • GPT:正确!
  • Deepseek:错误!
  • Qwen:错误!后两者犯了同样的错误,输出类似这样的内容:
SELECT DENSE_RANK() OVER (ORDER BY price) FROM df;

错误的原因在于它将空值排在最后,而 Polars 会保留空值,只对非空元素进行排名。GPT 能够正确处理这一点,并输出:

SELECT
    CASE
        WHEN price IS NULL THEN NULL
        ELSE DENSE_RANK() OVER (ORDER BY price)
    END AS dense_rank_price
FROM df;

请注意,这里增加了保留空值的逻辑,而其他两个逻辑中则缺少这个逻辑。

改进提示词能否改善结果?

LLM 的故障确实存在某种模式:它们生成的代码看起来合情合理。唯一的问题是,仔细检查后发现,诸如空值处理之类的细节并非总是得到妥善处理。我们能否通过提醒 LLM 注意 Polar 的行为细节来解决这个问题呢?

答案是肯定的!实际上,通过附加……

请记住,Polars 模型会在 n_unique 中统计空值,并在 rank 中保留空值。

根据我们的提示词,我们发现所有三个模型都能对所有任务给出正确的结果!因此,可以使用这些模型将 Polars 数据转换为 SQL,但这需要一些技巧和领域知识来确保转换的正确性。

非人工智能解决方案:Narwhals

LLM(逻辑逻辑模型)极易产生幻觉,其输出结果绝不可盲目信任。如果我们想要一个更稳健、更可预测的解决方案,可以考虑使用名为 Narwhals 的开源工具。Narwhals 是一个轻量级的数据帧库兼容层——尤其支持 DuckDB,因此我们可以用它来生成 SQL。

以下是 Narwhals 对上述第二个任务(所有 LLM 都出错的任务)的解决方案示例:

import polars as pl
import narwhals as nw
data = {'price': [1, 4, None, 4]}
df = pl.DataFrame(data)
print(
    nw.from_native(df).lazy('duckdb')
    .select(nw.col('price').n_unique())
    .to_native().sql_query()
)

输出结果的第一行显示

SELECT
  (count(DISTINCT price)
   + max(CASE  WHEN ((price IS NULL)) THEN (1) ELSE 0 END)) AS price
FROM ColumnDataCollection - [1 Chunks, 4 Rows]

而且这是正确的 SQL 转换——无需手动提示词工程!这种方法安全可靠,经过充分测试,不会产生任何不切实际的后果。缺点是它仅限于 Narwhals API 中包含的功能,而 LLM 至少可以尝试转换更复杂、更专业的查询。

如果您想资助未来与dataframes无关的工作流,或者需要定制 Narwhals 解决方案方面的帮助,可以联系 Quansight

结论

我们研究了如何将 Polars 代码转换为 SQL,并比较了不同的解决方案:

  • 专有人工智能模型
  • 开源人工智能模型
  • Narwhals

我们发现,AI 模型可能会出现一些细微的错误,但可以通过更好的提示词工程进行纠正。最后,我们发现 Narwhals 无需提示词工程即可进行正确的翻译,但这种方法仅限于 Narwhals API 中已实现的功能。如果您计划使用 LLM 将 Polar 转换为 SQL,我们建议您首先尝试 Narwhals,以避免 LLM 出现“幻觉”(即模型无法正确翻译)。只有在 Narwhals API 无法满足您的任务需求时,才考虑使用 LLM。在这种情况下,请务必尽可能详细地描述预期行为,以便模型能够最大限度地提高翻译的准确性。

可前往笔者微信公众号原文交流:LLM 能否将 Polars 代码转换为 SQL

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐