http://saptree.blog.163.com/blog/static/21318513820121127103050278/
大部分ABAPer都是從SAP報表及打印開始學起的,大家也都認為寫個SAP報表程序是最簡單不過的事了。
但是實際情況真的如此嗎?寫報表時除了保證數據的准確性,您可曾考慮過報表的性能問題嗎?
由於報表程序是被最多SAP用戶所訪問的,所以性能差的報表很可能會引來大量的抱怨和質疑,大大降低用戶滿意度。
最近做了較多性能優化方面的工作,頗有感觸,在此進行歸納總結,希望對大家有所幫助,也歡迎大家討論。
1, 關於表連接語句(INNER JOIN, LEFT JOIN…)
寫報表的時候,表與表之間的關聯是不可避免的。通常而言,表連接語句要掌握的原則有:
(1) 將最有效的查詢條件所對應的表放在第一位。換言之,讓查詢第一個表后所得到的結果集就盡可能小。
比如有一張報表叫做訂單狀態統計表,可能查完VBAK、VBAP后還查詢LIPS、VTTP等,界面上的查詢條件很多。不過據了解得知,該報表主要是查看昨天創建或前幾天創建的訂單。那么最有效的限制條件自然是訂單創建日期,以及銷售組織了,而表連接的主表宜選用VBAK。
(2) 確定了表連接的次序后,應考慮將查詢條件盡量限制在靠前的表里。比如選擇屏幕上有個物料號的查詢條件,而我們知道訂單VBAP和交貨單LIPS均有物料號,那就應該視情況而定,如果表連接中VBAP更早出現,那么WHERE子句中就使用vbap~matnr IN s_matnr,反之就是lips~matnr IN s_matnr。
(3) 兩個表之間進行連接的時候,應考慮關鍵字段或索引字段的作用。比如查詢VTTP和LIPS時,關聯關系是vttp~vbeln = lips~vbeln。那么vttp在前,lips在后,就會比較快,因為根據vttp的vbeln查詢lips時,vbeln是lips的關鍵字段,速度較快。而反過來如果lips在前,那根據lips~vbeln查詢vttp會慢一些,除非vbeln是vttp的索引字段。
2, 先構建RANGE再執行SQL語句
有時我們所能用的查詢條件不是很理想,比如查詢LIKP卻必須用公司代碼,而非銷售組織。
此時普通做法是用LIKP與TVKO根據VKORG進行表聯接,從而限制TVKO~BUKRS的值。
還有一種有效的辦法是,通過TVKO查詢到當期公司代碼所對應的全部銷售組織,從而組建一個RANGE出來,再根據此RANGE查詢LIKP。當然要注意RANGE的行項目有上限的,在ECC6中大概2萬行將導致ABAP DUMP。
提示:DATA r_vkorg TYPE RANGE OF likp-vkorg.
SIGN = ‘I’, OPTION = ‘EQ’, LOW = ‘XXXX’ 即可往r_vkorg中放入多個單值。
3, For All Entries與Select Single的比較
就個人而言,筆者不是很喜歡For All Entries語句,因為它的缺點多於優點。很多人都會說,為什么呀,For All Entries不是比Select Single快么?事實到底怎樣呢,讓我們做個比較。
假設我們有個內表代表銷售訂單的行項目,該內表有10萬行。此時我們要根據LIPS的VGBEL和VGPOS,查詢這些訂單行項目對應的交貨單行項目。
如果用SELECT SINGLE的話寫法很簡單:
LOOP AT it_vbap INTO wa_vbap.
SELECT SINGLE vbeln posnr
FROM lips INTO (wa_vbap-vbeln_vl, wa_vbap-posnr_vl)
WHERE vgbel = wa_vbap-vbeln AND
vgpos = wa_vbap-posnr AND
vgtyp = ‘C’.
MODIFY it_vbap FROM wa_vbap.
ENDLOOP.
如果用For All Entries則寫法分2步:
SELECT vbeln posnr
FROM lips INTO TABLE it_lips
For All Entries IN it_vbap
WHERE vgbel = it_vbap-vbeln AND
vgpos = it_vbap-posnr AND
vgtyp = ‘C’. “此交貨單是根據訂單創建的
LOOP AT it_vbap INTO wa_vbap.
READ TABLE it_lips INTO wa_lips WITH KEY vgbel = wa_vbap-vbeln
vgpos = wa_vbap-vgpos.
IF sy-subrc = 0.
wa_vbap-vbeln_vl = wa_lips-vbeln.
wa_vbap-posnr_vl = wa_lips-posnr.
MODIFY it_vbap FROM wa_vbap.
ENDIF.
ENDLOOP.
對於SELECT SINGLE而言,由於LIPS有個VGB的SAP自帶索引,每次查詢都挺快,即便循環10萬次,速度雖然快不了但也沒什么大的危害。
對於For All Entries的第一步,的確比SELECT SINGLE快些,本來10萬次的SELECT SINGLE,變成了1萬次的查詢(如果BASIS設置了參數為10),每次查詢10個訂單行項目。但是第二步就很慢了, 暫估it_lips為8萬行,則對於it_vbap的10萬個行項目,有8萬個行項目執行READ TABLE語句平均需要4萬次才能搜索到it_lips的目標行,另有2萬個行項目將讀遍it_lips然后才發現沒有對應的目標行。所以我們一共需要搜索48億次,太慢了!
在此針對For All Entries的使用提出幾點意見:
(1)如果是根據某數據量大的內表用For All Entries讀取數據量小的配置表,比如TVAK/T006等,那不如把For All Entries直接去掉,把表里的幾十條數據全部取出。
(2)使用For All Entries時,SELECT語句后面的字段必須包含所查表關鍵字段。比如上面的vbeln/posnr就是lips的關鍵字段。如果不含關鍵字段,比如SELECT lfimg FROM lips For All Entries ***,那么當LIPS中兩個條目關鍵字段不同而lfimg相同時,會被SAP自動過濾掉一條。
(3)上面關於性能問題,應該利用BINARY SEARCH、SORTED TABLE或者HASHED TABLE來解決。詳見下面第四節。
4, 關於BINARY SEARCH/SORTED TABLE/HASHED TABLE的使用
BINARY SEARCH即二分法查找,在保證內表按查詢字段以升序排列的時候,可以采用二分法查找。二分法查找的速度很快,最大查詢次數為log2n。以上面的例子來說,如果it_lips事先按vgbel和vgpos排好序,則每次查找最多不超過17次。則對於it_vbap的10萬個行項目,僅100多萬次就可以搞定了!
當使用READ TABLE語法時,如果查詢字段跟SORTED TABLE的排序開始字段能匹配上,則SAP將自動采用二分法查找。比如it_lips是SORTED TABLE且以vgbel和vgpos排序,則當read table以vgbel進行查找時,系統會自動采用二分法。但如果read table以vgpos和其他字段進行查找,由於vgpos並非SORTED TABLE的第一排序字段,系統將采用直線查找,速度會慢很多。總之,SORTED TABLE的排序字段次序也很關鍵。
針對STANDARD TABLE排序后可以進行二分法查找,使用SORTED TABLE也可進行二分法查找,那么二者有什么區別呢?簡單來說,由於SORTED TABLE自始至終都保持排序,如果需要對內部進行頻繁的插入、刪除操作,則不推薦使用SORTED TABLE,性能會很差。而另一方面,如果我們要用的是類似LOOP AT it_lips WHERE vgbel = ** 的語法(而非READ TABLE)時,對於STANDARD TABLE就無法采用二分法查找,或者說會很麻煩。而對於SORTED TABLE,系統會自動采用二分法優化查找過程。
至於HASHED TABLE,筆者用得也不太多。查看SAP的標准代碼,貌似用得也沒很多。理論上HASHED TABLE可以比SORTED TABLE更快些,但需要耗用更大的存儲空間。當某程序采用了二分法查找之后,如果效果還不是很理想,建議可以用HASHED TABLE試試。
5, 將循環內的重復性工作進行緩沖
有時我們的內表數據量很大,但又不得不在每次循環的時候,都進行類似的一些操作,比如調用函數FI_PERIOD_DETERMINE獲取某日期對應的會計年度和期間,調用函數MD_CONVERT_MATERIAL_UNIT進行單位轉換,等等。每個函數的調用背后都要執行一系列的讀表以及運算工作,程序的效率明顯下降了。所以我們得想出有效的辦法。
比如針對FI_PERIOD_DETERMINE的調用,可以改用循環前對函數G_PERIODS_OF_YEAR_GET的調用。根據公司代碼BUKRS讀取T001-PERIV,然后調用G_PERIODS_OF_YEAR_GET獲取某會計年度每個會計期間對應的起始日期和結束日期。這樣在循環內部,只要根據上面的結果即可算出某日期對應的會計年度和期間了。
又比如針對MD_CONVERT_MATERIAL_UNIT的調用。相信對於it_vbap的10萬個行項目,物料號重復的有很多。所以可以先匯總物料號,然后一次性讀取表MARM以存儲換算關系。有了MARM的換算關系,循環中大量的單位換算就可以自己算了,如果無法換算的再考慮調用函數MD_CONVERT_MATERIAL_UNIT。(函數MD_CONVERT_MATERIAL_UNIT除了讀取MARM的換算關系,還會考慮同一維度單位間的換算關系比如G和KG的關系,所以其功能更強大。)
6, 關於字段的增強以及TABLE INDEX的創建
這里提到字段的增強,主要是性能方面相關的。假設我們需要基於系統所有的billing document做個動作,比如將其導出到金稅系統。至少有兩種方案:第一是新建一個表,專門記錄已經導出到金稅的開票憑證;第二是在系統標准的開票憑證抬頭表vbrk中新增一個字段,記錄是否已導出到金稅。
用戶在處理業務的時候肯定會反復查詢“未導出到金稅”的所有開票憑證。那么第一種方案下,我們需要先查詢VBRK表(比如得到1萬條記錄),然后針對自建表的記錄(比如得到9800條記錄)做個減法,最后得到200條記錄的結果集。而第二種方案就快多了,查詢VBRK的時候判斷新字段=“未導出”即可。
隨着業務的持續,VBRK表的條目將越來越多,而“未導出”的條目則會維持在一個較為平穩的數字上,為了有效區分歷史數據和現用數據,可添加TABLE INDEX,提高報表的查詢速度。很多SAP標准表都自帶了一些索引,這些索引大都比較實用。
創建索引需要注意以下幾點:
(1)索引會占用額外的數據庫空間,還會降低插入/修改的速度(雖然可提高查詢速度),所以需要考慮實用性,肯定不是越多越好。如果表中已有類似的索引,則不推薦新建。而對於容量大的、被多個程序訪問的表加索引就更要謹慎了,比如VBFA、MSEG、FAGLFLEXA、LIPS、VBAP、EDIDC、STXH等等。
(2)創建索引時應注意字段的先后次序,MANDT是必須的而且都要放在第一位。字段的先后次序取決於實際業務需要。另外索引的字段不宜太多,字段越多占用的數據庫空間就越多,對於插入/修改的影響也更大。
7, 選用一些替代表/替代字段(VBFA, SHP_IDX_)
曾做過一些類似於“未揀配交貨單”、“未發貨過賬交貨單”的報表,剛開始用的是LIKP、VBUK等表,速度並不理想。后來調試了標准程序VL06O,發現其用的表是SHP_IDX_PICK、SHP_IDX_GDSI等。原來系統在創建交貨單的時候,也會更新這些臨時表。當揀配完成,該條目就從SHP_IDX_PICK中刪除。所以表SHP_IDX_PICK的條目數始終不多,查詢速度很快。
同樣的,當我們在多個表中進行查詢時,可能不同的限制條件都能達到同樣的結果集,但效率差異就很大,所以選用有效的字段非常關鍵。比如需要查詢某銷售組織某天已發貨的銷售訂單,我們可以將VBAP與LIPS聯查。此時查詢條件VBAK-LIFSK=SPACE或VBAP-ABGRU=SPACE並不影響查詢結果,但它們可以在第一時間就排除大量無關的訂單,對於性能的提升幫助很大。
8,考慮企業的特點及數據量狀況
做優化不是簡單的技術活,既要考慮報表的實際需求,也要考慮企業的業務狀況。比如采用零售模式的企業客戶量很大(KNA1表條目多),批發模式的企業客戶量較小;食品飲料行業的物料號一般不多,而機械行業的物料號則往往多而繁雜;零售業的訂單量很大、時間性很強,所以最有效的查詢條件往往就是組織編碼+日期,部分行業則可能訂單量小但價值高。只有充分考慮到企業的業務特點,才能更有效地提高報表性能。