In the previous blogs of this series (see below) the objects that were investigated with the Code Inspector were 'program-like', that is reports, function groups, or ABAP OO classes.

Code Inspector performance checks series:

  1. Table attributes check  (discussed in this blog)

But the tool can do a lot more to improve the software quality than just analyzing ABAP source code. In principle, all types of repository objects can be checked with respect to consistency of their technical properties.
 
In this blog I want to present the 'Table attributes check' of the Code Inspector, which analyzes the definition of ABAP data dictionary tables and views.

The properties of a database table or a database view are maintained within the ABAP dictionary (transaction SE11), for example on the tab strip 'Delivery and Maintenance' or in the 'Technical Settings'. Unfortunately, the ABAP dictionary does not assist you to choose table properties that are consistent and reasonable from the performance point of view.
The Code Inspector 'Table attributes check' provides messages if technical properties, buffer settings, or index settings appear to be dissonant. Please note that the Code Inspector has a simple rule set to identify such real or just would-be inconsistencies. Some of the simple rules of the 'Table attributes check' might need to be overruled by the wise developer in special cases.

Here is a short introduction into the most important technical parameters of a table or view in the data dictionary:

Delivery Class
This parameter determines, how the data in the table will behave during system installation, upgrade, client copy, and in a transport between systems. The most important delivery classes are A for application data and C for configuration data. Further delivery classes are L, G, E, S , and W. You can get more information on the delivery class within the fields' F1-help on tab strip 'Delivery and Maintenance' in transaction SE11.

Data Class
For some database platforms, this parameter determines in which physical area of the database the table will be created. The Code Inspector tool uses the data class as a categorization of the table with respect to it's data content. Since this influences some of the tools' check results, you should correctly maintain whether a table contains master data (data class APPL0), transactional data (APPL1), or configuration data (APPL2).

Size category
The size category is also maintained in the technical settings and is used to determine and reserve the initial size of a table in the database. The Code Inspector uses this parameter as an indicator for the real size of the table in production use.

Table class
This parameter determines the type of a table; the relevant ones for this check are TRANSP (transparent table), CLUSTER (cluster table), POOL (pooled table), and VIEW (general view structure).

Buffering and Buffering types
Tables that are frequently read during production use, but only rarely modified, such as those containing configuration data, should be buffered on the application server. The access to data in a buffered table is 10-50 times faster than accessing data in a non-buffered table, being read from the database cache.  In the technical settings of a table in transaction SE11, you are able to turn on the table buffering and select the buffer type (fully buffered, generically buffered, or single record buffering).

In the following diagram I depicted, how delivery class, data class, and buffering should be combined for (most of) the tables in your system. This diagram also reflects some of the rules according to which the Code Inspector 'Table attributes check' evaluates dictionary tables. As mentioned, there might be a few tables that do not fit into this scheme.  

Proposal for buffer settings consistent with data class and delivery class
Examples on how to read this diagram:

  • delivery class C (customizing table) should not be combined with data class APPL1 (transactional data)
  • a table that contains configuration data (APPL2), and which is assigned to delivery class S (system table), should be buffered
  • a master data table (APPL0) with delivery class A (application table) should not be buffered, except if it has only a few entries (size category 0, or single record buffered with size category 0, 1, or 2)

Indexes

Frequently executed database statements must be supported by a database index to allow a fast search of the required entries. There are some rough rules for database indexes: for example, there should not be too many indexes defined for one table. Also, two different indexes of one table should not have too many common fields.

Messages of the check

In the following I want to present the messages of the 'Table attributes check' and give you some hints what can be done to improve.

Messages 0001, 0002: No Table Class or Delivery Class Chosen; No Data Class or Size Category Chosen
Maintain these table categorizations. They are used by the Code Inspector to evaluate database accesses.

Messages 0003, 0004: "Buffering Permitted, but switched Off" Selected
From a performance point of view, this buffer setting should be omitted. You as developer should know whether some table is suited for buffering or not. Only if a table might be small at customer A and very large at customer B, this setting makes sense and should be selected.

Message 0010: Buffering Type is Initial but Delivery Class Is "C", "G", "E" or "S"
According to it's delivery class, the reported table is a configuration table (C or G), or a system table (delivery class E or S). Such tables are normally small and get rarely changed. Therefore they should be buffered. If the reported table does not contain configuration data or system data, you may have assigned the wrong delivery class to the table.

Message 0011: Buffering is Activated but Delivery Class Is "A" or "L"
According to it's delivery class, the reported table contains application data (A), that is master or transactional data, or it is a table with temporary data (delivery class L). In both cases it is expected that the table has many entries and is changed frequently. Normally, such a table should not be buffered. Eceptions are small master data tables, which might also be buffered. If the reported table does not contain the described type of data, the delivery class might be wrongly assigned.

Message 0012: Buffering is Activated but Size Category Is > 2
Very large tables should not be buffered, even if the other preconditions for buffering are fulfilled. One effect of buffering large tables is that numerous small buffered tables might be displaced from the SAP table buffer. If only a few entries of a large table are needed during program execution, it can be reasonable to choose single record buffering or a fine granular generic buffering for this table. If the reported table has only a few entries in production use, and buffering would be appropriate, you might have assigned a wrong size category to the table.

Messages 0013, 0014: Buffering is Initial but Data Class Is "APPL2"; Buffering is Activated but Data Class Is "APPL0"/"APPL1"
Data class and buffering must be consistent. Normally, tables with data class APPL2, such as configuration tables, should be buffered. Tables of data class APPL0 or APPL1, such as master data tables or transactional data tables, should not be buffered. As an exception from this rule, small master data tables may also be buffered.

Message 0015: Buffering is Activated but No Buffering Type Is Chosen
Choose an appropriate buffering type for the reported table, based on the planned accesses. Possible buffering types are fully buffered, generically buffered, and single record buffered.

Messages 0016, 0017: Buffering is permitted but table is contained in DB view db_view; Buffering is permitted but table can be changed using database view db_view
Until NetWeaver release 6.40, it was very critical if modification of a buffered table was allowed via a database view. The reason for this was, that data changes for the view did not lead to an invalidation of the table buffer - which could lead to data inconsistencies when reading data from the buffered table. Since release 7.0 the buffer is also invalidated, if there are changes for the database view.
But what about the first message - why is it bad, if there is a database view defined for a buffered table? The rational is that if some developer decided for the additional load that is implicated by activating the buffer option, most accesses to the table really should make use of the buffering. But the existence of a database view for the table indicates that also other accesses, which are not using the buffer, are planned.
Last two comments on this check: the messages are always reported for the buffered table, not for the database view. One reason is that the 'owner' of the table should have the control on how the data is accessed. She or he should be aware of alternative access strategies that might be implemented by other developers.
If the database view is also buffered, there is no message reported. But note that any invalidation of the underlying table will also invalidate the buffered view in the table buffer.

Messages 0020, 0021: Table has more than 100 (700) fields
For technical and design reasons, a database table should not have too many fields. For Business ByDesign the more strict value of 100 fields is applied, otherwise up to 700 fields are allowed.

Messages 0022, 0023: Change Log Active Despite Data Class "APPL0" or "APPL1";
Change Log Active for Large Table
The change log for a table is activated in the technical settings by selecting the flag 'Log data changes'. If the system parameter rec/client is also set for the client, a log entry is created for every change of the table. It's clear that for master data tables and for tables with transactional data (data classes APPL0 and APPL1) no change log should be created. The same is true for large tables in general (size category > 2). Writing change logs for such tables would slow down the production system.

Message 0030: Table Has Unique Secondary Index
That's just an information message with no performance relevance.
Every developer who inserts data into such a table should be aware, that the insertion of entries which are identical with respect to the secondary index will lead to a run time error 'Insert Duplicate Keys'.

Messages 0031, 0034, 0131, 0134: Table Has More Than 4 (7) Secondary Indexes Though Data Class Is "APPL0"; Table Has More Than 2 (5) Secondary Indexes Though Data Class Is "APPL1"
A database table should no have too many secondary indexes. On the one hand, every additional index increases the cost for data insertion and modification (the latter only, if fields of the secondary index are affected). On the other hand, the cost based optimizer of the database, which calculates the data access strategies, might get puzzled by too many indexes.

Message 0032: Secondary Index sec_index: More Than 4 Fields
More than four fields can be okay for an index, for example if you are planning an index-only access. But as a rule of thumb, four or less fields should be sufficient. The client field is not counted by the check.

Message 0033: Table Has Secondary Index But Is Buffered
The existence of a secondary index for a table signals, that beside buffered accesses also secondary index accesses are planned. It must be ensured that the additional costs of both buffering and the secondary index are justified by corresponding table accesses in production use. The check only reports non-unique secondary indexes, since sometimes unique secondary indexes are defined to preclude duplicates in a table with respect to certain key combinations.

Message 0035: At Least 2 Fields ("dbfld_1" and "dbfld_2") Are Included in 2 Indexes
Indexes should not have too many fields in common with other indexes. For example, it does not make sense to have a table with a (primary or secondary) index A for the fields dbfld_1 dbfld_2 and another index B for the combination dbfld_2 dbfld_1. A different story is an index on fields dbfld_1 dbfld_2 dbfld_3 - here it could be justified to have another index on fields dbfld_3 dbfld_2. This check demands the wise developer to find out what is the appropriate solution!

Message 0036: Index idx_1 contains index idx_2 (left-aligned)
If there is an index dbfld_1 dbfld_2 dbfld_3 it does not make sense to define another index with fields dbfld_1 dbfld_2.

Messages 0037, 0137: Table dbtab: Field fld_name in primary / secondary index idx_name is of the type FLOAT
When a field of type FLOAT is read from the database, rounding differences might occur during the value assignment to an ABAP data object. If such a field is part of the table's primary key, this means that a certain table entry may not be addressed from within ABAP. If a field of type FLOAT is part of a secondary index, reasonable accesses are only possible with range conditions in the WHERE-clause of the database statement for this field.

Messages 0038, 0039: Client-specific table dbtab: Secondary index idx without client field; Client-specific table dbtab: Secondary index idx does not have client as first field
Though the client field is not very selective (and indexes should contain highly selective fields), we vote for putting it at the first position of any secondary table index, since normally the client is known for all table accesses. Secondary indexes that consist of GUIDs (Globally Unique Identifier) may get along without the client field.

Message 0041: INDX-type table dbtab is buffered
So called 'INDX-type' tables have a structure similar to that of SAP system table INDX. Such tables are normally not accessed with a SELECT statement, but with the ABAP statement IMPORT FROM DATABASE. Since this statement does not make use of any buffering, it is not reasonable to buffer an INDX-type table.

Message 0042, 0043: View dbview: First field not client, although basis table dbtab is client-specific; View dbview: Client field of basis tables dbtabs not linked using join condition
Database views based on client-dependent tables should contain the client field in the join condition. Also, the first field of the database view should be the client. Otherwise it is possible to read data cross-client with the database view, which is a security-relevant issue. The performance aspect of this issue is, that without the client field, it is possible that indexes of the tables making up the view cannot be used to optimized the data access.

Messages 0050, 0051: Language-dependent table dbtab: Language is not first field (or second after the client); Language-specific table dbtab is not buffered generically with regard to language
If a table is language-dependent, most accesses to the table will use the language field, since a user only wants to see texts in the log-on language. Therefore it is a good idea, to define the language field as the first field (or as the second field after the client), to allow for index accesses with language field plus key. Since most language-dependent tables will contain configuration data, they should be buffered. To save main memory within the buffers, such a table should not be fully buffered, but generically with respect to the language. The most important accesses, which are via the language, will then be supported by the buffer.

These are a lot of rules, but I hope they will support you to get to consistent settings for your dictionary tables and views.

ABAP offers a great bunch of dynamic programming techniques, ranging from anonymous data objects to dynamic Open SQL statements, program generation, and others.

Since this is a performance blog, I recommend that you use all these different techniques only if you really need them. Many checks that are executed at compile time for static code have to be postponed until runtime and are then executed repeatedly for every execution when you use dynamic programming. As a result, the performance of dynamic program sections is always poorer than that of static ones.

Today I want to discuss a memory problem that can occur if ABAP Runtime Type Creation (RTTC) is used extensively. Sometimes you need to provide code that is capable of working with internal tables that have a line structure which is not known at design time. In such a case it is convenient to create the internal tables dynamically at runtime.

In the following code snippet, I first create a simple structure type my_struct dynamically, and then from this structure type, a table type my_table. This is done using the CREATE methods of ABAP Runtime Type Services (RTTS) classes CL_ABAP_STRUCTDESCR and CL_ABAP_TABLEDESCR. At the end, an internal table is created with the CREATE DATA statement: 

DATA: comp_tab TYPE cl_abap_structdescr=>component_table,
         comp          LIKE LINE OF comp_tab,
         my_struct    TYPE REF TO cl_abap_structdescr,
         my_table     TYPE REF TO cl_abap_tabledescr,
         tab_handle  TYPE REF TO data.

comp-name = 'FIRST_COL'.
comp-type = cl_abap_elemdescr=>get_i( ).
APPEND comp TO comp_tab.

comp-name = 'SECOND_COL'.
comp-type = cl_abap_elemdescr=>get_c( 10 ).
APPEND comp TO comp_tab.

TRY.
    my_struct = cl_abap_structdescr=>create( comp_tab ).
    my_table  = cl_abap_tabledescr=>create( p_line_type = my_struct ).
  CATCH: cx_sy_struct_creation, cx_sy_table_creation.
ENDTRY.

CREATE DATA tab_handle TYPE HANDLE my_table.

Now, in a real program, such generic code might be executed many times, and one often observes that the identical structure type or table type is created over and over again.

To simulate this, I put the table type creation into a DO … ENDDO loop, see code below.

DATA: comp_tab TYPE cl_abap_structdescr=>component_table,
         comp     LIKE LINE OF comp_tab,
         my_struct    TYPE REF TO cl_abap_structdescr,
         my_table     TYPE REF TO cl_abap_tabledescr,
         tab_handle   TYPE REF TO data.

comp-name = 'FIRST_COL'.
comp-type = cl_abap_elemdescr=>get_i( ).
APPEND comp TO comp_tab.

comp-name = 'SECOND_COL'.
comp-type = cl_abap_elemdescr=>get_c( 10 ).
APPEND comp TO comp_tab.
my_struct = cl_abap_structdescr=>create( comp_tab ).

DO 10000 TIMES.

  my_table = cl_abap_tabledescr=>create( p_line_type = my_struct ).

ENDDO.

BREAK-POINT.

When the program reaches the BREAK-POINT statement, I get into the debugger and open the memory analysis tool. In my first compilation of screen shots you find that ≈ 969 kB of 'ABAP Application' memory is allocated by 'Dyn. Memory Objects', most of it (≈ 963 kB) by the internal table ADMIN_TAB of RTTS class CL_ABAP_TYPEDESCR. During the execution of my little program this table grows up to 10,003 entries. What is even more important: the overall allocated memory (line 'Allocated' in area 'Total' below) is around 12 MB!

Memory Consumption with

What happened? While I repeatedly created table types with an identical line structure, always a new entry was entered into the ADMIN_TAB table of the class CL_ABAP_TABLEDESCR. But where does the high amount of 'Total' memory come from? This memory is allocated by the ABAP kernel and most of it corresponds to memory objects that represent the numerous table types I created in my ABAP program. The kernel memory is not under control of the ABAP garbage collector, and often is only released at the end of the user session.   

You can easily imagine, that when a program is making massive use of dynamic type generation, the memory allocated by the internal table ADMIN_TAB and the associated kernel objects can become very large. In fact, at one SAP customer, the ABAP extended memory was blown up to its limits - with the result of short dumps due to memory overflow.

Besides the CREATE method of CL_ABAP_TABLEDESCR, there are other critical methods:
CL_ABAP_TABLEDESCR, methods CREATE and CREATE_WITH_KEYS
CL_ABAP_STRUCTDESCR, method CREATE
CL_ABAP_REFDESCR, methods CREATE and CREATE_BY_NAME

“Is there any hope?”, you might ask now, and yes, there is:
Since release NW 7.02 there also exist corresponding GET methods that make use of an internal cache. These GET methods are therefore much more efficient with respect to memory consumption, as well as with respect to CPU consumption.

I repeated the execution of my program above, but this time the following statement was inside the DO … ENDDO loop:

 my_table = cl_abap_tabledescr=>get( p_line_type = my_struct ).

See the effect on the memory allocation in the second compilation of screen shots below:

Memory Consumption with

As you see, the memory allocated for ‘Dyn. Memory Objects' is only ≈ 7 kB, and the size of the internal table \CLASS=CL_ABAP_TYPEDESCR\DATA=ADMIN_TAB is down to ≈ 600 Bytes for 4 entries. The 'Total Allocated' memory is now only 4 MB - which is the default size for an initial user session. The 'Used' memory has reduced from nearly 10 MB to 1.7 MB.

So while the CREATE methods always create a new type object on every invocation, the GET methods are able to do a reuse for already existing type objects. This only holds true for type objects that were created with a GET method call. Type objects created with CREATE cannot be reused by an invocation of the GET method.

If you call the GET method repeatedly with identical parameters, it is guaranteed that the returned type objects are at least 'move compatible' with each other – that is, a MOVE between data objects created from these type objects will always be possible (this is the kind of compatibility that can also be checked with method APPLIES_TO_DATA of the RTTS classes). A higher degree of compatibility is neither guaranteed by the SAP NetWeaver layer, nor needed by a normal consumer.

So my recommendation for you is: if you really need to create a lot of type objects (structure types, table types, or reference types), and there is a high probability of identical type objects, use the appropriate GET method of the RTTC class instead of CREATE.

In the first two articles of this series we had a look at how to best read data from database tables. The Code Inspector checks whether the database access has a WHERE clause that will be able to make use of a database index, or if an access to a buffered table will implicitly bypass the SAP table buffers.

            Articles of this series:

  1. Low performance operations on internal tables (discussed in this article)

 

Now that you have read data from the database to the application server efficiently, it is essential that you streamline the way in which you deal with the data inside your program.
One big threat to the scalability of a program is large internal tables that are accessed sequentially (see Runtimes of  Reads and Loops on Internal Tables). Sequential access means, that each individual entry of the internal table is accessed by the ABAP runtime in a single step loop - either until a certain key value is found, or until the end of the internal table has been reached. This implies that a sequential access can be fast - but only if the entry searched for is at the beginning of the search area. However, on average, half of the internal table has to be stepped through to find one entry with sequential search.
In contrast, optimized accesses are non-sequential and apply indexes, binary or hashed key searches. So, while the sequential access scales linearly with the amount of processed data, optimized accesses scale logarithmic or even better (i.e. constant access time independent of amount of data).

Moreover: once you start nesting internal tables in your program, for example a statement READ TABLE itab2 inside a LOOP AT itab1 ... ENDLOOP, you can get caught out by quadratic run time behavior. This will happen if the size of the two internal tables depends linearly on the amount of data processed in your program, and if the access to the inner table is not optimized.

This article presents a Code Inspector check that detects sequential (or 'non-optimized') accesses to internal tables.

Types of internal tables and accesses

In ABAP, internal tables can be specified according to the following table categories:

  • STANDARD table
  • SORTED table
  • HASHED table

Additionally there are the generic table categories ANY TABLE and INDEX TABLE.

SORTED and HASHED tables have a table key that speeds up the table access if the key is fully specified. In the case of the SORTED table, the access can also be optimized if just the leading part of the key is specified in the READ or LOOP statement.
Fast accesses to STANDARD and SORTED tables are possible by using the table index, that is, the position of an entry in the table.
A fast access to a STANDARD table can also be achieved by using the option BINARY SEARCH in the READ accesses. To return correct results, the table must be sorted appropriately.

Sequential, 'non-optimized' accesses

The following accesses to internal tables may result in a sequential search:

  • READ TABLE itab
    • READ TABLE itab WITH KEY
      For a STANDARD table, this leads to a sequential access if the option ‘BINARY SEARCH' is missing.
      For HASHED tables you get a sequential access if the table key is not fully specified, and for SORTED tables if the leading part of the key is not specified.
    • The two variants READ TABLE itab WITH TABLE KEY and READ TABLE itab FROM wa result in sequential accesses for STANDARD tables.
    • The variant READ TABLE itab INDEX idx is always optimized (and only possible for the index table types STANDARD and SORTED).
  • LOOP AT itab WHERE ...
    This always leads to a sequential access for STANDARD tables.
    For HASHED and SORTED tables to be able to make use of the table keys, the WHERE clause must only contain ‘AND' conditions, and all fields must be specified with ‘EQ' or ‘='. Again, a SORTED table can make use of the leading part of the key, while for a key access to a HASHED table all key fields must be provided.
  • MODIFY and DELETE
    Both statements have three basic variants:
    • MODIFY/DELETE TABLE itab FROM wa
      This is always a sequential access for STANDARD tables.
    • MODIFY/DELETE itab ... WHERE ...
      This variant behaves like the LOOP AT itab WHERE ... (see above)
    • MODIFY/DELETE itab INDEX idx ...
      This access type is always optimized (and only possible for the index table types STANDARD and SORTED)
  • INSERT
    The statement INSERT wa INTO itab INDEX idx scales linearly for STANDARD tables up to a certain size, because a linear index has to be maintained. At some threshold value a B*-tree is created which leads to logarithmic scaling behavior for larger tables.

Details of the Code Inspector check

The check ‘Low Performance Operations on Internal Tables' has four attributes corresponding to the different table types that can be checked: STANDARD, SORTED, HASHED, and GENERICALLY typed tables.
If you define an IMPORTING parameter for a method as type ‘ANY TABLE' (that is, generically typed), the check tool will not know which non-generic table type this will be at runtime. It thus assumes that it will be a STANDARD table and accesses to the table are checked accordingly.

Note that in the global check variant ‘DEFAULT' of the Code Inspector the option to check STANDARD tables is normally de-selected.

The check itself is based on the ABAP compiler (class CL_ABAP_COMPILER) and its services. The compiler provides information like the type of an internal table that cannot be extracted easily by doing a simple source code scan. Here, the information as to whether the access to an internal table can be optimized or not is provided directly to the check framework.

The check detects non-optimized accesses to internal tables with the following statements (see above):

  • READ TABLE itab
  • LOOP AT itab WHERE ...
    LOOP without WHERE clause does not raise a message
  • MODIFY / DELETE

In the ‘Remarks' section below you will find some additional internal table statements that may be slow, but that will not give you a message with this check.

The possible Code Inspector messages for this check are:

 Message Default priority
 Sequential Read Access for a Standard Table Information
 Possible Sequential Read Access for a Sorted Table Warning
 Possible Sequential Read Access for a Hashed Table Warning
 Possible Sequential Read Access for a Generically Typed Table Warning
 Possible sequential access during deletion from a table Warning


The last message is only relevant with Release SAP NetWeaver 7.1 and higher. It comes into play when secondary table keys are defined for an internal table. Here, the deletion of entries in a STANDARD table via a SORTED or HASHED secondary key can lead to linear runtime behavior.

How to proceed with a Code Inspector message

The static check tool has no clue about how many entries an internal table will have at runtime. Therefore, not all check messages will have the same relevance for the performance of the program execution.
Now, also you as the developer are unlikely to know exactly how many entries an internal table will have in a productively used customer system. To make things easy we propose that you only distinguish between ‘small' internal tables with a maximum of 20-30 entries on the one side, and ‘large' tables on the other side. All frequently executed accesses to large internal tables should be optimized, that is, use an index, or use a table key, or do a BINARY SEARCH. In many cases, a SORTED table will do the job (* see comment at end).

This is how you should advance in detail if there is a sequential access to an internal table:

  • As with all Code Inspector messages, first check if the code will be productively used at all. If it's only a test report there may be no need for optimization.
  • If the internal table that is accessed sequentially will always be small (up to 20-30 entries) at runtime, it is normally not necessary to provide an optimized access - so there is no need to define such a small table as a SORTED or HASHED table. This is also why a sequential read access to a STANDARD table only raises an ‘information' message in the check tool.
  • If it's an access to a STANDARD table, and the table might become large at runtime:
    • Think about converting the table type to a SORTED table. Only if your data set is very large and has the character of a mapping table (unique keys), a HASHED table can also be appropriate.
    • A READ TABLE on a STANDARD table can be made faster with the option BINARY SEARCH. Also fast nested LOOP processing is possible with sorted STANDARD tables and BINARY SEARCH (see Runtimes of  Reads and Loops on Internal Tables). But the table must be sorted and/or kept sorted in the appropriate order. Note that a SORT is an expensive operation.
    • In newer releases of SAP NetWeaver, a secondary table key can be created. Keep in mind that this additional key will contribute to resource consumption and can only be justified by a corresponding number of read accesses.
  • If it's an access to a SORTED or HASHED table (that is, a table that might become large at runtime):
    • If the WHERE clause in a LOOP statement cannot make use of the table key, because it contains OR conditions (disjunctions) or comparison operators other than ‘EQ' / ‘=', think about re-defining the fields in the WHERE clause.
    • If the table key cannot be used for a HASHED table because not all fields were available, think about switching to a SORTED table. These can make use of left-justified parts of the table key to optimize the access.
    • If there are fields missing so that the full (or left-justified part of the) table key cannot be provided, think about the fields in the table key and their order. Maybe another order or other fields would better support your access. Please be aware that changing the fields in the key of a SORTED or HASHED table to improve one access can have negative impacts on many other accesses - and even lead to syntax errors. You need a ‘holistic' approach that takes all accesses to the internal table into account.
    • In newer releases of SAP NetWeaver, a secondary table key can be created. Keep in mind that this additional key will contribute to resource consumption and can only be justified by a corresponding number of read accesses.

Remarks

There are further potentially slow operations on internal tables that are not reported by the check, but that can be observed in performance measurements:

  • LOOP AT ... without WHERE clause
    If you do not apply a WHERE clause, it is expected that all table entries must be read. This may be slow, of course, for large tables. And sometimes there are CHECK statements or the like inside the LOOP ... ENDLOOP which show that in fact not all entries were needed for the processing.
  • LOOP AT ... with dynamic WHERE clause.
    For dynamic accesses, the check tool cannot decide whether they will be optimized at runtime or not. Therefore no message is issued.
  • READ TABLE with dynamic key components
    READ TABLE itab INTO wa WITH [TABLE] KEY (comp1) = ... (comp2) = ...
    The check tool will issue a message for the WITH KEY variant of this statement, though it may be optimized at runtime.
    On the other hand, the WITH TABLE KEY variant will not lead to a message for SORTED and HASHED tables. If, at runtime, the dynamically defined key differs from the table key, you will get a short dump.
  • SORT itab
    Sorting STANDARD tables (for example to provide the correct sort order for the READ TABLE ... BINARY SEARCH) scales worse than linear with the table size, so it is a rather expensive operation. To justify the cost of one SORT operation, at least 30 READ TABLE ... BINARY SEARCH have to follow.
  • Table index build up
    If a STANDARD table is filled with INSERT wa INTO itab INDEX idx, internally a new type of index will be build up when the number of entries reaches a certain threshold.
    Also, the new secondary table keys are - if they are non-unique - only build up on their first use.
    This ‘lazy' or ‘on-demand' behavior can lead to unexpected runtime delays.
  • Sometimes one finds the statements MODIFY/DELETE itab FROM wa inside a LOOP ... ENDLOOP over the same table. If itab is a SORTED or HASHED table, the MODIFY/DELETE gives no check message, since the access can be optimized; for a STANDARD table it's just an ‘information' message. But the MODIFY/DELETE statements start a new internal search for the entry given by the work area wa, though often it's already known - it is the current entry processed in the LOOP.
    Therefore, for index table types (STANDARD and SORTED tables), the statement MODIFY/DELETE itab INDEX idx ... should be used inside a LOOP, where idx is the current LOOP index. The addition INDEX idx can be omitted, since it is added implicitly in a LOOP.

(*) We do not discuss here cases were you need alternative access paths to large internal tables. The situation for such a scenario will improve in future with the new secondary table keys, but is rather complex for older releases.

This is the second article in a series dealing with the performance checks that are delivered with the Code Inspector, SAP’s tool for the analysis of static ABAP code and other repository objects. In the previous article we saw that every frequently executed database access should be supported by an appropriate database index. If the WHERE clause of a database access fails to hit an index, the Code Inspector will protest.

    Articles of this series:

  1. Code Inspector’s Performance Checks (I)
  2. SELECT statements that bypass the table buffer (discussed in this article)
  3. Code Inspector’s Performance Checks (III)
  4. Code Inspector’s Performance Checks (IIII)

Data that is frequently accessed and rarely modified should be read from a table buffered on the application server, and not from the database. It is about 10-30 times faster to read one entry from a buffered table than to read it from the database cache – say it takes only 20µs instead of 400µs. Again, things can go wrong when buffered tables are accessed inadequately; that is, the buffer may be bypassed and the performance advantage lost. But do not worry – there is another Code Inspector check at hand to help you. This article explains when it is best to buffer tables. It also describes the appropriate Code Inspector check to detect statements that bypass the buffer and how to correct the underlying problems.

When and how to buffer tables

Small or medium sized database table with data that is frequently read, but only rarely modified, should be buffered within SAP’s table buffer. In fact, there are two types of table buffer: one for single record buffered tables, and one for generically and fully buffered tables, but for simplicity let's refer to 'the (SAP) table buffer'. The best candidates for buffering are tables containing business configuration data; for example, a table containing the address data of a company's subsidaries. Since the address of a subsidary will not change frequently, and since there will be at maximum some hundred to thousand data sets, the table should be buffered. Whether or not a table is buffered is defined in its technical settings (transaction SE11). You can choose between the following buffering types:

single record buffering   to buffer single records, identified by the full primary key

generic buffering             to buffer generic key areas with 1, 2, ..., n key fields, where n is shorter than the primary key length

full buffering                      to buffer the whole table; for client dependend tables, full buffering corresponds to generic buffering with respect to the client field

If data is modified on application server A it can take up to two minutes for the buffer entries to be invalidated on another application server B of the same system. During this interval, a user on application server B may read outdated information. For some critical applications this behavior can lead to data inconsistencies. Therefore, for these applications buffering must be avoided. When a buffer range is invalidated by a modification on server A, the data is not reloaded from the database into the buffer of server B with the first read access. Instead, it takes up to ten read accesses until the database interface is 'confident' enough to re-fill the buffer with the data from the database. This 'pending' behavior of the buffer, plus the communication overhead created by the invalidation mechanism, is the reason why frequently modified tables should not be buffered.

SELECT statements that bypass the table buffer

For the table buffer to be quick in responding to a request, it works in a simple and straightforward fashion. The SAP table buffer is not aware of sophisticated things such as secondary indexes (it only knows the primary one), SQL Joins, aggregate functions, or complicated selection ranges. This is why there is a list of OpenSQL options that lead to an implicit bypassing of the buffer, because the buffer cannot handle them. Therefore, avoid the following options if you want to access data from buffered tables:
List of statements that bypass the SAP table buffer (as of July 2007):

Implicit bypassing caused by an option of the SELECT statement
  1. SELECT from a single record buffered table without explicit use of ‘SELECT SINGLE’ (starting with SAP NetWeaver 7.10, the key word 'SINGLE' is no longer required to make use of the single record buffer)
  2. SELECT from a buffered table in an Open SQL Join or use of a buffered table as a joined table in a database view defined in the data dictionary
  3. SELECT with a subquery
  4. SELECT with an aggregate function: COUNT(), MIN(), MAX(), SUM(), AVG()
  5. SELECT DISTINCT ...
  6. SELECT ... GROUP BY ... [HAVING cond ...]
  7. SELECT ... ORDER BY ... where the sort order differs from the primary key
  8. SELECT with option ‘CLIENT SPECIFIED‘, but no client field in the WHERE clause
  9. SELECT ... WHERE a IS [NOT] NULL
Implicit bypassing caused by an incomplete WHERE clause or inappropriate buffer setting
  1. Generic key not fully specified in the WHERE clause of a SELECT from a generically buffered table
  2. Primary key not fully specified in the WHERE clause of a SELECT from a single record buffered table
  3.  
Intended Bypassing
  1. Use the option ‘BYPASSING BUFFER’ to avoid accessing outdated table buffer information, and to read data from the database instead. This will only be relevant for some special applications.
  2. The statement ‘SELECT FOR UPDATE’ sets a database lock and therefore always has to bypass the table buffer.

Native SQL statements also bypass the SAP table buffer. Moreover, they do not trigger the buffer invalidation mechanism after a database change, and therefore should not be used in application programming. Using any statement of the list above with a buffered table causes the SAP table buffer to be bypassed, thereby forgoing the performance advantage of the buffer access over a database access.

Details of the Code Inspector check

For every SELECT statement, the Code Inspector reads the technical properties of the table accessed (including buffer settings) from the data dictionary. For this check, only SELECT statements accessing buffered tables will be further analyzed by the Code Inspector:

  • The Code Inspector analyzes whether one of the options mentioned in the above list is used in the SELECT statement
  • For single record buffered and generically buffered tables, the WHERE clause is also analyzed. Only if the buffered key range is fully specified in the WHERE clause and solely contains 'AND' relations (conjunctions), and if the fields in the WHERE clause are compared against the current parameters with a simple ‘EQ’ or ‘=’ condition, will the buffer be used.
  • Range conditions (‘WHERE field  IN range ’) can only make use of the buffer if the range contains one single ‘EQ’ or '=' condition. Since the Code Inspector does not know the content of a range at runtime, such range conditions are always reported.
  • If the SELECT comes with a 'CLIENT SPECIFIED' option, Code Inspector checks whether the WHERE clause contains the client field. Since client dependend tables are always buffered generically with respect to the client field (even if they are formally fully buffered), the buffer will be bypassed without an implicitly or explicitly specified client.
  • If there is an ORDER BY field1 field2 ... option, Code Inspector analyzes whether the fields determining the sort order correspond to the (leading part of the) primary key sequence. Of course, the 'ORDER BY PRIMARY KEY' option also uses the buffer.

For every statement identified to bypass the table buffer implicitly, the Code Inspector raises a warning message. The use of Native SQL is reported by the 'Critical Statements' check of the Code Inspector.

How to proceed with a Code Inspector message

Now, what should you do when the Code Inspector informs you that a SELECT statement on a buffered table bypasses the SAP table buffer?
First of all: Do not - without further analysis - change the buffer settings of the table, for example, buffering it more ‘generously’, with a more generic key range!
Consider that changes of buffer settings influence other statements and that the size of the table buffer is limited. Tables with wrong buffer settings may displace many other tables from the buffer, because they consume too much memory. Instead, investigate the following:

  • Sometimes developers misuse the implicit bypassing of the buffer caused by a statement of the above list as a feature, because they want to bypass the buffer for some reason. Never do this! The list of statements that bypass the table buffer can change. If bypassing is wanted, state this explicitly by using the “BYPASSING BUFFER” option.
  • If bypassing is not wanted, and the statement bypassing the buffer is frequently used in a production system, try to rewrite the SELECT statement so that the buffer can be used, as described below:
When implicit bypassing is caused by an option of the SELECT statement

  1. Insert the missing key word 'SINGLE' if you access a single record buffered table. If the primary key is fully specified in the WHERE clause, the access will be able to use the buffer.
  2. JOINs are very elegant and powerful SQL statements, and for tables that are not buffered it is clearly better to read data from the database with SELECT … JOIN … INTO TABLE than using nested SELECT ... ENDSELECT statements. But this is no longer true if one or more of the joined tables are buffered! Since the table buffer cannot handle OpenSQL Joins or subqueries, replace these constructs by explicit SELECT statements on each table (if they can make use of the table buffer). For example use subsequent SELECT statements, the FOR ALL ENTRIES option, or SELECT ... ENDSELECT loops.
  3. For SELECT statements with a subquery the recommendations for JOINs (see above) also apply.
  4. Replace the aggregate functions COUNT(), MIN(), MAX(), SUM(), AVG() with ABAP code. For a fully or generically buffered table it is better to read the data into an internal table and to perform the counting, summing or averaging in ABAP. A special case is the COUNT( * ) option which is often misused to check whether there is at least one entry (for a special key range) in a table. To perform an existence check in this way is a bad idea because it bypasses the table buffer and  hurts the database! An existence check with SELECT COUNT(*) FROM dbtab … UP TO 1 ROWS can be harmful because some database platforms really count all entries (the UP TO 1 ROWS option just tells them to return only one value, which is the count result), while you might think that it stops after one entry has been found. You better do a SELECT first_keyfield FROM dbtab … UP TO 1 ROWS instead. This also increases the probability that the statement will use the table buffer.
  5. Replace the DISTINCT option with functions of the ABAP LOOP processing.
  6. Rebuild the SQL option GROUP BY ... [HAVING cond] with functions of the ABAP LOOP processing.
  7. Replace ORDER BY (if the sort order differs from the primary key) by a SORT operation in ABAP.
  8. The option CLIENT SPECIFIED should not be used in application code. If it is needed, though, it can be beneficial to first read all existing clients into an internal table. It may then be possible to use the buffer by specifying the clients in the WHERE clause using the FOR ALL ENTRIES option.
  9. Since, in contrast to databases, neither the ABAP language nor the table buffer know NULL values, the condition WHERE a IS [NOT] NULL cannot be processed in ABAP - so bypassing the buffer is unevitable.
When implicit bypassing is caused by an incomplete WHERE clause or inappropriate buffer setting
    If you find that the buffer is bypassed because the single record key or the generic key range was not fully specified in the WHERE clause, do the following:
     
  1. Try to complete the WHERE clause with the missing key field. Sometimes the missing information can be read easily (and quickly) from another buffered table. If it is not possible to complete the WHERE clause, have a look at all SELECT statements accessing the buffered table. Check if the buffer is used in most of the (performance critical) cases, or if there are accesses that could benefit from a more ‘generous’ buffering. On the other hand, your analysis may also reveal that buffering the table is not sensible at all. Only change the buffer settings after such an analysis.
  2. The recommendations from list item 1 also apply.

Transaction ST10 (Table Call Statistics) shows the current status of a table in the table buffer. It is always a good idea to check the buffer status in a test or production system to see whether the buffer settings make sense. There should not be too many (ideally: no) buffer invalidations for a table, and it should not allocate too much memory in the buffer. You may also be able to identify further candidates for buffering with the help of this transaction. Now you have learned how the Code Inspector check "SELECT statements that bypass the table buffer" helps to improve program performance. Please remember that some Open SQL constructs like Joins can be very elegant - but that they should not be applied to buffered tables in performance critical code. Otherwise, you will compromise the efficiency of your programs. For general aspects of program efficiency see also my blog Cooking and software efficiency.

Do you cook? A nasty saying about developers is that they live mainly on fast food like take-out pizzas, take-out chop suey, or hot-dogs. Some people may argue that this is why software applications often lack efficiency. In this weblog I want to encourage developers to apply the rules of cooking to software, because cooking can tell us a lot about efficiency. So let’s poke our noses into the cook’s pot:

Starter: the tidy kitchen
One ‘must have’ for running an efficient kitchen is order. Have you ever tried to cook in someone else's kitchen? At first, you will waste a lot of time opening the wrong drawers and searching for the things you need: the knives, the strainer, the can opener (for cheaters only), and so on. After a while, you will be better oriented and learn where to find what you need. Unconsciously you have built up a ‘kitchen search index’ that helps you to find things faster. The clever cook will also keep his spices in a well-defined order on the shelf, for example in alphabetical order or according to their taste.

This is very similar to how you should access data in a program: every frequently executed database access, for example, should be supported by an appropriate database index. Otherwise, the database has to open too many ‘drawers’ when searching for the needed data. The same holds true for accesses to internal tables in ABAP. Do not access large internal tables sequentially, for example with a READ, w/o using an index or table key. If you do so, you act like a cook who scans dozens of identical looking spice flasks until he finds the one with the nutmeg. Instead, use sorted standard tables or internal tables with sorted or hashed key that allow a fast access.

Portions, packages, and bundles
A second thing that we can learn from cooking is portioning. If you want to cook a meal for you and your best friend, would you go to the cellar and shlep a 50kg sack of potatoes up the stairs? No - the efficient cook just fetches as much food as is needed. And if a fruit salad is on the menu, don’t get the whole fruit basket from the living room or the pantry. On the other hand, neither should you go once to get an apple, then again to fetch a pear, and once again for the banana. You are the cook, you know the recipe for the fruit salad – so just go once and get all the fruits you need, no more, no less.

Again, this gives us a hint of  how to read data from the database: not more than we (that is our application) need to do the current job. But if we know for sure that several data records from the same database table will be needed, an array fetch is better than many single fetches.  Portioning or bundling is also important for the communication between remote systems or between distinct system components, for example between a service provider and the consumer. In the same way as the cook does not want to overload himself with heavy sacks, or make superfluous walks to fetch single fruits, it is reasonable to bundle the data that is exchanged between remote systems into packages of adequate size. Packages that are too small lead to many round trips, thus increasing the overall response time. And very large packages can overload the system components and result in time outs.

Store, cache, and buffer
Speaking about superfluous round trips we return to the pantry, which saves the cook from going to the grocery store every day for food that is frequently needed like rice, sugar, or noodles (which kind of food is frequently needed in a kitchen of course heavily depends on the cook’s taste buds). So the pantry works as a kind of local store or buffer that minimizes the number of very slow round trips in favor of faster ones. Some ingredients like salt or certain spices are so frequently used in cooking, that no cook could afford even to fetch them from the pantry or from a drawer – they must be right beside the kitchen stove, always within reach.

In the world of software, buffering or caching also play an important role. Data that is frequently requested but only rarely modified (for example business configuration data) should not be read over and over again from the database. Identical information should not be extracted repeatedly from service methods that need to descend numerous levels in the program's call hierarchy to retrieve it (or, even worse: to deliver nothing). Instead, such data or information must be buffered on the application server (in the case of database tables within the SAP table buffer), or locally in the program closest to the information consumer, that is, within reach. Not caching such data or bypassing the cache is like ignoring the salt shaker next to the kitchen stove and walking down to the cellar every other minute to get a pinch of salt.

Main Course: Concurrency and Scalability
Two other important aspects of efficient cooking and programming are scalability and the concurrent use of resources. Cooks are masters in doing things in parallel. While the water is heating in the pot, and the onions are sizzling in the pan, the cook can chop the carrots (and don’t forget the soufflé in the oven!). Good kitchens are designed for parallel work: the kitchen stove has 2 or more hot plates or burners, and you will find more than one pot and more than one sharp knife. If you expect several guests, you have two choices to feed them all: first, if you still work alone, you will have to invest more time, because you cannot do everything in parallel (ever tried to chop vegetables with your right and your left hand at the same time?). Or you can hire some helping hands to prepare the meal faster. If there are no bottlenecks in your kitchen like lack of knives, pots, or burners, in principle a recipe can be scaled-up from two persons to several dozens of eaters, while leaving the preparation time nearly constant (some people say that meals coming from a canteen never reach the quality of those prepared for only a few persons. But this weblog is about cooking efficiency, not about meal quality).

What can a developer learn from cooking with regard to scalability and concurrent use of resources? If your recipe – pardon, your program – scales (linearly or better) with the amount of data to be processed, it will serve hundreds of users as well as a single one just by adding burners (CPUs, memory, disk space on multiple application servers). This only holds true, as long as there are no bottlenecks like database locks or other single resources that get overloaded and lead to a serialization of users or processes. If you have only one sharp knife, you can add as many burners, pots and helpers as you want to – they will all be idle because the onions cannot be chopped fast enough. Another threat to scalability – besides locks and resource bottlenecks – is non-linear runtime behavior. In software engineering that means an algorithm efficiency with a worse than linear (or linear times logarithmic) runtime behavior as a function of the processed data volume. In business applications an algorithm with quadratic or even cubic runtime behavior is rarely requisite, but non-linear code can often be found in deficient application coding. A prominent example are internal tables that grow with the amount of the processed data and that are accessed sequentially and in a nested way (see for example in Performance Problems Caused by Nonlinear Coding).  I never came across a quadratic effect in cooking. An example would be a big vegetable that has twice the weight of a small one of the same kind, but that needs to boil four times as long (and that cannot be cut into pieces for some reason). Have you ever encountered such a non-linear vegetable effect? Maybe the vegetables I tested were just too small. This is by all means the reason why application developers often ignore non-linear effects in their code: the amount of data they use for testing is too small. Or they only perform a single measurement from which nothing can be concluded with respect to scalability. If your application scales, you can keep the response times for concurrent users acceptable just by adding resources. In the same way you will be able to process a large set of independent objects in parallel tasks, thus reducing the overall processing time tremendously. But remember: precondition for this is the absence of bottlenecks and a linear or better runtime dependence on the amount of processed data.

I hope you found this excursion into cooking and software efficiency flavorful. The next time you implement a piece of program code, try to look at it with the eyes of a cook: Will your guests like its taste? And will it also be efficient?

Code Inspector is SAP’s tool for the analysis of static ABAP code, data dictionary objects and other repository objects. The tool (transaction SCI) has a set of predefined performance checks that can help you to improve your code so as to optimize the performance of your program. In this weblog series I want to present some of these checks, their theoretical background, and what you can do to remedy the problem when you get a message from the Code Inspector (don’t panic!). When working with SCI you should always be aware that a simple, static check tool cannot cure all the performance or scalability problems your applications may have. These problems often only become visible at runtime – especially when the code becomes more complex. This can be, for example, identical database accesses within one dialog step, or frequently called routines that, after having gone down several call stack levels, realize that there is nothing to do, or that do identical executions over and over again. Such problems cannot be detected by a check tool that works on the static definition of repository objects. But such a tool can help you to realize some quick wins, by means of detecting shortcomings in the implementation. So, running the Code Inspector is just one step on the way to better code, but only a fool would do without it. The performance checks I want to discuss in this weblog series in some more detail are:

     Articles of this series:

  1. Analysis of the WHERE clause for SELECT, UPDATE and DELETE (discussed in this article)
  2. Code Inspector’s Performance Checks (II)
  3. Code Inspector’s Performance Checks (III)
  4. Code Inspector’s Performance Checks (IIII)

Analysis of the WHERE clause for SELECT, UPDATE and DELETE

An inefficient database access can significantly slow down the whole application. As a rule, all frequently executed database accesses should be supported by an appropriate database index. Even if there is an appropriate index, database accesses can fail to use this index at runtime, for example, because the database table statistics are not up-to-date. But these are exceptions and they are outside the scope of a static check tool such as the Code Inspector. Code Inspector only analyzes the fields of the WHERE clause and compares them to the indexes defined in the data dictionary. This, of course, only works as long as neither the database name nor the WHERE clause is defined in the code dynamically. Note: Remember that business configuration tables, which are normally small, often read but rarely modified, should be buffered within SAP’s table buffer on the application server. How to access such tables without loosing the advantage of buffering will be described in a subsequent weblog (SELECT statements that bypass the table buffer).

Check details

This is what the check does in detail:

  1. A first check determines whether there is a WHERE clause at all. A SELECT without a WHERE clause does not restrict the result set (or only restricts the result set to all entries in one client for a client dependent table) – which is a real no-no with respect to performance.
  2. Then, for all normalized sub-clauses of a WHERE clause the Code Inspector checks whether they contain a field that is also the (first) field in a database table index. Normally, databases have difficulties to fill gaps in an index, especially if the first index field is concerned. Thus, if the database index contains the fields A, B, C, it does not help to have a WHERE clause with only the condition "B = ‘SHERLOCK’ AND C = ‘DETECTIVE’". Since the first field A is missing, the database will do a full table (or full index) scan. Only if field A has just a few values, some database platforms might be able to fill the gap – but you should not rely on this. Another thing that the Code Inspector checks is whether the (index) fields in the WHERE clause are compared against the current parameters with 'positive' conditions like ‘EQ’, ‘>=’, or ‘BT’. This check is necessary because, to the database, it is of no help to have a WHERE clause of the Type "A NE ‘HOLMES’" (NE stands for ‘not equal’) or "A IS NOT NULL", or the like. Such negations always lead to a full scan.
  3. Is the accessed database table large or small? Clearly, a full table scan is more harmful when it is done on a large table with thousands of entries than for a small table. The answer to this question influences the prioritization of the check messages by the Code Inspector. To determine the table size, the Code Inspector examines the value for the ‘size category’ in the technical settings of the accessed table in the data dictionary. Accessing large tables (size category >=2) is seen more critical than accessing small ones. Counting the real number of database entries in the table would be far too slow, and so the Code Inspector does without.

How to proceed with a Code Inspector message

Now what should you do when the Code Inspector tells you that a WHERE clause cannot use any of the existing indexes of a database table (or that there is no WHERE clause at all)?
First of all: Do not create new indexes for statements which are rarely used!
Instead investigate the following:

  • If the statement, routine, or program with the inefficient database access is not used or needed anymore: => Delete it! You would not believe how many unused scraps of source code accumulate in the repository over the years. So use the opportunity provided by these messages to get rid of them
  • If the statement, routine, or program is rarely used (for example, because it is a test program, or a tool that is only used in exceptional cases) => Mark the statement with a pseudo-comment. As you may know, the Code Inspector allows you to declare exceptions to its rules in the form of (pseudo-)comments in the code. You will find the appropriate comment in the documentation on the checks. Using the pseudo-comment you indicate that you have noticed the Code Inspector message, but think that it is not relevant in this case. But under no circumstances set pseudo-comments ‘automatically’ without first using your common sense!
  • If the statement is frequently executed => Try to re-write the WHERE clause so that an existing index can be used. Maybe you can read a missing index field ‘cheaply’ from a buffered table to fill the gap in the WHERE clause. Or maybe you even have the required information at hand, but forgot to add it to the WHERE clause. Always formulate the WHERE clause as complete as possible. Do not read entries from the database just to throw them away shortly afterwards (as it is sometimes done in SELECT … ENDSELECT loops with a CHECK statement inside). Most check conditions on fields of the accessed database table can be incorporated into the WHERE clause. Side remark: try to formulate the fields in the WHERE clause always in the same order (preferably according to the primary key order). This will save space in the databases statement cache.
  • If the statement is frequently executed and cannot be rewritten => Adjust one of the existing indexes However, be careful, changes on an index can affect other statements! => Alternatively, create a new index Be careful here, too, as every additional index stresses the database. The reason for this is that every modification of the indexed database table that touches one or more of the index fields also leads to a modification of the corresponding indexes. Now, if you think of an index as being a small table in its own right, you can imagine that an update on a table with many indexes can become as ‘heavy’ as several updates. Another reason why a multitude of indexes can be harmful is that the database optimizer can be puzzled by too many choices and make less-than-optimal decisions. Data and index design itself is an interesting topic that could fill further weblogs …

This first part of my weblog series about Code Inspector’s performance checks showed you how the analysis of database accesses is done by the tool and what possibilities you have to resolve the messages that are returned. In the next weblog we will discuss the access to buffered tables.

Filter Blog

By author: By date:
By tag: