上线前能跑上线后挂了三个隐性的SQL陷阱上线的第二天功能突然不工作了前年做了一个国产化迁移项目从Oracle换到金仓。系统上线之后头两天风平浪静。第三天开始有个模块偶尔报错再过一天直接崩溃了。最开始只是偶尔返回空结果后来直接不干活了。业务方很着急“你们换数据库是不是把逻辑搞乱了”我查了半天代码最后发现是WHERE子句里函数调用顺序的问题。Oracle里的写法是这样的SELECT*FROMmy_tableWHEREid1pkg_abc.get_id()ANDpkg_abc.set_id(10)1;这个逻辑很明显先给变量赋个值再拿这个值去查数据。在Oracle里条件是从左往右执行的。get_id()先跑变量没值查不到东西。但因为set_id(10)1会在get_id()之前被先执行Oracle里赋值函数被当作常量条件优先处理了所以实际能跑通。到了金仓上执行顺序变了。金仓在兼容性设计上虽然也按从左到右处理但这种“赋值函数”和“取值函数”混在一起的情况执行结果极其不稳定。刚开始还能返回几条后来干脆全返回空。问题的根源在于优化器会根据代价判断执行顺序。有时候它觉得get_id()更便宜就先跑get_id()变量还没赋上结果自然是空。有时候它觉得set_id()更便宜先跑了数据又能出来。结果就是同一个SQL有时候行有时候不行。更坑的是如果当前会话里之前执行过正确的SQL变量已经被赋过值了后面再跑错误的SQL也能拿到数据。这会造成“测试通过了”的假象上线之后才发现问题。后来我重写了一下把这两个函数调用分开了-- 第一步设置值SELECTpkg_abc.set_id(10)FROMDUAL;-- 第二步查询数据SELECT*FROMmy_tableWHEREid1pkg_abc.get_id();两步走逻辑清晰不管在哪个数据库上跑都一样。这件事之后我学乖了WHERE子句里千万别放有副作用的函数。什么叫副作用就是会改数据、改状态、改变量值的函数。这类函数应该单独执行不要跟查询混在一起。迁移后少了一条数据另一个项目从MySQL迁到金仓。迁移完第二天业务方反馈某个报表少了一条数据。查了半天发现是LEFT JOIN的问题SELECT*FROMorders oLEFTJOINrefunds rONo.idr.order_idWHEREr.status1;这条SQL的意思是查所有订单有退款的带上退款信息没退款的就算了。MySQL里跑出来1280条金仓里只有1123条少了157个没退款的订单。原因我后来才搞清楚金仓的优化器会把LEFT JOIN转成INNER JOIN。WHERE条件里有r.status 1右表没匹配的行r.status是NULLNULL不等于1这些行会被干掉。优化器一算既然这些行最后都要被过滤掉我直接转成INNER JOIN算了。逻辑上没错但业务语义变了。正确的写法应该是把条件挪到ON里SELECT*FROMorders oLEFTJOINrefunds rONo.idr.order_idANDr.status1;这样优化器就没理由消除了因为连接的时候右表已经过滤过了左表的所有行都能保住。后来我养成了个习惯凡是外连接右表的过滤条件一律放ON里。除非你确实想把没匹配的行过滤掉那直接用INNER JOIN就行了。类型不匹配带来的性能爆炸还有一次迁移完发现某个查询慢得离谱。原系统里毫秒级响应到了金仓上变成几十秒。查了一圈发现是数据类型的问题。原SQL里有个条件SELECT*FROMordersWHEREorder_no123456;order_no在表里是VARCHAR类型传参传的是字符串没问题。但另一个地方是这么写的SELECT*FROMordersWHEREorder_no123456;传的是数字。MySQL里能做隐式类型转换VARCHAR跟数字比的时候会把字符串转成数字再比较。索引还能用上。金仓的处理方式不一样。数字和VARCHAR比较的时候会把VARCHAR转成数字。但问题是这个转换是逐行执行的索引用不上了只能全表扫描。100万条数据全部扫一遍几十秒就出去了。查了一下执行计划确实是全表扫描。改起来很简单统一用字符串就行SELECT*FROMordersWHEREorder_no123456;但问题在于代码里这种地方太多了。有些是前端传过来的参数类型不一致有些是ORM自动生成的SQL有些是不同模块的开发写了不同的写法。最后我把所有传参的地方都统一了确保传的是字符串。然后加了一条规则代码里传参必须跟字段类型一致不能依赖隐式转换。总结一下这三个陷阱有个共同点在原来的数据库上能跑到了新数据库上就不行了。第一个是函数调用顺序的问题。Oracle里能依赖执行顺序的写法到了金仓上就不稳定了。解决办法是把有副作用的函数移出WHERE子句。第二个是优化器行为差异的问题。MySQL不做的优化金仓做了。外连接的右表条件放到ON里就是安全的。第三个是隐式类型转换的问题。MySQL里能用的隐式转换金仓里不一定能用索引可能失效。保持类型一致不要图省事。后来我总结出几条迁移排查的经验不一定全面但至少能帮你少走些弯路先看执行计划。慢查询、结果对不上第一件事就是拉执行计划。一看计划就知道优化器干了什么。再看函数调用。WHERE子句里有自定义函数吗函数里有状态变更吗有的话先改掉。最后看类型匹配。字段是什么类型传参就用什么类型。不要依赖隐式转换。那次迁移之后我学到一个东西不同数据库之间的差异不只是在SQL语法上更在底层的行为逻辑上。写过五年Oracle的人真不一定能写好金仓的SQL。好在这些坑踩过一次之后下次就知道怎么绕过去了。
上线前能跑,上线后挂了,三个隐性的SQL陷阱
上线前能跑上线后挂了三个隐性的SQL陷阱上线的第二天功能突然不工作了前年做了一个国产化迁移项目从Oracle换到金仓。系统上线之后头两天风平浪静。第三天开始有个模块偶尔报错再过一天直接崩溃了。最开始只是偶尔返回空结果后来直接不干活了。业务方很着急“你们换数据库是不是把逻辑搞乱了”我查了半天代码最后发现是WHERE子句里函数调用顺序的问题。Oracle里的写法是这样的SELECT*FROMmy_tableWHEREid1pkg_abc.get_id()ANDpkg_abc.set_id(10)1;这个逻辑很明显先给变量赋个值再拿这个值去查数据。在Oracle里条件是从左往右执行的。get_id()先跑变量没值查不到东西。但因为set_id(10)1会在get_id()之前被先执行Oracle里赋值函数被当作常量条件优先处理了所以实际能跑通。到了金仓上执行顺序变了。金仓在兼容性设计上虽然也按从左到右处理但这种“赋值函数”和“取值函数”混在一起的情况执行结果极其不稳定。刚开始还能返回几条后来干脆全返回空。问题的根源在于优化器会根据代价判断执行顺序。有时候它觉得get_id()更便宜就先跑get_id()变量还没赋上结果自然是空。有时候它觉得set_id()更便宜先跑了数据又能出来。结果就是同一个SQL有时候行有时候不行。更坑的是如果当前会话里之前执行过正确的SQL变量已经被赋过值了后面再跑错误的SQL也能拿到数据。这会造成“测试通过了”的假象上线之后才发现问题。后来我重写了一下把这两个函数调用分开了-- 第一步设置值SELECTpkg_abc.set_id(10)FROMDUAL;-- 第二步查询数据SELECT*FROMmy_tableWHEREid1pkg_abc.get_id();两步走逻辑清晰不管在哪个数据库上跑都一样。这件事之后我学乖了WHERE子句里千万别放有副作用的函数。什么叫副作用就是会改数据、改状态、改变量值的函数。这类函数应该单独执行不要跟查询混在一起。迁移后少了一条数据另一个项目从MySQL迁到金仓。迁移完第二天业务方反馈某个报表少了一条数据。查了半天发现是LEFT JOIN的问题SELECT*FROMorders oLEFTJOINrefunds rONo.idr.order_idWHEREr.status1;这条SQL的意思是查所有订单有退款的带上退款信息没退款的就算了。MySQL里跑出来1280条金仓里只有1123条少了157个没退款的订单。原因我后来才搞清楚金仓的优化器会把LEFT JOIN转成INNER JOIN。WHERE条件里有r.status 1右表没匹配的行r.status是NULLNULL不等于1这些行会被干掉。优化器一算既然这些行最后都要被过滤掉我直接转成INNER JOIN算了。逻辑上没错但业务语义变了。正确的写法应该是把条件挪到ON里SELECT*FROMorders oLEFTJOINrefunds rONo.idr.order_idANDr.status1;这样优化器就没理由消除了因为连接的时候右表已经过滤过了左表的所有行都能保住。后来我养成了个习惯凡是外连接右表的过滤条件一律放ON里。除非你确实想把没匹配的行过滤掉那直接用INNER JOIN就行了。类型不匹配带来的性能爆炸还有一次迁移完发现某个查询慢得离谱。原系统里毫秒级响应到了金仓上变成几十秒。查了一圈发现是数据类型的问题。原SQL里有个条件SELECT*FROMordersWHEREorder_no123456;order_no在表里是VARCHAR类型传参传的是字符串没问题。但另一个地方是这么写的SELECT*FROMordersWHEREorder_no123456;传的是数字。MySQL里能做隐式类型转换VARCHAR跟数字比的时候会把字符串转成数字再比较。索引还能用上。金仓的处理方式不一样。数字和VARCHAR比较的时候会把VARCHAR转成数字。但问题是这个转换是逐行执行的索引用不上了只能全表扫描。100万条数据全部扫一遍几十秒就出去了。查了一下执行计划确实是全表扫描。改起来很简单统一用字符串就行SELECT*FROMordersWHEREorder_no123456;但问题在于代码里这种地方太多了。有些是前端传过来的参数类型不一致有些是ORM自动生成的SQL有些是不同模块的开发写了不同的写法。最后我把所有传参的地方都统一了确保传的是字符串。然后加了一条规则代码里传参必须跟字段类型一致不能依赖隐式转换。总结一下这三个陷阱有个共同点在原来的数据库上能跑到了新数据库上就不行了。第一个是函数调用顺序的问题。Oracle里能依赖执行顺序的写法到了金仓上就不稳定了。解决办法是把有副作用的函数移出WHERE子句。第二个是优化器行为差异的问题。MySQL不做的优化金仓做了。外连接的右表条件放到ON里就是安全的。第三个是隐式类型转换的问题。MySQL里能用的隐式转换金仓里不一定能用索引可能失效。保持类型一致不要图省事。后来我总结出几条迁移排查的经验不一定全面但至少能帮你少走些弯路先看执行计划。慢查询、结果对不上第一件事就是拉执行计划。一看计划就知道优化器干了什么。再看函数调用。WHERE子句里有自定义函数吗函数里有状态变更吗有的话先改掉。最后看类型匹配。字段是什么类型传参就用什么类型。不要依赖隐式转换。那次迁移之后我学到一个东西不同数据库之间的差异不只是在SQL语法上更在底层的行为逻辑上。写过五年Oracle的人真不一定能写好金仓的SQL。好在这些坑踩过一次之后下次就知道怎么绕过去了。