ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

032、内表排序与二分查找

032、内表排序与二分查找 调试了一个下午最后发现是二分查找出了问题。内表数据明明存在READ TABLE却返回sy-subrc 4而且更诡异的是有时候能查到有时候查不到完全看数据心情。后来把BINARY SEARCH去掉顺序查找一切正常。那一刻的心情相信搞过ABAP的都懂。问题就出在内表排序和二分查找的配合上这不是一个简单的语法问题背后藏着ABAP内表索引的实际行为。今天这篇笔记就把这个坑彻底讲透。先说排序。ABAP里内表排序用SORT语句标准语法很简单SORT lt_itab BY field1 field2 ASCENDING field3 DESCENDING.ASCENDING可以省略默认升序。DESCENDING要写清楚只作用于紧挨着它的那个字段。注意这里有一个容易踩的地方BY后面的字段顺序决定了排序的优先级左边优先于右边。比如SORT BY A B是先按A排A相同再按B排。如果你写SORT BY B A那就是按B排B相同按A排结果完全不一样。排序默认是按数字大小还是按字符字典序这取决于字段类型。数字类型按数值大小字符类型按二进制值比较实际是代码页里的顺序一般就是字典序。字符串排序时要注意如果字段长度固定比如CHAR10那排在“A2”后面的是“A10”因为字符比较是逐位比较“A2”末尾跟着空格空格小于’1’所以“A2”会排在“A10”前面。这个细节在二分查找时很容易出错因为二分查找依赖的排序顺序和比较规则必须和SORT时完全一致。再说排序的稳定性。ABAP标准内表排序是不稳定的。什么意思如果两条记录排序字段值相同它们排序后的相对位置不保证和排序前一样。如果你需要稳定排序得自己加一个序号字段排序时把序号也带上。否则别依赖相同键值的内部顺序。这个坑在二分查找里更明显后面说。接下来是二分查找。ABAP里对标准内表做二分查找写法是READ TABLE lt_itab WITH KEY field lv_value BINARY SEARCH.就这么一行但前提极其苛刻。内表必须已经按WITH KEY里指定的字段排序而且排序方向和字段顺序必须一致。什么叫一致你SORT BY A B那么BINARY SEARCH的WITH KEY也得是A和B顺序不能变。如果你SORT BY A B然后READ TABLE WITH KEY B … A … BINARY SEARCH那结果就是玄学。别问我为什么ABAP的实现就是要求这个匹配。这里有个实际调试场景。我用SORT BY field1 field2然后二分查找时写的是READ TABLE lt_itab WITH KEY field2 lv_b field1 lv_a BINARY SEARCH.结果时对时错因为内表按field1排序但查找按field2先过滤二分算法直接在中间跳着找根本不知道你把field2放在前面。把顺序改成和SORT一致后问题立刻消失。还有一个大坑排序方向。SORT BY field ASCENDING那么BINARY SEARCH默认也认为内表是升序。如果你用DESCENDING排序比如SORT BY field DESCENDING然后BINARY SEARCH不指定方向ABAP默认按升序二分结果肯定错。ABAP里BINARY SEARCH没有直接指定升序降序的语法所以要么你老老实实按升序排要么你写两个二分查找的变通方案比如用SORT BY field DESCENDING然后READ TABLE … BINARY SEARCH之前先做个倒序索引。其实最简单的方法就是永远只用升序别在排序方向上玩花活。再说说查找失败时的行为。顺序查找时READ TABLE找不到记录sy-subrc 4sy-tabix可能是内表行数加1或者未定义。二分查找失败时sy-tabix指向的是一条“逻辑上应该插入”的位置但不一定等于下一条记录的位置。这里有ABAP的一个经典怪异行为当二分查找命不中时sy-tabix会被设置为比目标键小的最大记录的位置或者是比目标键大的最小值的位置具体取决于实现。所以千万别在sy-subrc 4时用sy-tabix去访问内表行那可能是一个无效索引也可能指向一条莫名其妙的数据。我在二次开发时见过同事写READ TABLE lt_itab WITH KEY id lv_id BINARY SEARCH. IF sy-subrc 4. lv_index sy-tabix. 然后插入一条新记录到lv_index位置 ENDIF.这个逻辑在顺序查找时可能还能用但二分查找时完全不可靠。正确做法是先顺序查找或者用LOOP AT … WHERE或者直接APPEND后重新排序。还有个重复键值的问题。二分查找在遇到重复键值时返回哪一条ABAP标准行为是如果排序稳定通常返回第一条。但标准内表排序不稳定所以无法保证一定是第一条。如果你需要处理重复键值的所有记录别用READ TABLE … BINARY SEARCH要用LOOP AT … USING KEY BINARY SEARCH? 其实标准做法是READ TABLE lt_itab WITH KEY key_field lv_value BINARY SEARCH. IF sy-subrc 0. lv_first sy-tabix. LOOP AT lt_itab INTO ls_itab FROM lv_first. IF ls_itab-key_field lv_value. EXIT. ENDIF. 处理记录 ENDLOOP. ENDIF.但这样依然基于一个假设从返回的那一条开始后续相同键值连续排列。这依赖于排序的正确性。如果排序字段不完整比如你只按key排而key相同但其他字段顺序乱那二分查找返回的可能是任意一条相同键值的记录但相同键值记录确实是连续的因为排序只按key排。所以循环处理相同键值是安全的但返回的位置不确定是哪一条。如果业务要求处理所有重复键值上面的循环没问题如果只处理一条那要小心业务上是否允许随机选择一条。性能方面标准表顺序查找是O(n)二分查找是O(log n)。内表数据量大时差距明显。但二分查找的开销在于必须先排序。如果内表只读一次顺序查找也许更快。如果频繁查找维护一个排序好的内表更划算。ABAP里还有排序表SORTED TABLE和哈希表HASHED TABLE。排序表按主键自动维护有序查找用READ TABLE WITH TABLE KEY时天然二分不需要你手动SORT。哈希表用哈希索引查找O(1)但没法按非主键字段排序查找。标准表灵活但需要手动管理排序。实际项目里我见过不少为了二分查找而二分查找的情况。内表只有几百行非要用BINARY SEARCH结果排序开销比线性查找还大。而且因为排序后可能破坏原有顺序还得额外备份代码复杂度上升反而容易出bug。我建议内表数据量低于几千行直接用顺序查找代码可读性好也不容易出错。数据量大且查找频繁优先考虑排序表或哈希表减少手动SORT和BINARY SEARCH的配对风险。再聊一个隐藏问题类型混洗。二分查找要求查找字段的字段类型、长度、符号性都和排序时一致。如果内表字段是INT4你用一个CHAR10的变量去查ABAP会做隐式转换。这会导致比较规则不一致排序是按INT4排的查找时却按字符比较结果就是找不到。遇到这个问题检查变量类型是第一步。有时候ALV导出的字段是LVC_T_CHAR类型你塞给一个整型字段做二分查找那肯定出问题。转换一下用正确的类型去查问题就消失了。还有个关于二进制搜索和表键的细节。标准表没有主键概念WITH KEY可以指定任何字段。但你SORT时必须确保排序字段覆盖了WITH KEY中的所有字段。如果WITH KEY包含字段顺序SORT仅按其中一部分排序二分查找就会失效。比如SORT BY A然后READ TABLE WITH KEY A lv_a B lv_b BINARY SEARCH这也不行因为排序表里只按A排列B在A相同的情况下不一定有序二分算法在比较A相等时无法用B继续二分。正确做法是SORT BY A B或者改用LOOP WITH KEY。调试这种问题时最直接的方法就是关掉BINARY SEARCH用顺序查找看结果。如果顺序查找正确二分查找不正确那100%是排序和查找不匹配。不要怀疑ABAP的二分算法有问题它很死板就是按你给的键在排好序的区间里跳。你给它的搜索键必须和排序键一样否则它就真的“二分”给你看。写代码的时候我习惯把排序和查找的键抽成一个常量字符串或者用宏老项目适合或者直接写注释。比如 这里排序和二分查找的键必须完全一致改一个另一个也得改 SORT lt_itab BY ebeln ebelp. READ TABLE lt_itab WITH KEY ebeln ls_po-ebeln ebelp ls_po-ebelp BINARY SEARCH.这样至少少一半的坑。还有一个场景内表经过APPEND、INSERT、MODIFY之后原来的排序可能被破坏。很多人排序一次之后后续往里加了几条数据忘了重新SORT直接二分查找。这必死无疑。ABAP的SORT不会自动应用标准表也没有自动排序能力。所以每次数据变动后如果要保持有序要么手动重新SORT要么用INSERT … INDEX插入到合适位置要么直接换成排序表。我个人方案是如果数据在程序生命周期内变化频繁就追加到一个待处理表最后一次性排序再处理如果必须在变化中查找用排序表或者自己维护索引。说到底BINARY SEARCH的本质是拿“提前排好序”这个信息去换性能。ABAP不会替你检查内表是否有序它假设你有序然后盲目地跳。如果你没满足这个前提所有结果是未定义的。这也是为什么BINARY SEARCH出bug时特别隐蔽——它大部分时间能查到因为数据恰好落在某个区间偶尔查不到因为数据在另一次运行中排列不同。最后说点经验性的建议。第一新写的代码里优先考虑排序表或哈希表除非内表需要按多种不同键查找才用标准表加SORT。第二如果必须用BINARY SEARCH把SORT和READ TABLE放在一起中间不要隔太久的业务逻辑降低别人误改破坏排序的可能性。第三代码评审时看到BINARY SEARCH一定要找它的SORT语句确认排序键和查找键完全一致。第四测试数据里一定要包含重复键值、最大值、最小值、空值二分查找在这些边界上最容易暴露问题。第五别把sy-tabix在二分查找失败后当作插入点这是很多烂代码的根源。我自己的习惯是能用LOOP AT … WHERE解决问题就不去手动二分。ABAP里LOOP AT … WHERE对标准表也是线性扫描但可读性高很多而且在数据量不大时性能差异可以忽略。只有做到几十万行以上的数据才值得精细调优。那时候与其自己SORTBINARY SEARCH不如直接用FOR ALL ENTRIES或者数据库层处理。这种问题我也不是没犯过。有一次在报表里优化一个库存逻辑把顺序查找改成了二分查找单元测试全过结果集成测试时库存数据量一上去就掉链子。查了一个小时发现是SORT之后一个后续逻辑又给内表APPEND了几条没排序的数据。从那以后我就养成了习惯凡是内表有二分查找就在代码附近加一个醒目的注释注明“此表必须保持按XX排序任何修改后都要重新SORT”。这种注释看起来笨但真的能救命。内表排序和二分查找看似简单却是ABAP开发生态里非常经典的一道坎。它不涉及核心理念也没有难度上的门槛纯粹是落实层面的细节。但恰恰是这些细节让无数资深开发者也栽过跟头。希望这篇笔记能把那些坑摊开摆在你面前下次你再遇到READ TABLE返回4别急着怀疑人生去检查SORT和WITH KEY的配对关系。大概率就是它们在最基础的规则上悄悄背叛了你。
返回列表