SQL 方言迁移最坑的不是报错,是那些不报错的

2026-08-13 · SQL, 数据库, 迁移

把一堆 SQL 从 MySQL 搬到 PostgreSQL、从 Oracle 搬到 Doris、从 Snowflake 搬到 BigQuery—— 这活最难的部分从来不是那些跑不起来的语句。跑不起来的会报错,报错就会被修。

真正会带进生产的,是那些照跑不误、结果不一样的。

下面是我拿 12 组真实语句实测出来的,挑几条最阴的说。

一、LIMIT 10, 20 换个库就是另一页

MySQL 里这么写分页:

``sql SELECT IFNULL(nick, name) FROM users ORDER BY id LIMIT 10, 20 ``

MySQL 的 LIMIT a, b先跳过 a 条,再取 b 条。搬到 PostgreSQL,正确写法是:

``sql SELECT COALESCE(nick, name) FROM users ORDER BY id LIMIT 20 OFFSET 10 ``

注意两个数字换位了。而 PostgreSQL 也认 LIMIT 20 这种单参数写法——所以如果有人手抄成 LIMIT 10 OFFSET 20,或者干脆漏掉一个数,数据库不会有任何抱怨,只是从此以后每一页都是错的。 分页错位这种 bug,通常要等用户投诉"第 2 页有条记录看不见了"才被发现。

二、NULL 排在最前还是最后,两家默认相反

同一条语句转过去,ORDER BY id 变成了 ORDER BY id NULLS FIRST

这不是转换器多事。MySQL 默认把 NULL 排在最前,PostgreSQL 默认排在最后。 只要排序列里 有 NULL,同一条 ORDER BY 在两个库里给出的顺序就是不同的。你如果只取前 N 条,取到的 就是不同的行。

同一组测试里,T-SQL 的 ORDER BY id DESC 转到 PostgreSQL 补的是 NULLS LAST—— 方向还不一样。这种事全靠记是记不住的。

三、格式化字符串是另一门语言

```sql

-- MySQL SELECT DATE_FORMAT(created_at, '%Y-%m-%d') FROM orders

-- PostgreSQL SELECT TO_CHAR(CAST(created_at AS TIMESTAMP), 'YYYY-MM-DD') FROM orders ```

%Y-%m-%dYYYY-MM-DD 是两套完全不同的占位符体系。函数名要换、格式串要换, 还得补一个显式 CAST。这条至少会报错(DATE_FORMAT 在 PG 里不存在),算是仁慈的。

四、国产库这边,参数顺序会翻过来

PostgreSQL → Doris:

```sql

-- 原文 date_trunc('month', ts)

-- Doris DATE_TRUNC(ts, 'MONTH') ```

参数顺序是反的。 同名函数、同样两个参数、顺序相反——这类差异在 Doris / StarRocks 迁移里成片出现。MySQL 的 JSON_EXTRACT(payload, '$.user.id') 到 StarRocks 也变成了 payload -> '$.user.id'

五、说说它治不了的

这才是我觉得该写清楚的部分。PostgreSQL 的条件聚合:

``sql SELECT count(*) FILTER (WHERE status='ok') FROM jobs ``

转成 MySQL,输出的还是 COUNT(*) FILTER(WHERE status = 'ok')——而 MySQL 根本不支持 FILTER 语法。正确做法是改写成 SUM(status = 'ok')COUNT(CASE WHEN … END), 这是语义改写,不是语法翻译,转换器没做。

所以结论要说全:方言转换能帮你抓住语法层面的差异,替不了你对目标库的了解。 它的价值在于把"我根本没意识到这里有差别"变成"我看到了这里有差别"。

想自己试

我把 sqlglot 挂成了一个可以直接 curl 的接口,免注册、有免费额度:

`` curl "https://ainetcafe.com/t/transpile_sql?sql=SELECT%20NOW()&read=mysql&write=postgres" ``

参数就三个:sqlread(源方言)、write(目标方言)。支持 mysql / postgres / sqlite / tsql / oracle / snowflake / bigquery / redshift / spark / hive / presto / trino / duckdb / clickhouse / databricks / doris / starrocks 等。语法错误会返回精确到行列号的位置, 而不是一句"parse error"。

它是确定性解析器,不是大模型——同样的输入永远给同样的输出,不会今天一个样明天一个样。 这在迁移场景里挺重要:你需要的是可复现的批处理,不是每次都要人复核的猜测。

如果你手上是几百个文件的迁移,可以让 AI agent 挂上 MCP 直接批量调用 (https://ainetcafe.com/mcp,工具名 transpile_sql),它自己会循环。

---

利益披露:ainetcafe.com 是我做的。上面这个转译接口免费用、有匿名额度,底层是开源的 sqlglot(MIT),我只是把它托管起来、加了行列号报错。 本文里的每一条输出都是我在上线后一条条跑出来的——包括第五节那条它治不了的。 挂这个接口的起因是:sqlglot 本身很成熟,但它是个 Python 库,你想临时验一句 SQL 得先装环境;而"临时验一句"恰恰是最常见的需求。

给你的 agent 接上 AI 网吧(一行 MCP)→