那些年我们写过的“炸弹SQL“——生产环境里的不规范写法到底埋了多少雷

📅 2026/7/26 5:08:48
那些年我们写过的“炸弹SQL“——生产环境里的不规范写法到底埋了多少雷
文章目录先说个最致命的——在WHERE里玩函数执行顺序函数稳定态声明——VOLATILE不是万能标签SQL注入——2026年了还在踩的坑隐式类型转换——索引是怎么悄悄消失的SELECT * 的隐性代价统计信息不更新——优化器的近视眼聊点深层的——这些坑背后的理论问题通用开发编码规范——我的个人建议兼容是对前人努力的尊重是确保业务平稳过渡的基石然而这仅仅是故事的起点说实话干数据库这行十几年了我最怕的不是系统宕机不是数据损坏而是那种明明跑得好好的SQL突然就不对了。这种情况十有八九不是因为数据库出了问题是因为代码本身就有问题——只是一直没被发现而已。测试环境能过不代表生产环境能跑今天能跑不代表明天不会炸。我经手过不少国产化迁移项目也帮好几个团队做过性能调优。每次坐下来review代码的时候总能发现一堆让人血压飙升的写法。这些写法在测试环境里能过在生产环境里也可能跑但就是埋着不知道什么时候会炸的雷。今天这篇文章我就结合参考资料里提到的那些坑再加上金仓论坛上收集到的实战案例把生产环境里常见的不规范SQL写法捋一遍最后给出一些编码规范的建议。先说个最致命的——在WHERE里玩函数执行顺序这个坑我在之前的文章里也聊过但每次看到有人还在这么写就来气。业务里有个Package里面有一对函数set_id设值get_id取值。代码大概长这样-- 看着像是先设置再获取但实际上执行顺序不可控SELECT*FROMorder_infoWHEREcust_idpkg_util.get_id()-- 先取值ANDpkg_util.set_id(1001)1;-- 后设值开发者以为WHERE里条件从左到右执行get_id会拿到set_id设进去的值。在Oracle里大部分情况确实是这个行为但等式和不等式混合时优化器可能打乱顺序。KES在兼容模式下默认也是从左到右但这不代表你能依赖这个行为。更要命的是会话污染。Package级别的全局变量在整个会话存续期间有效测试环境里你先跑了一条正确的SQL变量被赋了值。然后跑那条顺序反的SQL——居然也能返回数据因为会话里还残留着上次的值。等你上线到生产环境新开会话、新开连接池变量初始为空程序直接挂掉。这种bug排查起来真的是噩梦因为你在测试环境复现不了。-- 测试假象的产生过程-- 第一步正确顺序执行EXECpkg_util.set_id(1001);SELECT*FROMorder_infoWHEREcust_idpkg_util.get_id();-- 正常返回数据变量被设置了-- 第二步错误顺序执行SELECT*FROMorder_infoWHEREcust_idpkg_util.get_id()ANDpkg_util.set_id(1001)1;-- 也能返回数据但不是因为SQL写对了-- 而是因为会话变量里还残留着第一步设的值-- 第三步新开会话执行同样的错误SQL-- 直接返回空集程序失效还有个深层问题SQL是声明式语言。你在WHERE里写条件的先后顺序从语义上来讲不应该影响结果。优化器的核心任务是找代价最低的路径如果它评估认为右侧条件过滤率更高理论上有权调整过滤顺序。当前版本可能按左右顺序执行下一个大版本升级后呢这就是个定时炸弹。正确做法是逻辑解耦先设值再查询-- 正确做法把状态设置逻辑移到SQL外面BEGINpkg_util.set_id(1001);END;/SELECT*FROMorder_infoWHEREcust_idpkg_util.get_id();DBA在日常审计的时候也应该重点关注执行计划中的Filter顺序。通过explain analyze查看实际执行中各条件的过滤顺序和耗时如果发现SQL行为跟会话状态相关——同样的SQL换个会话结果就不一样——那十有八九是Package变量或临时表在作怪得立刻排查。我之前遇到过一个案例开发在测试环境跑了两个月都没问题上线第一天就出事。排查了整整一天才发现是连接池回收连接后会话变量被重置而业务逻辑依赖这个变量的值。这种问题你光看SQL代码是看不出来的得理解整个执行链路才行。函数稳定态声明——VOLATILE不是万能标签说到WHERE里的函数就不得不提函数三态。KES沿用了VOLATILE、STABLE、IMMUTABLE三档分类这个声明直接影响优化器的执行计划生成。但很多开发者根本不知道这个分类的存在创建函数的时候默认就是VOLATILE然后莫名其妙地查询就慢了。这个问题在金仓社区有详细的技术分析VOLATILE函数导致无法走索引扫描。三档分类的具体区别VOLATILE函数在同一个查询中同样参数可能被多次执行不能用于创建函数索引不能走索引扫描。STABLE函数优化器可以根据场景减少调用次数能用于索引扫描但不能创建函数索引。IMMUTABLE函数优化器在处理时先评估结果直接用常量替换只有IMMUTABLE能创建函数索引。-- 函数三态对执行计划的影响实测-- 创建STABLE函数CREATEORREPLACEFUNCTIONf_stable(idint)RETURNSintAS$$BEGINRAISE NOTICECalled.;RETURNid;END;$$LANGUAGEplpgsql STABLE;-- 6行数据f_stable被调用6次参数是列值不是常量SELECT*FROMt3WHEREf_stable(id)1;-- 但用于索引扫描条件时行为不同EXPLAINSELECT*FROMt5WHEREidabs(10);-- abs默认VOLATILESeq Scan-- 改成STABLE后Index Scan且abs(-100)只在解析时计算一次-- 改成IMMUTABLE后Index Scanabs(-100)直接被替换成常量100所以编码规范上纯计算函数声明IMMUTABLE只读数据库的声明STABLE有修改操作的才声明VOLATILE。别图省事全部用默认值。SQL注入——2026年了还在踩的坑这个问题说起来有点无语都2026年了还有人写SQL拼接。KES Plus的后端开发规范里明确写了使用参数化查询防止SQL注入攻击。-- 错误写法SQL字符串拼接存在注入风险DECLAREv_usernameTEXT:admin;BEGINEXECUTEDELETE FROM kesplus_user WHERE username ||v_username|| ;END;-- 正确写法使用动态SQL的USING占位符DECLAREv_usernameTEXT:admin;BEGINEXECUTEDELETE FROM kesplus_user WHERE username $1USINGv_username;END;KES Plus平台还内置了SQL注入防护机制会限制参数中的SQL关键字SELECT、UPDATE、EXECUTE、DELETE等和特殊字符–、、//、/、/等。若在业务中确实需要这些单词或符号需要先将参数进行转义或者加密然后在逻辑代码中进行还原再使用。比如传递图片的base64信息因为信息中含有//符号不能通过校验可以将//符号替换为$$然后在具体业务处还原。但这只是平台层面的防护如果你直接连数据库写存储过程这层防护是不存在的。靠数据库兜底不如从代码层面杜绝。另外KES Plus还强调了几个安全原则敏感信息尽量不保存不传输必要时加密存储参数的大小和类型尽量做严格限制身份验证与访问控制要遵循最小化原则不能多个独立功能共用一个权限点。这些原则看着像废话但实际项目中能严格执行的团队真不多。隐式类型转换——索引是怎么悄悄消失的这个坑金仓的SQL优化建议器特别指出来了当索引列的类型被隐式转换之后索引会失效查询变成全表扫描。-- 建表和索引CREATETABLEt3(idVARCHAR(32));CREATEINDEXidxONt3(id);INSERTINTOt3SELECTgenerate_series(1,1000000);-- 问题来了SELECT*FROMt3WHEREid1000;-- id列是VARCHAR类型1000是integer-- 数据库需要把id列的值隐式转成integer来比较-- 结果idx索引完全没用走全表扫描100万行数据的表因为一个类型不匹配索引形同虚设。这种问题在迁移过程中特别容易出现——原来在别的数据库里可能隐式转换行为不一样或者数据量小的时候全表扫描也很快量一上来就露馅了。-- 正确写法显式指定类型SELECT*FROMt3WHEREid1000;-- 或者SELECT*FROMt3WHEREidCAST(1000ASVARCHAR(32));KES的SQL调优建议器能自动检测这类问题。它会对比执行计划前后的性能数据如果性能提升幅度达到5%以上就给出索引建议和改写建议。使用方式也很简单-- 在kingbase.conf中配置shared_preload_librariesplsql, sys_stat_statements, sys_sqltune-- 创建插件CREATEEXTENSION sys_sqltune;-- 直接对SQL生成调优建议SELECTPERF.QUICK_TUNE_BY_SQL(SELECT * FROM t3 WHERE id 1000);SELECT * 的隐性代价这个问题在高并发场景下影响很大。SELECT *会导致数据库读取所有列的数据包括那些你根本不需要的大字段。-- 不好的写法SELECT*FROMorder_infoWHEREcreate_time2025-01-01ANDuser_id1001;-- 好的写法只查需要的字段SELECTorder_id,amount,create_timeFROMorder_infoWHEREcreate_timeTO_DATE(2025-01-01,YYYY-MM-DD)ANDuser_id1001;金仓论坛上有一篇性能优化实战文章提到一个案例原始慢SQL用SELECT *查询5.2秒按建议只查业务需要的三个字段之后配合联合索引直接降到0.3秒。另外注意上面那个TO_DATE的改写——原始SQL用字符串做日期比较这同样会导致隐式类型转换。把字符串条件改成日期类型避免隐式转换这也是调优建议器会指出的改写建议之一。统计信息不更新——优化器的近视眼这个问题不报错SQL也能跑但就是慢。如果表的统计信息没有及时更新优化器会基于过时的数据生成非最优的执行计划。在数据分布严重倾斜的情况下尤其突出。-- 查看表的统计信息状态SELECTrelname,last_analyze,n_live_tup,n_dead_tupFROMsys_stat_user_tablesWHERErelnameorder_info;-- 手动收集统计信息ANALYZEorder_info;金仓的SQL调优建议器在检测到表中修改的数据量超过autovacuum的计算结果时会自动生成收集统计信息的建议。它还能检测多列统计信息缺失的情况——当你的WHERE条件同时过滤多个有关联的列时单列统计信息可能不够准确需要创建多列统计信息。聊点深层的——这些坑背后的理论问题说到这里我想聊点更深层的东西。上面这些坑表面上看是编码习惯问题但往深了想其实反映了一个根本性的矛盾SQL是声明式语言但开发者习惯了过程式思维。你在WHERE里写条件的先后顺序本质上是一种过程式思维——先执行这个再执行那个。但SQL标准要求数据库把这些条件当作一个整体来评估执行顺序是由优化器决定的。Oracle恰好默认按从左到右的顺序执行KES也做了兼容但这不代表这是标准行为。把业务逻辑建立在实现细节之上就是所有问题的根源。隐式类型转换也是同样的道理。还有一个概念界定的问题。我们说SQL规范到底指的是什么SQL标准ISO/IEC 9075是一回事数据库厂商的实现是另一回事开发者社区的惯例又是第三回事。当这三者冲突的时候以谁为准在迁移场景下这个问题尤其突出——你的代码是符合Oracle惯例的但Oracle惯例本身就不完全符合SQL标准。比如Oracle把空字符串和NULL等价处理这个行为就明显偏离了SQL标准。迁移到新库的时候是要求新库兼容你的非标准用法还是要求你改写代码符合标准这个问题没有标准答案但从长远来看朝着标准靠拢总是更安全的选择。另外我还注意到一个有意思的现象不同数据库厂商在兼容性策略上存在路线分歧。一种路线是模式兼容通过参数开关来模拟源库行为。KES走的就是这条路ora_input_emptystr_isnull、ora_func_style这些参数本质上就是在做行为模拟。好处是灵活用户可以按需开启坏处是参数太多容易遗漏而且不同参数之间可能有交互效应。另一种路线是工具转换通过迁移工具把源库语法自动转换成目标库的原生语法。两种路线各有利弊但目前很少有系统能把两者完美结合起来。这也是为什么迁移项目里总是有踩不完的坑——工具能帮你转语法但转不了语义。通用开发编码规范——我的个人建议上面说了这么多坑最后整理一份不太正式的编码规范。不是什么权威标准是我这些年踩坑踩出来的经验。规范一WHERE子句里禁止放有副作用的函数。这条是铁律。任何会修改状态、读写会话变量的函数都不能出现在WHERE里。先在外面调用好再拿结果做过滤。规范二参数化查询严禁SQL拼接。动态SQL必须用USING占位符。直接拼接字符串的写法存在SQL注入风险。规范三类型要显式匹配。索引列是什么类型过滤条件就用什么类型。字符串列用字符串常量数值列用数值常量。不要依赖隐式转换。规范四UNION能不用就不用。确认需要去重用UNION确认不需要去重用UNION ALL。大部分场景下UNION ALL就够了。规范五删全表用TRUNCATE不用DELETE。除非你需要事务回滚的能力或者MVCC的旧版本可见性。*规范六禁止SELECT。只查业务需要的字段。这不仅影响性能还影响索引覆盖扫描的可能性。规范七参数和变量命名加前缀。存储过程参数用p_前缀变量用v_前缀常量用c_前缀。避免跟表字段名冲突。规范八函数要正确声明稳定态。纯计算函数声明IMMUTABLE只读数据库的声明STABLE有修改操作的才声明VOLATILE。这个声明直接影响优化器的执行计划生成。规范九定期收集统计信息。大表的数据变化超过一定量级后主动跑一次ANALYZE。不要完全依赖autovacuum特别是在数据分布严重倾斜的情况下。规范十迁移前做全量兼容性扫描。不要相信工具自动转换的结果每个存储过程、每个触发器、每个自定义函数都要手动验证。特别是隐式类型转换的行为差异、函数执行顺序的兼容性、时间函数的语义差异。规范十一OR条件涉及不同字段时手动改写UNION ALL。不要指望优化器帮你改自己写清楚更可控。规范十二日期条件用日期类型。不要用字符串做日期比较统一用TO_DATE或DATE类型字面量避免隐式转换导致索引失效。规范十三改表结构前确认是否触发表重写。修改字段类型时检查新旧类型是否二进制兼容。大表的结构变更要选在维护窗口或者用在线变更方案。规范十四大对象操作要控制读取粒度。含LOB字段的查询要控制游标读取记录数和LOB预读取大小避免一次性把大量大对象加载到内存。说了这么多顺便提一句最近金仓社区搞了个智能运维工具开发大赛https://bbs.kingbase.com.cn/forumDetail?articleId2394013b19f3ef84a43edb994692b88e做数据库运维相关工具的同学可以关注下。上面提到的SQL调优建议器就是金仓在智能化运维方向的一个尝试能把那些常见的不规范写法自动检测出来并给出改写建议包括UNION转UNION ALL、DELETE转TRUNCATE、隐式类型转换导致的索引失效这三类规则还有索引建议和统计信息收集建议。最后再啰嗦一句。数据库这玩意儿吧不像前端代码能肉眼看出大部分问题。SQL写得对不对和好不好之间的差距往往要等数据量上来、并发上来、运行时间长了之后才能看出来。所以编码规范这东西不是写给领导看的是写给你自己的——写给半年后回来看代码的自己。到那时候你还能不能一眼看明白当初为什么这么写这个SQL会不会在某个极端情况下出问题。说实话每次做项目我都会跟团队强调写SQL的时候多想一步这个条件优化器会怎么处理这个函数的稳定态声明对不对这个类型会不会触发隐式转换这个UNION是不是其实可以用UNION ALL多想这几步能少加很多班。我见过太多因为一个不规范SQL导致生产事故的案例了排查起来费时费力最后改一行代码就解决了。预防永远比修复成本低。还有一点想说的是工具虽然能帮你发现问题但工具不是万能的。金仓的SQL调优建议器能检测UNION转UNION ALL、DELETE转TRUNCATE、隐式类型转换这三类问题还能给出索引建议和统计信息收集建议。但有些更深层的问题——比如WHERE里的函数副作用、会话变量污染、参数倾斜导致的执行计划偏移——这些工具不一定能发现。最终还是得靠人的经验和对业务的理解。所以编码规范的价值不在于能自动执行而在于让团队形成共识code review的时候知道该重点看什么知道什么样写法是危险的、什么样的习惯必须改掉。