文章目录
- 先说个最致命的——在WHERE里玩函数执行顺序
- 函数稳定态声明——VOLATILE不是万能标签
- SQL注入——2026年了还在踩的坑
- 隐式类型转换——索引是怎么悄悄消失的
- SELECT * 的隐性代价
- 统计信息不更新——优化器的近视眼
- 聊点深层的——这些坑背后的理论问题
- 通用开发编码规范——我的个人建议
说实话,干数据库这行十几年了,我最怕的不是系统宕机,不是数据损坏,而是那种"明明跑得好好的SQL突然就不对了"。这种情况十有八九不是因为数据库出了问题,是因为代码本身就有问题——只是一直没被发现而已。测试环境能过不代表生产环境能跑,今天能跑不代表明天不会炸。
我经手过不少国产化迁移项目,也帮好几个团队做过性能调优。每次坐下来review代码的时候,总能发现一堆让人血压飙升的写法。这些写法在测试环境里能过,在生产环境里也可能跑,但就是埋着不知道什么时候会炸的雷。今天这篇文章,我就结合参考资料里提到的那些坑,再加上金仓论坛上收集到的实战案例,把生产环境里常见的不规范SQL写法捋一遍,最后给出一些编码规范的建议。
先说个最致命的——在WHERE里玩函数执行顺序
这个坑我在之前的文章里也聊过,但每次看到有人还在这么写就来气。业务里有个Package,里面有一对函数:set_id设值,get_id取值。代码大概长这样:
-- 看着像是先设置再获取,但实际上执行顺序不可控SELECT*FROMorder_infoWHEREcust_id=pkg_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_id=pkg_util.get_id();-- 正常返回数据,变量被设置了-- 第二步:错误顺序执行SELECT*FROMorder_infoWHEREcust_id=pkg_util.get_id()ANDpkg_util.set_id(1001)=1;-- 也能返回数据!但不是因为SQL写对了-- 而是因为会话变量里还残留着第一步设的值-- 第三步:新开会话执行同样的错误SQL-- 直接返回空集,程序失效还有个深层问题:SQL是声明式语言。你在WHERE里写条件的先后顺序,从语义上来讲不应该影响结果。优化器的核心任务是找代价最低的路径,如果它评估认为右侧条件过滤率更高,理论上有权调整过滤顺序。当前版本可能按左右顺序执行,下一个大版本升级后呢?这就是个定时炸弹。
正确做法是逻辑解耦,先设值再查询:
-- 正确做法:把状态设置逻辑移到SQL外面BEGINpkg_util.set_id(1001);END;/SELECT*FROMorder_infoWHEREcust_id=pkg_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 NOTICE'Called.';RETURNid;END;$$LANGUAGEplpgsql STABLE;-- 6行数据,f_stable被调用6次(参数是列值不是常量)SELECT*FROMt3WHEREf_stable(id)=1;-- 但用于索引扫描条件时行为不同EXPLAINSELECT*FROMt5WHEREid<abs(10);-- abs默认VOLATILE:Seq Scan-- 改成STABLE后:Index Scan,且abs(-100)只在解析时计算一次-- 改成IMMUTABLE后:Index Scan,abs(-100)直接被替换成常量100所以编码规范上:纯计算函数声明IMMUTABLE,只读数据库的声明STABLE,有修改操作的才声明VOLATILE。别图省事全部用默认值。
SQL注入——2026年了还在踩的坑
这个问题说起来有点无语,都2026年了还有人写SQL拼接。KES Plus的后端开发规范里明确写了:使用参数化查询,防止SQL注入攻击。
-- 错误写法:SQL字符串拼接,存在注入风险DECLAREv_usernameTEXT:='admin';BEGINEXECUTE'DELETE FROM kesplus_user WHERE username = '''||v_username||''' ';END;-- 正确写法:使用动态SQL的USING占位符DECLAREv_usernameTEXT:='admin';BEGINEXECUTE'DELETE FROM kesplus_user WHERE username = $1'USINGv_username;END;KES Plus平台还内置了SQL注入防护机制,会限制参数中的SQL关键字(SELECT、UPDATE、EXECUTE、DELETE等)和特殊字符(–、`、//、/、/等)。若在业务中确实需要这些单词或符号,需要先将参数进行转义或者加密,然后在逻辑代码中进行还原再使用。比如传递图片的base64信息,因为信息中含有//符号不能通过校验,可以将//符号替换为$$,然后在具体业务处还原。
但这只是平台层面的防护,如果你直接连数据库写存储过程,这层防护是不存在的。靠数据库兜底不如从代码层面杜绝。另外KES Plus还强调了几个安全原则:敏感信息尽量不保存不传输,必要时加密存储;参数的大小和类型尽量做严格限制;身份验证与访问控制要遵循最小化原则,不能多个独立功能共用一个权限点。这些原则看着像废话,但实际项目中能严格执行的团队真不多。
隐式类型转换——索引是怎么悄悄消失的
这个坑金仓的SQL优化建议器特别指出来了:当索引列的类型被隐式转换之后,索引会失效,查询变成全表扫描。
-- 建表和索引CREATETABLEt3(idVARCHAR(32));CREATEINDEXidxONt3(id);INSERTINTOt3SELECTgenerate_series(1,1000000);-- 问题来了SELECT*FROMt3WHEREid=1000;-- id列是VARCHAR类型,1000是integer-- 数据库需要把id列的值隐式转成integer来比较-- 结果:idx索引完全没用,走全表扫描!100万行数据的表,因为一个类型不匹配,索引形同虚设。这种问题在迁移过程中特别容易出现——原来在别的数据库里可能隐式转换行为不一样,或者数据量小的时候全表扫描也很快,量一上来就露馅了。
-- 正确写法:显式指定类型SELECT*FROMt3WHEREid='1000';-- 或者SELECT*FROMt3WHEREid=CAST(1000ASVARCHAR(32));KES的SQL调优建议器能自动检测这类问题。它会对比执行计划前后的性能数据,如果性能提升幅度达到5%以上就给出索引建议和改写建议。使用方式也很简单:
-- 在kingbase.conf中配置shared_preload_libraries='plsql, 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_time>'2025-01-01'ANDuser_id=1001;-- 好的写法:只查需要的字段SELECTorder_id,amount,create_timeFROMorder_infoWHEREcreate_time>TO_DATE('2025-01-01','YYYY-MM-DD')ANDuser_id=1001;金仓论坛上有一篇性能优化实战文章,提到一个案例:原始慢SQL用SELECT *查询5.2秒,按建议只查业务需要的三个字段之后,配合联合索引,直接降到0.3秒。另外注意上面那个TO_DATE的改写——原始SQL用字符串做日期比较,这同样会导致隐式类型转换。把字符串条件改成日期类型,避免隐式转换,这也是调优建议器会指出的改写建议之一。
统计信息不更新——优化器的近视眼
这个问题不报错,SQL也能跑,但就是慢。如果表的统计信息没有及时更新,优化器会基于过时的数据生成非最优的执行计划。在数据分布严重倾斜的情况下尤其突出。
-- 查看表的统计信息状态SELECTrelname,last_analyze,n_live_tup,n_dead_tupFROMsys_stat_user_tablesWHERErelname='order_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?articleId=2394013b19f3ef84a43edb994692b88e),做数据库运维相关工具的同学可以关注下。上面提到的SQL调优建议器就是金仓在智能化运维方向的一个尝试,能把那些常见的不规范写法自动检测出来并给出改写建议,包括UNION转UNION ALL、DELETE转TRUNCATE、隐式类型转换导致的索引失效这三类规则,还有索引建议和统计信息收集建议。
最后再啰嗦一句。数据库这玩意儿吧,不像前端代码能肉眼看出大部分问题。SQL写得对不对和好不好之间的差距,往往要等数据量上来、并发上来、运行时间长了之后才能看出来。所以编码规范这东西不是写给领导看的,是写给你自己的——写给半年后回来看代码的自己。到那时候你还能不能一眼看明白当初为什么这么写,这个SQL会不会在某个极端情况下出问题。
说实话每次做项目我都会跟团队强调,写SQL的时候多想一步:这个条件优化器会怎么处理?这个函数的稳定态声明对不对?这个类型会不会触发隐式转换?这个UNION是不是其实可以用UNION ALL?多想这几步,能少加很多班。我见过太多因为一个不规范SQL导致生产事故的案例了,排查起来费时费力,最后改一行代码就解决了。预防永远比修复成本低。
还有一点想说的是,工具虽然能帮你发现问题,但工具不是万能的。金仓的SQL调优建议器能检测UNION转UNION ALL、DELETE转TRUNCATE、隐式类型转换这三类问题,还能给出索引建议和统计信息收集建议。但有些更深层的问题——比如WHERE里的函数副作用、会话变量污染、参数倾斜导致的执行计划偏移——这些工具不一定能发现。最终还是得靠人的经验和对业务的理解。所以编码规范的价值不在于能自动执行,而在于让团队形成共识,code review的时候知道该重点看什么,知道什么样写法是危险的、什么样的习惯必须改掉。