传统数据库迁移国产化,别把 WHERE 条件当成程序执行 数据库迁移现场有一种问题很磨人SQL 不报错数据也不是完全不对只是偶尔查不到。拿到开发工具里重跑结果又出来了。多跑几遍还是正常。于是大家开始怀疑连接池、网络或者怀疑金仓数据库执行计划不稳定。折腾一圈真正的问题可能只是WHERE里放了两个函数而且后一个函数要等前一个函数执行完才能正常工作。代码大致是这个样子SELECTid1,name1FROMmy_tableWHEREpkg_abc.set_id(10)1ANDid1pkg_abc.get_id();写这段代码的人想得很顺先用set_id(10)把值存起来再由get_id()取出来。两行条件从上往下排着似乎已经把步骤交代清楚了。可WHERE不是流程图。A AND B只说明 A、B 最终都要成立并没有规定数据库必须先算 A再算 B。优化器关心的是怎样找到符合条件的行它可能调整条件的处理方式也可能把表达式放到另一个计划节点。今天看执行计划set_id()确实在前面明天数据量变了计划跟着变这个顺序未必还在。有人会把条件上下交换SELECTid1,name1FROMmy_tableWHEREid1pkg_abc.get_id()ANDpkg_abc.set_id(10)1;这样更糟。新会话里get_id()读到的可能是初始值查询直接返回空集。偏偏它又不是每次都错。如果当前连接以前调用过set_id()包变量还留着旧值这条 SQL 可能正常返回。这种“有时候能跑”比直接报错麻烦得多。手工测试一般在同一个客户端窗口里连续进行前面的操作已经把会话状态准备好了。到了应用端连接从池里取出来谁也不知道它刚创建还是刚服务过另一个请求。测试时看不见的问题换个连接就冒出来了。所以我看到查询条件里有set、init、put这类函数第一反应不是研究它应该写左边还是右边而是先查函数有没有改状态。只要函数会修改包变量、临时数据或者业务表就不该依赖它在过滤过程中“恰好先执行”。该设置的状态在查询前明确设置-- 伪代码由应用或存储过程先完成上下文设置CALLpkg_abc.set_id(10);-- 查询阶段只读取不再偷偷改变状态SELECTid1,name1FROMmy_tableWHEREid1pkg_abc.get_id();如果10本来就是应用传进来的业务参数那就更简单直接绑定给查询SELECTid1,name1FROMmy_tableWHEREid1:current_id;少了一层看似聪明的封装问题反而清楚了。谁传的值、这次查询用了什么值都能顺着调用链查到不必猜连接里还残留着什么。“前面已经拦住了”这句话也靠不住同一种误解还会出现在数据转换上。比如导入表里有一个字符字段amount_text。有人为了跳过脏数据会这样写SELECTamount_textFROMpayment_importWHEREis_number(amount_text)1ANDto_number(amount_text)1000;他的解释通常是“前面已经判断过是不是数字了不是数字的行走不到to_number。”听上去很合理这是写过程代码时常用的短路思路。放进 SQL就不能只靠条件位置作保证。数据库并没有义务按照我们看到的顺序逐项求值。只要非法字符真的进入to_number()转换错误照样会发生。除零也有人这么防WHEREdenominator0ANDnumerator/denominator0.8我更愿意让除法本身能接住零而不是安排另一个条件站在前面WHEREnumerator/NULLIF(denominator,0)0.8分母为零时NULLIF返回NULL表达式不会满足大于条件。这里的安全性来自表达式本身不来自“希望数据库先判断哪一行”。字符转数字要麻烦一点。可以使用实际环境支持的安全转换方式也可以在数据入库时先完成校验把合法值转换后放入数值列把原始内容留在旁边供追溯。至少别让核心业务查询一边猜字符串是不是数字一边又立刻做数值运算。这不是语法洁癖。导入数据早晚会出现一条待确认、一个全角数字或者一串只在页面上看不出的空格。正常样本跑通只能说明样本太正常。条件顺序的错觉不只影响函数再看一条常见的订单查询SELECTo.order_id,p.pay_statusFROMorders oLEFTJOINpayments pONo.order_idp.order_idWHEREp.pay_statusSUCCESS;业务人员说“先左连接订单已经全保留了后面只是筛一下支付成功状态。”这句话的问题仍然在“先”和“后”。连接没匹配到支付记录时右表字段会被补成NULL到了WHERENULL不满足pay_status SUCCESS整行订单就被过滤了。写了LEFT JOIN最后却只剩匹配成功的记录。金仓数据库的优化器发现补空行必然会被后续条件排除时可能直接把外连接转换成内连接。看到计划变化不要急着下结论说优化器改错了。原 SQL 的结果本来就和内连接相同。若需求真是保留所有订单只关联支付成功的数据条件应该放在连接规则里SELECTo.order_id,p.pay_statusFROMorders oLEFTJOINpayments pONo.order_idp.order_idANDp.pay_statusSUCCESS;我一般用一个很土、但好用的问题判断条件放哪右边没有符合条件的数据左边这条还要不要要那就不能让右表条件在WHERE里把它删掉。不要原写法可能没有任何问题。并不是见到右表条件就往ON里挪还是得把业务要求问清楚。顺便说一句执行计划中出现Hash Join或Nested Loop不等于已经判断出内外连接。那是所用的连接算法。真正要看的是计划节点的连接类型以及条件落在哪个节点。拿算法名称直接判断语义很容易查偏。排查这类问题我会先换一个新会话遇到“工具里正常、应用里异常”的 SQL我会先关掉当前连接重新开一个干净会话。这个动作很简单却能排掉不少由包变量、临时状态和初始化脚本造成的假象。接着去看自定义函数。不是只看函数返回什么还要看它背后做了什么有没有写包变量有没有查依赖会话的对象有没有修改数据同一个输入连续调用两次是否一定返回同样结果。函数名字叫get_xxx也不代表它真的只读代码还是要打开看。测试数据我也不会只用正常值。字符列里放一条不能转换的内容分母放一个零连接右表故意缺一条记录再让连接池把同一个会话交给两个不同业务上下文。很多 SQL 平时“特别稳定”只是因为没人拿这些数据试过它。至于条件到底先执行哪个可以通过实际执行计划观察但观察结果只能解释这一次不能拿来当长期承诺。优化器今天选择这个计划并没有答应以后永远照这个顺序走。说到底查询条件应该像筛子不管数据库从哪一层开始筛最后都得到同一个正确结果。它不应该像一排机关必须先碰第一个、再碰第二个顺序错一下整个查询就失灵。传统数据库迁移到金仓数据库时真正值得修掉的不是某个条件的摆放位置而是这种对执行顺序的依赖。把状态设置移出查询把请求值改成明确参数让可能报错的表达式自己处理边界输入。代码可能没以前那么“巧”但接手的人看得懂换个会话还能跑执行计划变了也不怕。这就够了。