最近在处理计资名单账期中间表数据时,遇到一个比较隐蔽的问题:SQL 在 PostgreSQL 客户端中可以正常执行,但放到 MyBatis XML 里执行却失效或报错。
问题 SQL 大致如下:
ini
<delete id="deleteLockedLeaveJobs" parameterType="java.util.Map">
DELETE FROM xxx.salary_list_middle_monthly t1
USING xxx.salary_list t2
WHERE t1.task_unique_id = #{taskUniqueId}
AND t2.ytenant_id = #{ytenantId}
AND t1.staff_id = t2.staff_id
AND t1.period = t2.period
AND t2.dr = 0
AND t1.depart_date IS NOT NULL
AND t1.depart_date <![CDATA[ < ]]> #{today}
</delete>
这段 SQL 的业务含义是:
删除账期中间表中,已经离职且离职时间小于当前日期,并且计资名单中已经存在对应离职账期数据的人员记录。
一、现象
这段 SQL 直接放到 PostgreSQL 数据库工具里执行是正常的。
但是放到项目 MyBatis XML 中,通过程序执行时,可能出现:
sql
SQL 执行失败
SQL 被框架改写
SQL 结构被重新排序
DELETE USING 语法失效
一开始容易误以为是 PostgreSQL SQL 写法有问题。
但实际排查后发现,问题不在数据库本身,而更可能出在项目里的 SQL 解析、租户翻译、数据权限、SQL 改写等框架层。
二、原因分析
PostgreSQL 支持这种写法:
sql
DELETE FROM table1 t1
USING table2 t2
WHERE t1.id = t2.id;
这是 PostgreSQL 中常用的关联删除语法。
但是在某些项目框架里,MyBatis 执行 SQL 前,SQL 可能会经过一层或多层处理,例如:
sql
租户字段注入
SQL 翻译
数据权限拦截
SQL 解析器重写
多数据源适配
这些组件有时对 PostgreSQL 的 DELETE ... USING ... 语法支持不完整。
尤其是遇到下面这种结构时:
sql
DELETE FROM schema.table t1
USING schema.table2 t2
WHERE ...
框架可能会错误识别:
sql
表别名
USING 语法
DELETE 目标表
关联表
WHERE 条件归属
最后导致原本正确的 PostgreSQL SQL,在进入数据库前已经被框架改坏了。
所以这个问题的关键不是:
sql
PostgreSQL 不支持 DELETE USING
而是:
sql
项目框架中的 SQL 翻译/拦截逻辑没有正确处理 PostgreSQL 的 DELETE USING 语法
三、解决方式
最终处理方式是在 SQL 前加上跳过翻译的标记,并把别名写得更明确:
ini
<delete id="deleteLockedLeaveJobs" parameterType="java.util.Map">
/*!sql_no_translate*/
DELETE FROM xxx.salary_list_middle_monthly t1
USING xxx.salary_list AS t2
WHERE t1.task_unique_id = #{taskUniqueId}
AND t2.ytenant_id = #{ytenantId}
AND t1.staff_id = t2.staff_id
AND t1.period = t2.period
AND t2.dr = 0
AND t1.depart_date IS NOT NULL
AND t1.depart_date <![CDATA[ < ]]> #{today}
</delete>
关键改动有两个:
markdown
1. 增加 /*!sql_no_translate*/
2. USING 表别名改成显式 AS t2
其中最核心的是:
arduino
/*!sql_no_translate*/
它的作用是告诉项目框架:
这段 SQL 不要再进行自动翻译或改写,直接按原 SQL 执行。
AS t2 不是必须解决点,但可以让 SQL 语义更清晰,减少部分 SQL 解析器对别名的误判。
四、为什么数据库里能执行,项目里不行?
这个问题很典型。
数据库客户端执行时,链路是:
sql
SQL → PostgreSQL
而项目中执行时,链路可能是:
sql
MyBatis XML
→ MyBatis 参数替换
→ 框架 SQL 拦截器
→ 租户/权限/翻译处理
→ JDBC
→ PostgreSQL
所以你在数据库里验证通过,只能说明:
sql
SQL 本身符合 PostgreSQL 语法
但不能说明:
sql
这段 SQL 在项目框架链路中不会被改写
如果项目里有 SQL 拦截器、租户插件、数据权限插件、SQL 翻译组件,就要特别注意这种差异。
五、排查建议
遇到这类问题时,不要只盯着 SQL 本身,可以按下面顺序排查:
1. 先确认原 SQL 是否能在数据库中执行
如果数据库中都不能执行,那就是 SQL 写法问题。
如果数据库中可以执行,但项目中失败,继续往下查。
2. 打印最终执行 SQL
重点看程序最终发给数据库的 SQL,是否和 XML 里写的一致。
如果发现 SQL 被改写、排序、加条件、换别名,就说明中间有框架处理。
3. 检查是否存在 SQL 翻译/租户/权限拦截器
常见影响点包括:
sql
租户字段自动注入
数据权限 SQL 改写
分页插件
多数据库方言转换
SQL 翻译器
自定义 MyBatis Interceptor
4. 对 PostgreSQL 特有语法加跳过翻译标记
例如:
arduino
/*!sql_no_translate*/
尤其是这类 PostgreSQL 语法:
sql
DELETE ... USING ...
INSERT ... RETURNING ...
UPDATE ... FROM ...
DISTINCT ON (...)
LATERAL JOIN
IS DISTINCT FROM
这些语法在 PostgreSQL 中很常见,但某些通用 SQL 解析器未必支持完整。
六、踩坑总结
这次问题的核心结论是:
SQL 在数据库里能正常执行,不代表它在 MyBatis 项目中一定能正常执行。中间如果存在 SQL 翻译、租户注入、权限拦截等框架逻辑,SQL 可能在真正到达数据库前已经被改写。
对于 PostgreSQL 的特殊语法,尤其是:
sql
DELETE ... USING ...
如果项目框架存在 SQL 改写能力,建议:
markdown
1. 优先打印最终 SQL;
2. 确认 SQL 是否被框架改写;
3. 必要时使用 sql_no_translate 跳过翻译;
4. 表别名尽量写完整,例如 USING table AS alias;
5. 不要只用数据库客户端执行结果判断项目一定没问题。
这类问题表面看是 SQL 报错,实际往往是框架链路问题。排查时要区分清楚:
数据库语法问题
和
sql
项目框架 SQL 改写问题
否则很容易在 SQL 本身上反复修改,浪费大量时间。