多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

063、使用Open SQL注意事项

063、使用Open SQL注意事项 那是一个周五的傍晚生产系统告警像潮水一样涌进手机。某个报表程序在月底结算时直接卡死数据库CPU飙到99%。远程连上去一看问题出在一句看似人畜无害的Open SQL上。SELECT * FROM MARA WHERE MATNR IN (SELECT MATNR FROM ZTMP_MAT). 子查询里那个临时表有80万行数据库优化器直接选择放弃治疗把整张表扫描了一遍又一遍。那次事故之后我养成了一个习惯凡是看到SELECT *凡是看到子查询都要停下来手放在键盘上默念三秒——这可能是系统崩溃的前奏。Open SQL是ABAP开发者最亲密的老朋友但这位朋友脾气古怪写错一个字符可能性能差出百倍。先从最基础的SELECT说起。很多人刚学ABAP时喜欢这样写SELECT * FROM sflight INTO wa_sflight. WRITE: wa_sflight-carrid, wa_sflight-connid. ENDSELECT.别这样写。虽然它能跑通但“SELECT *”会取出所有字段SFLIGHT表还好如果换成MSEG这种大表内存直接爆炸。更关键的是这种循环逐条读的方式每取一条数据都要和数据库交互一次。想象一下你去超市买东西每拿一瓶酱油都跑到收银台结一次账后面排队的人不揍你才怪。应该养成指定字段的习惯SELECT carrid connid fldate FROM sflight INTO CORRESPONDING FIELD OF wa_sflight.这里用INTO CORRESPONDING FIELD OF可以自动按名称匹配结构字段但注意如果表结构有更改而工作区没有同步运行时会有诡异问题。最稳的做法是定义精确的结构或者直接内表TYPES: BEGIN OF ty_sflight, carrid TYPE sflight-carrid, connid TYPE sflight-connid, fldate TYPE sflight-fldate, END OF ty_sflight. DATA: lt_sflight TYPE TABLE OF ty_sflight. SELECT carrid connid fldate FROM sflight INTO TABLE lt_sflight.一次全量取出干净利落。这就是Open SQL的第一条铁律能用内表一次抓就别用ENDSELECT循环。接着是那个让人又爱又恨的FOR ALL ENTRIES IN。这玩意能极大减少数据库往返但用不好就是炸弹。最常见的地雷如果内部表是空的FOR ALL ENTRIES会无视所有条件把整张表数据全捞出来。你说气不气。所以每次用之前必须检查IF lt_conn IS NOT INITIAL. SELECT * FROM sflight INTO TABLE lt_sflight FOR ALL ENTRIES IN lt_conn WHERE carrid lt_conn-carrid. ENDIF.这里踩过坑。别觉得这检查多余生产环境上因为空表导致全表扫描的事我见多了。FOR ALL ENTRIES还有一个雷它会自动去除重复的连接字段值并且把数据库缓冲区的特性搞乱。如果你对这个机制不熟就记住一句内表很大的时候FOR ALL ENTRIES并不是最好的方案尤其当内表超过几千行时SQL语句会变得非常长数据库解析那一下就能卡半天。这种情况下分块或改用JOIN可能更合理。说到JOINOpen SQL支持INNER JOIN和LEFT OUTER JOIN。但注意ABAP里JOIN的ON条件只能用等值连接而且多表连接时不能太贪。有些人喜欢把八个表拼在一起每个表都带一堆条件看起来高大上实际上数据库优化器见到这种SQL直接懵圈。我的经验是超过三个表用JOIN就要谨慎不如拆开分两步查内存换性能反而更快。而且ABAP的JOIN有个历史遗留限制FOR ALL ENTRIES不能和JOIN的语法成分直接混用除非你用子查询。子查询这玩意刚才讲过了能不用就不用除非你的数据库是HANA并且能保证子查询结果集极小。另外一个经典坑是ORDER BY和GROUP BY。Open SQL的ORDER BY只能用在按数据库排序的场景而且如果你用了FOR ALL ENTRIES排序结果基本是不固定的因为数据库内部做了hash join。有次我为了图方便在SELECT后用了一个ORDER BY结果内表数据顺序在测试环境正常到了生产环境完全乱掉。原因就是FOR ALL ENTRIES的并行处理导致顺序不稳定。如果要保证顺序老老实实排序到自己的内表SORT lt_sflight BY carrid connid.或者好一点的SELECT时带上ORDER BY但别对FOR ALL ENTRIES的结果抱太大期望。GROUP BY的话注意数据库表字段的聚合方式比如MAX, MIN, SUM这些都可以用但要注意聚合时如果有NULL值SUM会忽略NULL而不是当0处理。还有那个UP TO n ROWS。这个在取前几条记录时很好用但如果你放在FOR ALL ENTRIES里面它作用于整体查询不是每个条目取n条。比如你想对每个航空公司取最新一条航班SELECT carrid connid fldate FROM sflight UP TO 1 ROWS ORDER BY carrid fldate DESCENDING INTO TABLE lt_sflight.这是取全表按fldate倒序的第一条不是每个carrid一条。如果你真想要每组一条推荐使用窗口函数但ABAP Open SQL的窗口函数支持要看版本HANA上可以写其他数据库就别想了。老版本的替代方案是循环内表SELECT SINGLE性能差些但逻辑清晰。SELECT SINGLE是另一个双刃剑。它只取一条记录条件必须写全尤其主键条件。但很多人用它查多条时只写部分条件查出来哪条完全看数据库心情。更糟糕的是SELECT SINGLE * FROM huge_table WHERE matnr lv_matnr. 这种其实不如SELECT … UP TO 1 ROWS因为SINGLE在某些数据库上会走特殊路径导致索引使用不理想。我的建议是明确知道只返回一条时才用SELECT SINGLE否则就用UP TO 1 ROWS ORDER BY。接下来是ABAP里独有的“SQL条件规则”比如在WHERE中能不能用ABAP变量当然能。但记得变量名要和字段名区分开。还有字符串比较Open SQL默认是大小写敏感的除非你的数据库是非大小写敏感的排序规则。还有LIKE通配符百分号和下划线如果你要查的字符串本身含下划线忘记了转义那结果会很酸爽。比如查物料号带下划线的直接写WHERE MATNR LIKE ‘ABC_D%’这个下划线会匹配任意一个字符根本不是你想要的字面下划线。正确姿势是WHERE matnr LIKE ABC\_D% ESCAPE \.这个知识点很冷门但命中一次就是事故。另外Open SQL里字符串比较时ABAP会默认忽略尾随空格trailing blanks这个跟数据库行为略有差异调试时容易一头雾水。比如你在数据库客户端执行WHERE NAME 张三 可能和ABAP里执行的WHERE NAME 张三’结果不同因为数据库排序规则强制了binary比较会带空格。解决方法是统一去掉空格再传参或者在条件中使用显式的空格处理函数但HANA上函数名又不一样。稳妥起见在程序里提前CONDENSE或者SHIFT掉尾随空格别指望SQL帮你处理。再说一个很隐蔽的坑CLIENT客户端处理。Open SQL默认自动带上MANDT字段这是SAP的客户端逻辑。可如果你在写了FOR ALL ENTRIES后内表里包含MANDT字段那么这个条件会参与WHERE导致你去查别的客户端的数据如果权限允许的话。反过来如果内表没有MANDT系统会自动加上当前的SY-MANDT。这个行为在不同版本上有微妙的差异。我的建议是永远不要在内表里放MANDT字段并且所有查询都默认只能看到自己客户端。如果你确实需要跨客户端用authorization-check或直接用数据库连接但别在Open SQL里搞特殊。关于事务和数据的一致性Open SQL本身不隐式提交。除非调用了COMMIT WORK否则UPDATE、INSERT、DELETE只在当前数据库会话中可见。但注意IMPORT/EXPORT到数据库表或者用RFC调用远程系统会强制提交。这个坑在批量程序中特别容易踩你循环一堆行每行更新但忘记在每1000行时COMMIT结果运行到一半SESSION锁爆了或者日志文件膨胀。正确姿势LOOP AT lt_data INTO ls_data. UPDATE ztable SET field ls_data-field WHERE key ls_data-key. lv_counter lv_counter 1. IF lv_counter MOD 1000 0. COMMIT WORK. ENDIF. ENDLOOP. COMMIT WORK.别嫌啰嗦这是救命的。还有UPDATE … FROM内表用法可以一次性更新多行但它的行为是“每行独立update”如果中间某行有错误整个语句会如何处理ABAP文档说大字段LCHR/LRAW不能这样用而且更新时不会触发数据库的update hint。我倾向于直接LOOP UPDATE可控性强。删除操作DELETE FROM table WHERE条件如果条件不小心没写你会把整表清空。别笑真有人干过。所以DELETE之前要确认条件值非空并且最好先SELECT COUNT计数验证。类似地INSERT时要小心重复键。如果你直接INSERT而主键已存在会抛异常。可以先用MODIFY它会先尝试INSERT如果已存在就UPDATE但注意MODIFY不能用于类型组或带某些数据库特性的表。如果要求严格用INSERT … ACCEPTING DUPLICATE KEYS这是标准写法它会忽略重复键并把重复次数统计在内置变量SY-DBCNT里。不过SY-DBCNT的语义在不同ABAP版本有不同压测环境注意验证。说说DB缓存。Open SQL默认读取数据库缓冲池尤其对于SAP标准表有Table Buffer机制。如果你使用WHERE条件中包含非主键字段且字段不是完全匹配Buffer就会失效。这不一定全是坏事但可能导致性能抖动。比如你用“WHERE BUKRS ‘1000’”如果BUKRS是部分缓冲键而缓冲方式为“single record”或“generic area”那么你可能命中缓冲或Miss甚至产生全表缓冲扫描。这个问题经常出现在系统刚启动或数据刚更新时SQL第一次执行奇慢后续变快。若怀疑Buffer可以用“REPLACE INF”或者直接“SET PARAMETER ID ‘DBBUFFER’”不ABAP里控制缓冲的是数据库的表属性不容易在代码里屏蔽。经验是对性能敏感且频繁运行的查询尽量用主键访问如果必须按非主键查考虑自己建索引或使用SAP的专有索引。Open SQL还有一堆“方言”问题尤其涉及HANA与旧数据库的兼容。比如SELECT … AS别名HANA支持别名但某些老数据库可能忽略别名导致后续字段引用失败。比如CONCAT函数ABAP里的字符串连接用“”但Open SQL里要用数据库的CONCAT或||。不过ABAP的“”在Open SQL中也适用不对Open SQL中字符串连接是使用“”作为转义字符让我理清楚。在ABAP中字面量用单引号如果要在字符串中包含单引号用“”转义。在Open SQL里字符串连接可以用“”吗并不。Open SQL的字符串操作非常受限不像原生SQL那么丰富。所以在ABAP中别在SQL里做太多字符串处理能取出后在ABAP层面操作就不要依赖SQL函数。尤其HANA上SQLScript函数很多但ABAP Open SQL解析器只支持它们的一个子集。这种不一致造成的问题往往是“本地测试好好的上到S4/HANA突然报语法错”。还有那个著名的“内表作为WHERE条件”的扩展在ABAP 7.4以上可以使用“(lt_tab)”作为SQL的查询变量就是所谓的“SELECT … FROM … FOR ALL ENTRIES”的替代品叫“AMDP/子查询”其实是指“ABAP SQL Expression with Table”在括号内使用内部表。但要注意这个表会被按值传输到数据库量不能太大。比如SELECT carrid FROM sflight WHERE carrid IN (lt_carrid)这里lt_carrid是一个内表系统会将其炸裂为多个值。但内表超过一定大小会报“TOO_MANY_ITEMS”通常上限是1000个左右取决于版本和数据库。所以明明有FOR ALL ENTRIES的心智负担还是老实分块。调试真实问题的经验往往最能说明白。曾经有个后台作业处理一万多行数据每次到第3500行必然dump。查了很久发现是Open SQL中使用了动态表名比如“TRANSPORTING”字段列表里包含了不存在的字段。动态编程动态字段、动态表名会让Open SQL在运行时才做语法检查结果某些条件下字段名拼错或表类型不匹配直接抛短转储。解决方法是动态部分先用ABAP语句检查表结构和字段存在性或者干脆避免动态。SAP的ABAP运行时虽然有代码生成但错误信息不友好排查成本极高。从那以后我只在不得已时用动态Open SQL比如通用报表且每次都加上TRY-CATCH。另一个真实案例一个接口程序频繁死锁。原因是在UPDATE一张表后没有刷新或清理缓冲区导致别的进程读到旧数据然后回退重试。其实死锁跟Open SQL选用的锁机制也有关。SAP的锁对象ENQUEUE是ABAP层的锁和数据库锁是两回事。如果你只依赖UPDATE语句会拿数据库行锁但如果你在ABAP层用LOCK OBJECT会独立于数据库事务。这两者的交互很容易让人懵。建议规则全局锁用ENQUEUE内部计数用数据库更新。更新完成后记得DEQUEUE。别把ENQUEUE和COMMIT的顺序搞乱否则锁提前释放或永久占用都可能。Open SQL里的“UP TO 1 ROWS”经常和“ORDER BY PRIMARY KEY”配合。有些ABAP版本要求ORDER BY的字段必须是主键的一部分不是是如果包含ORDER BYSELECT SINGLE不被允许不是“SELECT SINGLE”不能加ORDER BY。所以要用“UP TO 1 ROWS”来取给定排序下的第一条。另外内表排序后取第一条可以用READ TABLE … TRANSPORTING NO FIELDS然后看SY-SUBRC这个比SELECT还快因为内存操作。很多新手混淆了数据库排序与ABAP排序乱用ORDER BY导致性能浪费。实际上如果要把结果内表按某种顺序处理直接在ABAP里SORT更快数据库排序占用临时表空间在数据量大时很危险。建议除非必须用LIMIT或UP TO结合ORDER BY来筛选否则一律SELECT后ABAP排序。还有“SELECT … AS”别名在Open SQL中几乎可以忽略因为ABAP字段符用工作区你根本不需要别名。但有一种情况当你SELECT一个聚合表达式时比如“COUNT() AS cnt”ABAP里没有直接接收聚合值的字段你需要定义一个带有CNT字段的结构。这时AS起作用。注意COUNT()返回类型是INT4而SUM可能产生DECIMAL或INT4精度可能丢失。要小心。例如DATA: lv_cnt TYPE int4. SELECT COUNT(*) AS cnt FROM sflight INTO lv_cnt.这里直接INTO一个基本变量是允许的吗在ABAP 7.4以后可以。老版本需要INTO CORRESPONDING FIELD OF WA。所以写代码时确认ABAP版本。还有“SELECT AVG(…)”可能返回小数但整数变量会舍入最好用f类型接收。关于“MAX”和“MIN”不推荐在Open SQL中对字符串类型使用因为数据库排序规则可能字母大小写不敏感结果不可靠。更稳的是把所有记录取到内表然后ABAP的MAX/MIN或SORT。毕竟ABAP的字符串比较有明确定义。再谈一个分布式场景SE11的“Foreign Key Check”和Open SQL无关但不少人误以为数据字典的表字段就是数据库真字段。实际上SAP表中的某些字段如“MANDT”在Open SQL中会自动处理但“DML”语句中的表名如果加了“CLIENT SPECIFIED”那就要自己写MANDT条件。这个东西属于高级用法建议避开。如果用了CLIENT SPECIFIED记得在WHERE里显式加MANDT SY-MANDT并且使用SELECT … IN CLIENT SPECIFIED不语法不是那样。我翻下记忆ABAP的“CLIENT SPECIFIED”是加在表名后的比如“FROM sflight CLIENT SPECIFIED”。这样会让ABAP运行时不做自动客户端限定然后你必须自己处理MANDT。这种写法通常用于系统工具普通应用别碰。写到这里其实最大的“注意事项”是培养数据库思维。很多人写ABAP默认所有数据都在内存中忽视了Open SQL是关系数据库的翻译层。一条SQL走没走索引扫描了多少行返回了多少字节这些本应用性能监控STS/ST05去观察。我习惯在开发环境打开SQL追踪把每条Open SQL的“DB Operation”和“Row Count”记下来。有一次我发现一个简单的SELECT竟然扫描了全表因为WHERE里对索引字段用了“CONCAT( , )”函数导致索引失效。换成等值条件后性能提升几百倍。这个经历让我深刻认识到无论ABAP的封装多友善数据库原理才是底牌。关于“BYPASSING BUFFER”和“DEFAULT JOIN”也会在一些超大数据量场景中用到。BYPASSING BUFFER会绕过ABAP表缓冲强制走数据库。对于实时性要求高的查询比如刚用SE16N看到的数据你写了缓冲可能从缓冲里读到旧值。这时直接用BYPASSING BUFFER但代价是每次访问数据库。如果只是偶尔交互没问题。DELETE和UPDATE的“SCOPE”问题也值得注意。Open SQL中的副作用如触发器在相同工作区程序中不可见。例如在更新后立即SELECT可能从数据库缓冲中读到旧值即“uncommitted data”。这个问题在SAP NetWeaver中经常出现。解决办法是在更新后“COMMIT WORK”但会引入锁。或者使用“SET PARAMETER”语言不确切。真正常用的是“WAIT UP TO … SECONDS”这显然太粗暴。更实际的是如果必须立即读回使用“SELECT SINGLE … BYPASSING BUFFER”强制读库再在ABAP内存中做缓存。关于性能不得不提“SELECT *”对列存储的影响。在HANA中列式存储的表SELECT * 会导致所有列被加载即使只需两列。这会浪费大量内存。所以HANA环境下一定要精确列出字段。另一个相关的是HANA支持“GROUPING SETS”但ABAP Open SQL并不完全支持。如果要用复杂聚合考虑用AMDP或数据库过程。AMDP是ABAP Managed Database Procedure它可以直接写SQLScript是处理复杂SQL的正确姿势。标准Open SQL留给简单查询就够了。还有“内表行数”与FOR ALL ENTRIES的交互。很多生产程序将海量数据放到FOR ALL ENTRIES的内表中导致语句超长。我处理过一个案例内表有50000行生成的SQL脚本有几十MB数据库直接拒绝执行。于是改成循环按1000行分块每次SELECT再收集结果。这里有个技巧分块时保证内表没有重复条目用SORT BY … DELETING ADJACENT DUPLICATES。因为FOR ALL ENTRIES会自动去重但分块后可能每块都有重复浪费数据库资源还可能影响结果集大小。最后还想提一个很多人忽略的“算术运算与字段类型”。Open SQL中WHERE条件里的变量类型如果与数据库字段类型不匹配ABAP运行时可能会强制转换。例如字段是CHAR10你传一个NUMC10内部会进行隐式转换可能导致索引失效。更糟糕的是数字字段传成字符数据库排序规则会按字符串比较得到错误的结果。所以务必保证变量类型和字段类型一致或者用“CONV#( )”显式转换。在ABAP 7.40以上的“-ITF”命名规范可以在SQL中使用“lv_var”来提示运行时识别变量类型。总之多观察程序日志里SQL的绑定变量能发现很多蛛丝马迹。不要依赖“数据库索引”来解决所有SQL性能问题。Open SQL不管你的索引策略它只负责执行。但是SAP中表索引有时被缓冲或忽略。关键时刻你可以使用“USE INDEX”提示但在SAP标准表中不建议因为应用服务器与数据库索引映射不透明。更强力的工具是“SELECT OPTIONS”或“RANGES”可以构造复杂条件但要小心“OPT”等组合逻辑。用“SIGN”和“OPTION”生成的WHERE子句在数据量大的时候非常低效。尽量用简单的等值条件。写这篇文章的时候我想起一次排障某个报表在测试环境跑2秒生产环境跑20分钟。对比了数据量差异不大最后发现生产环境数据库统计信息过期优化器选择了错误执行计划。ABAP无法直接控制优化器但可以通过“ST05”的消化计划看到。解决办法是让DBA更新统计信息或者调整SQL语句的写法比如将子查询改为JOIN让优化器别脑抽。这种问题防不胜防但积累经验后你会形成一种直觉哪种SQL模式在跨数据库时更稳健。总结我的经验性建议不是教科书总结第一把Open SQL当成数据库的“翻译官”不要试图在ABAP里写出花式SQL简单直接最可靠。第二每写一条SELECT心里盘算一下它要扫多少数据返回多少行。如果答案是“全表”马上回头看看能不能把条件收紧。第三FOR ALL ENTRIES用前先判空用后观察总行数别偷懒。第四永远不要假设数据库排序是稳定的。第五动态SQL是大坑能不用就不用。第六把COMMIT和锁对象当成事务的一部分谨慎设计否则死锁和脏读会像幽灵一样缠着你。第七调试SQL性能ST05和SQL Trace是必杀技但要会看理解“Buffer”和“DB”的区别。Open SQL这只老狐狸你越懂它它越听话。希望这篇笔记能让你少踩几个坑。下次遇到诡异的SQL问题记得先检查自己是不是在哪个细节上得罪了它。
返回列表