043、表类型与表定义
043、表类型与表定义
昨天半夜被一个实习生拉去排障,程序逻辑看起来完全没问题,数据也没问题,就是把一条记录INSERT到内表的时候,运行时直接dump了。报错是ITAB_ILLEGAL_SORT_ORDER。我一看,这哥们用的内表是SORTED类型,往里面塞数据的时候,并没有按关键字顺序放入。他一脸无辜:“我循环里加了排序啊,怎么可能没排序?”我让他把定义贴出来,他写的是:
DATA: lt_itab TYPE SORTED TABLE OF mara WITH NON-UNIQUE KEY matnr.这定义看着没问题吧?问题出在他循环里用INSERT往这个表里插数据时,数据来自两个不同的源,合并后乱了序。SORTED表要求你插入时键值已经有序,或者你直接APPEND,但APPEND在SORTED表上很容易出问题,因为你没法保证后加的键值比之前的大。实际上,SORTED表内部用的是二叉搜索树,你插入一个比根节点小的数据,树会自动调整,但ABAP运行时为了性能,做了个假设:你要是用APPEND,那必须保证键值单调递增。如果违反了这个假设,它不一定报错,但如果你用了INSERT,它会严格检查键的顺序,一旦发现前一条比后一条大,直接抛异常。
这事让我想起另一个坑:HASHED表。很多人以为HASHED表就是哈希索引,访问快,就什么都用它。但HASHED表最怕你用LOOP加SORT。它本身是无序的,你没法按索引访问,更不能APPEND(因为哈希表根本不允许APPEND,只能用INSERT)。你要是想对它排序,对不起,你不能对HASHED表直接SORT,必须先COPY到STANDARD表里再排序。这不是性能问题,这是语义上不允许。
所以,回到“表类型”这四个字,在ABAP里其实有两层意思。一层是数据字典里的“表类型”(Table Type),它是描述内表结构的类型对象,属于DDIC里和域、数据元素、结构体并列的元数据类型。另一层是我们日常挂在嘴边的“内表类型”,就是STANDARD、SORTED、HASHED这三种。而这三种类型,在数据字典里又必须通过一个Table Type来定义,不然你没法在ABAP程序里直接引用一个字典表类型作为内表类型。这两者千万别搞混。
我见过不少从其他语言转过来的朋友,一开始被“表类型”这个名字唬住,以为是数据库表的结构定义。其实数据库表的定义在ABAP里叫“透明表”(Transparent Table),它对应物理上的数据库表,而Table Type是纯逻辑层的内表模板。你在SE11里创建的是Table Type,给别人用的时候,别人直接用TYPE table_type_name就能得到一个内表。透明表则要用TYPE STANDARD TABLE OF dbtab之类的方式去引用。这是两个完全不同的东西,甚至可以说一个管内存,一个管存储。
现在展开说下三种内表类型。STANDARD表是最普通的,它按插入顺序排列,可以用索引访问,也可以键访问。它的查找是线性扫描,数据量大时会慢,但ABAP也会用二分法查找——前提是你先SORT,并且用READ TABLE WITH KEY或者SORTED BY。注意,如果你没排序就去用BINARY SEARCH,那你得到的结果是未定义的,可能在调试时看起来对,但上线后偶发错乱。这不算踩坑,这算自爆。
SORTED表一旦声明了关键字,你插入的数据就得符合排序规则。它的好处是读取非常快,READ TABLE直接用键值二分查找,不需要先SORT。但它的写入代价高,而且容易触发上面说的dump。所以在SAP项目中,SORTED表通常只用来存配置数据、主数据这些静态内容。动态拼接的临时数据,千万别用SORTED。
HASHED表则是为等值查询优化的,键唯一性要求高,内部采用哈希码映射。它没有索引,不能做LOOP操作,也不能SORT。如果你需要大表快速取单条记录,并且键唯一,用HASHED很合适。但如果你要把多个表关联或者做嵌套循环,HASHED反而会因为不支持索引扫描而变慢,但它的READ TABLE速度是O(1),和SORTED表的O(log n)有本质区别。
实际开发中,我用STANDARD表占了九成。为什么?因为ABAP内表操作太灵活了,APPEND、INSERT、DELETE、MODIFY、SORT、LOOP全部都能用。SORTED和HASHED只是特定场景的优化。不要为了炫技而用高级表,除非你明确知道性能瓶颈在哪。曾经有个项目,某个接口需要批量读取物料主数据,用了STANDARD表然后READ TABLE WITH KEY,在数据量上万以后明显卡顿。我把那个内表改成SORTED表,并且确保数据插入时按MATNR排序后,接口耗时从5秒降到0.3秒。这才是用SORTED表的正确姿势:读多写少,插入有序。
再讲一下数据字典里的Table Type定义。SE11创建Table Type时,有几个关键字段:行类型(Row Type)、访问方式(Access Type)、键定义(Key Definition)。访问方式就是STANDARD、SORTED、HASHED,还有RANGE(特殊类型,用于表示区间)。键定义可以指定非唯一或唯一。你把这个Table Type建好后,在程序里DATA: lt_itab TYPE zts_mytype.就直接得到一个内表。这个内表的类型和属性完全由字典控制,好处是同一个表类型可以被多个程序复用,改一处全生效。而且该类型还能作为函数方法接口的参数类型,跨程序传递内表时不用反复定义结构。我们项目里有个公共的ZTT_AMC_MSG,就专门用来传消息列表,所有接口都引用它,测试和排错都能统一处理。
但有个细节容易坑人:如果你在SE11里定义了一个SORTED表类型,但它允许非唯一的键,那么在ABAP里使用它时,如果插入相同键的数据,会排在后面,符合预期。但如果你定义成唯一键,却试图插入相同键,直接运行时错误。这比SORTED表的排序错误更隐蔽,因为很多人以为非唯一=可以重复,唯一=不能重复,但在SORTED表下,唯一键是依靠键值比较来保证的。敲代码时别只想着业务逻辑,务必先看表类型的键定义是UNIQUE还是NON-UNIQUE。
还有一种“表定义”聊法,是指创建透明表。透明表在SE11中定义,它包含字段、数据类型、长度、是否允许空值,以及搜索帮助、外键等。这个定义和Table Type不同,它最终会在数据库中生成一个物理存储表。ABAP程序里访问透明表,一般用OPEN SQL,比如SELECT * FROM ztable INTO TABLE @gt_data。透明表本身的定义直接决定了你的数据存储结构,所以字段类型选择上要谨慎。这里有个常见坑:数据库表字段类型如果是QUAN或CURR,需要同时定义参考表域中的单位字段,否则激活时可能报错;或者激活后数据精度有问题。定义表时还要注意,不要随便用CHAR类型做日期字段,要用DATS,否则排序大小和范围都可能出错。这些都是老生常谈,但每次项目里总有人犯。
还有,如果你在透明表定义时加了某个字段做索引,查询时能用到;但如果建了太多索引,写入就会变慢。索引不是越多越好。我的经验是:一个表最多三个索引,优先放在外键和常用过滤条件上。别为一个偶尔跑的报表建索引,那只会拖累日常业务。
写到这里,回到我那个实习生的问题。他最后怎么解决的?我让他把内表类型从SORTED改回STANDARD,然后如果需要按物料号快速读取,就用SORT BY matnr,再READ TABLE WITH KEY matnr = ... BINARY SEARCH。这样既不会dump,性能也够用。但他不甘心,问为什么不用SORTED表然后每次插入前排序?我说你插入前排序意味着你每来一条数据都要对现有表排序,那比维护一棵二叉搜索树还慢,而且还要处理数据来源顺序的不确定性。何必呢?内表类型选择是权衡,不是炫技。
最后说个实用的小建议。如果你在一个循环里往一个STANDARD表里插入数据,并且之后要按某个字段频繁读取,别急着APPEND,你可以先APPEND,等循环结束后再SORT,然后READ时加BINARY SEARCH。这是最经典、最不容易出错的组合。如果你确定数据本身有序,也想省那一次排序,那就直接声明SORTED表,但插入时一定要保证顺序,否则dump了别怪我没提醒。如果你要的是按主键查一条,并且主键唯一,那HASHED表是不错的选择,但你要不要再想着对它做SORT或LOOP动态修改了——那不是它该干的事。
表类型和表定义,看起来只是ABAP里几个名词,但用不好就会在半夜收到runtime error短信。你是想半夜被叫醒,还是想安稳睡个觉?先从这几种类型的底层逻辑开始理解吧。
