Chapter 17. Optimizing Your Application

<feature><title>In This Chapter</title> </feature>

Why This Chapter Is Important

In a world where it is often difficult for hardware to keep up with software, it is important to do everything you can to improve the performance of your application. This chapter helps you optimize your application’s speed and reduce its memory and hard disk space requirements.

Introducing Optimization

Optimization is the process of reviewing your operating environment, VBA code, application objects, and data structures to ensure that they are providing optimum performance. In a nutshell, optimization is the process of making your application leaner and meaner.

Users become frustrated when an application runs slowly. In fact, if a user is not warned about a slow process, he often will reboot or shut down the power on the machine while a process is running. This can have dire results on the integrity of the data.

Tip

To help reduce the chance of users rebooting the computer during a lengthy process, it generally is a good idea to provide them with some sort of indication that a process will take a while. You can do this by using a message box that appears before processing begins, or by providing a status bar or progress meter that shows the progress of the task being completed.

You can do many things to optimize an application’s performance, ranging from using a front-end tool such as the Performance Analyzer to fastidiously adhering to certain coding techniques. This chapter highlights the major things you can do to optimize the performance of your applications.

Modifying Hardware and Software Configurations

The Access environment refers to the combination of hardware and software configurations under which Microsoft Access runs. These environmental settings can greatly affect the performance of an Access application.

The easiest way to improve an application’s performance is to upgrade its hardware and software configuration. This form of optimization requires no direct intervention from the developer. Plus, a side benefit of most of the environmental changes you can make is that any improvements made to the environment are beneficial to users in all their Windows applications.

Improving the environment involves more than just adding some RAM. It also can mean optimally configuring the operating system and the Access application.

Hardware, Hardware, More Hardware, Please!

The bottom line is that Windows 95, Windows 98, Windows NT, and Access 2002 all crave hardware—the more, the better. The faster your users’ machines are, and the more memory they have, the better your applications will run. Obtaining additional hardware might not be the least expensive solution, but it certainly is the quickest and easiest thing you can do to improve the performance of your application.

RAM, RAM—That’s All I Need!

Memory is what Access craves most, whether you are running under the full version of Microsoft Access or using the runtime version of the product. Microsoft Access requires 32MB of RAM just to run under Windows 98 and 64MB of RAM to run under Windows 2000. These minimums are considered Access’ standard operating environments. Although 32MB of RAM is required, the recommended RAM is even higher. Both requirements for Windows 98 and Windows 2000 can climb dramatically if your user is running other applications or if your application uses OLE automation to communicate with other applications. Put in a very straightforward way, the more RAM you and the users of your application have, the better. RAM measuring 128MB is a great environment for Access 2002. In fact, if every one of your users has at least 128MB of RAM, you can stop reading this chapter because everything else covered here is going to provide you with minor benefits compared to adding more RAM. If you are like most of us, though, meaning that not every one of your users has a Pentium III 666Mhz with 512MB of RAM, read on.

Note

Developers should have a bare minimum of 128MB of RAM installed on their machines. Remember that this is a minimum! Most developers agree that 256MB of RAM or more is ideal if you intend to do any serious development work, especially if you plan to develop client/server or Internet/intranet applications.

Defragment Your User’s Hard Disk

As your computer writes information to disk, it attempts to find contiguous space on which to place data files. As the hard disk fills up, files are placed in fragmented pieces on the hard disk. Each time your application attempts to read data and programs, it must locate the information scattered over the disk. This is a very time-consuming process. It therefore is helpful to defragment the hard disk on which the application and data tables are stored, using a utility such as the Disk Defragmenter that ships with Windows 95, 98, and Windows 2000.

Tip

The process of defragmenting a hard disk easily can be automated by using the System Agent, which is included as part of the Microsoft Plus! Pack. This package is sold as an add-on to Windows 95. The System Agent is a useful tool that enables you to schedule when and how often the defragmentation process occurs. Windows 98 ships with the Maintenance Wizard, which is used to schedule various system tasks to run automatically, such as the Disk Defragmenter and the ScanDisk utility. Windows 2000 ships with a very basic defragmenter utility. If you are using Windows NT or Windows 2000, numerous third-party tools can assist you with the process of automating the defragmentation process.

Compact Your Database

Just as the operating system fragments your files over time, Access itself introduces its own form of fragmentation. Each time you add and modify data, your database grows. The problem is that, when you delete data or objects within your database, it does not shrink. Instead, Access leaves empty pages available in which new data will be placed. The problem is that these empty pages are not necessarily filled with data. The empty space can be freed using the Compact utility, included in the Microsoft Access software. The compact utility frees this excess space and attempts to make all data pages contiguous. You should compact your database frequently, especially if records or database objects (for example, forms and reports) are regularly added and deleted. The Compact utility can be accessed only when no database is open. Choose Tools|Database Utilities. Then choose the Compact and Repair Database option.

Note

It is worth noting that, if you plan to distribute an Access application to other users, possibly via the runtime module, it is a good idea to include some means of compacting the database. The runtime module does not allow access to the Compact menu item. The CompactDatabase method enables you to compact a database from within an Access database, but you cannot call this command from within the current application. A second application must be created to use the CompactDatabase method on the original application.

Don’t Use Compressed Drives

Regardless of the compression utility you are using, disk compression will significantly degrade performance with Access 2002.

Tune Virtual Memory: Tweak the Swap File

Although Windows 95, Windows 98, Windows NT, and Windows 2000 attempt to manage virtual memory on their own, you might find it useful to provide them with some additional advice. To modify the physical location of the swap file under Windows 95 and Windows 98, right-click My Computer. Choose Properties and then select the Performance tab. Click the Virtual Memory button. Under Windows 2000, right-click My Computer, choose Properties, and click the Advanced tab. Then click Performance Options and Change. It might be useful to move the swap file to a faster disk drive or a drive connected to a separate controller card. Any changes you make might adversely affect performance. It is important that you evaluate whether any changes you make will help the situation—or, perhaps, make things worse! In general, it is advisable to let Windows dynamically manage the size of the swap file unless the system is running very low on disk space.

Tip

If Access 2002 or Windows is running on a compressed drive, you can improve performance by moving the swap file to an uncompressed drive. If possible, the swap file should be located on a drive or partition solely dedicated to the swap file, or on a drive or partition that is accessed rarely by other applications. This helps to ensure that the entire swap file remains in a contiguous location on a disk.

Run Access and Your Application Locally

In Chapter 20, “Developing Multiuser and Enterprise Applications,” you will learn that it is best to install both the Access software and your application objects on each user’s local machine. Only the data tables should be stored on a network file server. Otherwise, you will be sending DLLs, OLE objects, help files, type libraries, executables, and database objects all over the network wire.

Tip

One very viable option is to run Access 2002 using Windows 2000 Terminal Services. In this scenario, Access is installed on a very powerful server machine running Windows 2000 Server Terminal Services. Workstations connect to the terminal server using the Terminal Services Client utility. No data travels over the network wire. Each user becomes a session running on the server machine. All processing is done on the server machine. Keystrokes and mouse movements are sent from the client machine to the server, which processes them and sends a screen image back to the client.

Do Everything You Can to Make Windows Itself Faster

It always amuses me that the users with the slowest machines and the least memory have the most accessories running. These accessories include multimedia, fancy wallpapers, and other nifty utilities. If performance is a problem, you might try to see whether eliminating some of the frivolous niceties improves the performance of your application. If it does, encourage the user to eliminate the frills, get more memory, or accept your application’s performance. Furthermore, if you are finished using other applications, such as Microsoft Excel, close them. This frees up system memory for Access.

Another tip to make Windows 95 and Windows 98 run faster is to shut down and restart on a regular basis. Memory tends to get fragmented, making applications run more slowly. Although I can go weeks or months in Windows NT or Windows 2000 without rebooting, I find it beneficial to reboot my Windows 95 and Windows 98 machines a couple of times a day.

Change Access’s Software Settings

In addition to the more obvious measures just outlined, some minor software tweaking can go a long way toward improving performance. Adjusting several settings in the Windows registry can dramatically improve performance. These changes all involve the registry’s ISAM section. The properties you might want to change include MaxBufferSize and ReadAheadPages. Both these settings determine how the Jet Engine uses memory.

MaxBufferSize controls the maximum size of the Jet Engine’s internal cache. By default, it is set to optimize performance on most machines. It does this by reading data in 2KB pages, placing the data in a memory cache. The data in the cache is readily available to forms, reports, tables, and queries. Lowering the value for MaxBufferSize frees memory for other tasks. This might be helpful on a machine with a minimum memory configuration.

ReadAheadPages controls the number of 4KB data pages that the Jet Engine reads ahead when performing sequential page reads. This number can range from 0–31, with the default at 16. The higher the number, the more efficient Access is at reading ahead so that data is available when you need it. The lower this number, the more memory is freed up for other tasks.

As you configure any of these settings, remember that what is good for one machine is not necessarily good for the next. The settings for each machine must be optimized with its unique hardware configuration in mind.

What Jet 3.5 Did to Improve Performance

Specific improvements that appeared with Jet 3.5 (Access 97) include the following:

  • Faster delete operations. Portions of a page can be removed at once, instead of having to be removed row by row.

  • Better multiuser concurrency on indexed columns. More users can read and update indexed columns without experiencing locking conflicts; indexed columns no longer contain read locks.

  • Implicit transaction processing. Whereas many people wrapped processing loops in the BeginTrans...CommitTrans construct in earlier versions of Access to limit the number of disk writes, the Jet 3.5 Engine handled this quite well on its own.

  • Large queries run faster. This is because of improvements in the transactional behavior for SQL data manipulation language (DML) statements as well as new registry settings that force transactions to commit when a certain lock threshold is reached.

  • Queries containing the inequality operator (<>) run faster.

  • Sequential reads are faster. Up to 64K of disk space can be allocated at a time.

  • Temporary queries run faster.

  • Deleting a table is faster when you use SQL DROP or SQL DELETE.

  • The amount of space occupied by indexes is reduced.

  • When you compact a database, all indexes are optimized for performance.

  • Improved page allocation mechanism. This better ensures that data from a table is stored on adjacent pages and improves the read-ahead capability.

  • Dynamically configured cache. The cache is configured at startup based on the amount of system memory available and contains the most recently used data, thereby enhancing performance.

  • ISAM support for HTML files.

  • The MaxLocksPerFile registry setting enables you to force records to commit when a certain lock threshold is hit. This speeds up the completion of large queries when data is stored on NetWare- and Windows NT–based servers.

Understanding What Jet 4.0 Does to Improve Performance

Improvements made to the Jet 4.0 Engine, which is included with Access 2000 and Access 2002, have dramatically improved its performance over that of its predecessors. Some of these improvements appeared with the Jet 3.0 Engine that shipped with Access 95 or the Jet 3.5 Engine that shipped with Access 97, but many are new to Jet 4.0. The Jet 4.0 Engine is thoroughly 32-bit. It takes advantage of multiple execution threads, providing significant performance benefits.

The following are improvements introduced in Jet 4.0:

  • Data page size is now 4KB instead of 2KB. This increases the maximum database size from 1.07GB to 2.14GB, and provides better performance for many operations because of a decrease in I/O.

  • The native OLEDB Provider offers superior performance to ODBC data.

  • The first 255 characters of memo fields can be indexed. This significantly improves performance when searching and sorting memo fields.

Letting the Performance Analyzer Determine Problem Areas

You can do many things to improve the performance of an application. Most of them require significant attention and expertise on your part. The Performance Analyzer is a tool that does some of that work for you. This tool analyzes the design of an Access application in order to suggest techniques you can use to improve the application’s performance. Many of the techniques the Performance Analyzer suggests can be implemented automatically.

To use the Performance Analyzer, choose Tools|Analyze|Performance. The dialog box in Figure 17.1 appears.

The Performance Analyzer dialog box.

Figure 17.1. The Performance Analyzer dialog box.

Select the individual tables, queries, forms, reports, macros, modules, and relationships you want the Performance Analyzer to scrutinize. If you want Access to analyze the relationships, you must click the Current Database tab and then select Relationships. Make all your selections and click OK. When the Performance Analyzer completes the analysis process, the second part of Performance Analyzer dialog box appears, as shown in Figure 17.2. This window provides you with a list of suggested improvements to the selected objects. The results are broken down into Recommendations, Suggestions, Ideas, and Items (meaning items that were automatically fixed). Suggested improvements will include enhancements such as the addition of an index or the conversion of an OLE object. After analyzing the Northwind database that ships with Access, for example, the Performance Analyzer suggested that fewer controls should be placed on the Employees form.

The second part of the Performance Analyzer dialog box.

Figure 17.2. The second part of the Performance Analyzer dialog box.

Designing Tables to Optimize Performance

Now that you have seen the changes you can make to your environment to improve performance, take a look at the changes you can make to your data structures to optimize performance. Such changes include eliminating redundant data, using indexes, selecting appropriate field data types, and using various query techniques.

Tweaking the data structure is imperative for good performance. No matter what else you do, poor data design can dramatically degrade the performance of your application. All other optimization attempts are futile without proper attention to this area.

Days and days can be spent optimizing your data. These changes must be well thought out and carefully analyzed. Changes often are made over time as problems are identified. Such changes can include those in the following sections.

Why Be Normal?

In essence, “be normal” means normalize your tables—that is, consolidate common data in related tables. Processing the same data that appears in multiple places can significantly slow down your application. This is due to both the volume of data that is generated, as well as the need to update all copies of the data whenever the data changes. Suppose a company address appears in both the Customer table and the Orders table. If the company address changes, it must be changed in both the Customer table and in the Orders table. This information should be included only in the Customer table. Queries should be used to combine the address and order data when needed.

I Thought You Just Told Me to Normalize

When it comes to performance, unfortunately, there are no hard-and-fast rules. Although, most of the time, you gain performance by normalizing your data structure, denormalizing your structure can help at times. This generally is the case when you find yourself creating a particular join over and over again. Another example is an accounting application in which you need to be able to readily see the total amount that a customer owes. Rather than evaluating all the open invoices each time you move to a customer record, you can store the total amount that the customer owes on the customer record. Of course, this requires that you update the summarized figure whenever the customer is billed or makes a payment. In summary, you can try denormalizing the data to see whether dramatic performance improvements result. Remember that denormalization has definite downsides regarding data integrity and maintenance.

Index, Index, Index!

It is amazing how far an index can go in improving performance. Any fields or combination of fields on which you search should be included in an index. You should create indexes for all columns used in query joins, searches, and sorts. You should create primary key indexes rather than unique indexes, and unique indexes rather than non-unique indexes. It is not necessary to create an index for the foreign key field in a one-to-many relationship. The index is created automatically by Access when the relationship is established. Furthermore, there is no benefit to creating an index on a field containing highly repetitive data. An example is a state field in a customer table where all the customers are located in one of two states. Although indexes can be overused, when used properly, the performance improvements rendered by indexes are profound.

Caution

Although indexes can dramatically improve performance, you should not create an index for every field in a table. Indexes do have their downside. Besides taking up disk space, they also slow down the process of adding, editing, and deleting data.

Tip

In a multiple-field index, index on as few fields as possible. Searching through multiple-field indexes can dramatically degrade performance.

Note

Client/server optimization strategies are covered in detail in Alison Balter’s Mastering Access 2002 Enterprise Development.

Select the Correct Data Type

When defining a field, select the shortest data type available for the storage of the data. If you will be storing a code between 1 and 10 within the field, for example, there is no reason to select Double for a numeric field.

Designing Queries to Optimize Performance

Optimizing your queries requires a great deal of practice and experimentation. Some queries involving a one-to-many relationship run more efficiently if the criteria is placed on the one side of the relationship, for example. Others run more efficiently if the criteria are placed on the many side. Understanding some basics can go a long way toward improving the performance of your queries and your application as a whole, as listed in the following:

  • Include as few columns in the resultset as possible.

  • Try to reduce the number of complex expressions contained in the query. Although including a complex expression in a query eliminates the need to build the expression into each form and report, the performance benefits gained sometimes are worth the trouble.

  • Use the Between operator rather than greater than (>) and less than (<) operators.

  • Use Count(*) rather than Count([column]).

  • Group Totals queries by the field that is in the same table you are totaling. In other words, if you are totaling cost multiplied by price for each order in the Order Detail table, group by the order ID within the Order Detail table, not by the order ID within the Orders table.

Now that you have seen what you can do with the design of your queries to improve performance, take a look at a couple simple techniques you can use to improve the performance of your queries.

A simple but often neglected method of optimizing queries is to deliver your queries compiled. A query compiles when you open it in Datasheet view and then simply close it. If you modify a query and then save it, it is not compiled until the query runs. All queries are compiled when you compact a database. Delivering precompiled queries ensures that they run as quickly as possible. It is therefore a good idea to compact a database before you distribute it to your users.

Finally, it is important that you compile your queries using the same amount of data that your application will contain. This is because Jet’s Query Optimizer optimizes the query differently, depending on the amount of data it finds. If you build a query using 100 records that will run on a live table containing 100,000 records, the query will not be optimized properly. You must rerun and resave your query using the correct quantity of data if you want the query to be optimized properly, or compact the database after the live data has been entered.

Making Coding Changes to Improve Performance

No matter what you do to optimize the operating system environment and improve your data design, poor code can continue to bog you down. A properly optimized application is optimized in terms of the environment, data design, and code. Just as poor table design can degrade performance, poor coding techniques also can have a dramatic negative effect on performance. Changes to your code include eliminating variants and dead code, using built-in collections, and using specific object types. An important code-related optimization is to deliver your modules precompiled.

The following changes and techniques can aid in the improvement of performance. It is important to recognize that any one change won’t make much of a difference. However, an accumulation of all the changes, especially where code is being re-executed in a loop, can make a significant impact on your application’s performance.

Eliminate Variants and Use the Smallest Data Type Possible

Variant variables are the slowest for the operating system to process; they carry a lot of overhead because they are resolved at runtime. Remember that this statement declares a variant type of variable:

Dim intCounter

To strong-type this variable as an integer, for example, your code must be modified to look like this:

Dim intCounter As Integer

Not only should you strong-type your variables, but you also should use the smallest data type possible. Remember that data types such as Boolean, Byte, Integer, and Long are the smallest and therefore the fastest to resolve. These are followed by Single, Double, Currency, and (finally) Variant. Of course, if you must store very large numbers with decimal points in a variable, you cannot pick Single. Just keep in mind that it is wise to select the smallest data type appropriate for the use of the variable. Listing 17.1 provides code that illustrates the difference between using a variant and a long integer.

Example 17.1. Data Type Benchmark

Private Sub cmdVariantBenchMark_Click()
    Dim vntAny
    Dim intCounter As Long
    Dim dblStartTime As Double
    Dim dblTime1 As Double
    Dim dblTime2 As Double

    'Execute loop with variant
    dblStartTime = Timer

    Do Until vntAny = 500000
        vntAny = vntAny + 1
    Loop

    dblTime1 = Timer - dblStartTime

    'Execute loop with integer
    dblStartTime = Timer

    Do Until intCounter = 500000
        intCounter = intCounter + 1
    Loop

    dblTime2 = Timer - dblStartTime

    'Display time and percent differences
    Me.txtSlow = dblTime1
    Me.txtOptimized = dblTime2
    Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100

End Sub

The code, found in the form frmBenchmark in the Chap17Ex.mdb sample database, loops using a variant, and then a long integer. The amount of time required to execute each loop is displayed, along with the percent difference between the two techniques.

Use Specific Object Types

Just as using the General variant data type is inefficient, using generic object variables also is inefficient. This is because the compiler needs to evaluate their type at runtime. The MakeItBold subroutine uses a generic object variable, as shown in Listing 17.2.

Example 17.2. The MakeItBold Subroutine

Private Sub cmdMakeBold_Click()
    Call MakeItBold(Screen.PreviousControl)
End Sub

Sub MakeItBold(ctlAny As Control)
    ctlAny.FontBold = True
End Sub

On the other hand, the SpecificBold subroutine uses a specific object variable, as Listing 17.3 shows.

Example 17.3. The SpecificBold Subroutine

Private Sub cmdSpecificBold_Click()
    Call SpecificBold(Screen.PreviousControl)
End Sub

Sub SpecificBold(txtAny As TextBox)
    txtAny.FontBold = True
End Sub

The difference is that the SpecificBold routine expects to receive only text boxes. It does not need to resolve the type of object it receives and therefore is more efficient.

This code is contained in the CHAP17EX.MDB database on the accompanying CD-ROM. You can find the example in frmObjVar.

The SpecificBold Subroutine

The best way to truly compare using a specific control versus a generic control is to benchmark the techniques, as shown in Listing 17.4.

Example 17.4. Object Type Benchmark

Private Sub cmdObjectTypes_Click()
    Dim intCounter As Long
    Dim dblStartTime As Double
    Dim dblTime1 As Double
    Dim dblTime2 As Double

    'Execute loop with generic control
    dblStartTime = Timer

    For intCounter = 1 To 5000
        Call MakeItBold(Me.txtOptimized)
    Next intCounter

    dblTime1 = Timer - dblStartTime

    'Execute loop with specific control
    dblStartTime = Timer

    For intCounter = 1 To 5000
        Call SpecificBold(Me.txtOptimized)
    Next intCounter

    dblTime2 = Timer - dblStartTime

    'Display time and percent differences
    Me.txtSlow = dblTime1
    Me.txtOptimized = dblTime2
    Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100

End Sub

The code, found in frmBenchmark, passes a text box to two different routines. The first routine receives any control as a parameter. The second routine receives only text boxes as a parameter. The benchmarks prove that routines that utilize specific object types take less time and therefore are more efficient.

Use Inline Code

There is a tendency to call out to procedures for everything. This is good from a maintenance standpoint but not from an efficiency standpoint. Each time VBA calls out to a procedure, additional time is taken to locate and execute the procedure. This is particularly evident when the procedure is called numerous times. The alternative is to use inline code. Executing inline code is more efficient than calling out to procedures because Access does not need to locate the code. The downside of inline code is that it is more difficult to maintain. You must decide how important maintainability is compared to speed.

Listing 17.5 shows the same code called as a routine and executed in-line. The benchmark shows that the inline code executes much more quickly.

Example 17.5. Inline Code Benchmark

Private Sub cmdInLine_Click()
    Dim dblAny As Double
    Dim intCounter As Long
    Dim dblStartTime As Double
    Dim dblTime1 As Double
    Dim dblTime2 As Double

    'Execute loop calling out to procedure
    dblStartTime = Timer

    For intCounter = 1 To 50000
        Call SmallRoutine
    Next intCounter

    dblTime1 = Timer - dblStartTime

    'Execute loop with in-line code
    dblStartTime = Timer

    For intCounter = 1 To 50000
        dblAny = 5 / 3
    Next intCounter

    dblTime2 = Timer - dblStartTime

    'Display time and percent differences
    Me.txtSlow = dblTime1
    Me.txtOptimized = dblTime2
    Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub

Private Sub SmallRoutine()
    Dim dblAny As Double
    dblAny = 5 / 3
End Sub

Toggle Booleans Using Not

This code is very inefficient:

If bFlag = True Then
  bFlag = False
Else
   bFlag = True
End If

It should be modified to look like this:

bFlag = Not bFlag

Besides requiring fewer lines of code, this expression evaluates much more quickly at runtime. Listing 17.6 proves that toggling the Boolean variable is a much more efficient approach to the problem than having to test each condition separately. This code is found in frmBenchmarks on the CD-ROM accompanying this book.

Example 17.6. Toggling Boolean Benchmark

Private Sub cmdBooleans_Click()
    Dim boolAny As Boolean
    Dim intCounter As Long
    Dim dblStartTime As Double
    Dim dblTime1 As Double
    Dim dblTime2 As Double

    'Execute loop with If statement
    dblStartTime = Timer

    For intCounter = 1 To 100000
        If boolAny = True Then
            boolAny = False
        Else
            boolAny = True
        End If
    Next intCounter

    dblTime1 = Timer - dblStartTime

    'Execute loop toggling boolean
    dblStartTime = Timer
    For intCounter = 1 To 100000
        boolAny = Not boolAny
    Next intCounter

    dblTime2 = Timer - dblStartTime

    'Display time and percent differences
    Me.txtSlow = dblTime1
    Me.txtOptimized = dblTime2
    Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100

End Sub

Use the Built-In Collections

The built-in collections are there whether or not you use them. By using For Each...Next and a collection of objects, you can write very efficient code, as shown in Listing 17.7.

Example 17.7. Using For Each...Next

Sub FormCaption()
   Dim frm As Form
   For Each frm In Forms
      frm.Caption = frm.Caption & " - " & CurrentUser()
   Next
End Sub

In this example, you use the Forms collection to quickly and efficiently loop through each form, changing the caption on its title bar. The code shown in Listing 17.8 illustrates the use of the Forms collection, as well as an alternative method of accomplishing the same task.

Example 17.8. For Each...Next Benchmark

Private Sub cmdCollections_Click()
    Dim frm As Form
    Dim intNumForms As Integer
    Dim intLoop As Integer
    Dim intCounter As Long
    Dim dblStartTime As Double
    Dim dblTime1 As Double
    Dim dblTime2 As Double
    'Execute loop with For Next
    dblStartTime = Timer

    For intCounter = 1 To 50
        intNumForms = Forms.Count - 1
        For intLoop = 0 To intNumForms
            Forms(intLoop).Caption = "Hello"
        Next intLoop
    Next intCounter

    dblTime1 = Timer - dblStartTime

    'Execute loop with For Each
    dblStartTime = Timer

    For intCounter = 1 To 50
        For Each frm In Forms
            frm.Caption = "Hello"
        Next frm
    Next intCounter

    dblTime2 = Timer - dblStartTime

    'Display time and percent differences
    Me.txtSlow = dblTime1
    Me.txtOptimized = dblTime2
    Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub

Without the For Each..Next loop, a variable must be used to loop through the forms. Notice that intNumForms is set equal to the number of forms in the Forms collection minus one. The loop goes from zero to the value stored in intNumForms, changing the caption of the specified form. Although the performance gains realized by using the Forms collection are not dramatic, you’ll probably agree that the Forms collection technique is much simpler to implement.

Use the Len Function

Using the Len function (as shown in Listing 17.9) is more efficient than testing for a zero-length string (as shown in Listing 17.10).

Example 17.9. Using the Len Function

Sub SayNameLen(strName As String)
   If Len(strName) Then
      MsgBox strName
   End If
End Sub

Example 17.10. Testing for a Zero-Length String

Sub SayNameZero(strName As String)
   If strName <> "" Then
      MsgBox strName
   End If
End Sub

Listing 17.9 is easier for VBA to evaluate and therefore runs more quickly and efficiently. This is emphasized by the code shown in Listing 17.11 (located in frmBenchmark). The code shows two loops. One utilizes the Len function and the other does not. The benchmark proves that the routine that utilizes the Len function executes more quickly.

Example 17.11. Len Benchmark

Private Sub cmdLen_Click()
    Dim dblStartTime As Double
    Dim dblTime1 As Double
    Dim dblTime2 As Double
    Dim strTextBoxValue As String

    strTextBoxValue = Me.txtOptimized

    'Execute loop with zero-length string
    dblStartTime = Timer

     For intCounter = 1 To 50000
         If strTextBoxValue <> "" Then
         End If
     Next intCounter

     dblTime1 = Timer - dblStartTime

     'Execute loop with Len
     dblStartTime = Timer

     For intCounter = 1 To 50000
         If Len(strTextBoxValue) Then
         End If
     Next intCounter

     dblTime2 = Timer - dblStartTime

     'Display time and percent differences
     Me.txtSlow = dblTime1
     Me.txtOptimized = dblTime2
     Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100

End Sub

Use True and False Instead of 0

This example is similar to the preceding one. It is better to evaluate for True and False (as shown in Listing 17.12) instead of 0 (as shown in Listing 17.13).

Example 17.12. Evaluating for True and False

Sub SaySalaryTrue(lngSalary As Long)
    If lngSalary Then
        MsgBox "Salary is " & lngSalary
    End If
End Sub

Example 17.13. Evaluating for 0

Sub SaySalaryZero(lngSalary As Long)
    If lngSalary <> 0 Then
        MsgBox "Salary is " & lngSalary
    End If
End Sub

The code in Listing 17.12 runs more efficiently. The benchmark shown in Listing 17.14 provides an example. The lngSalary is evaluated against zero in the top loop. The bottom loop tests lngSalary against True. The second loop runs more quickly.

Example 17.14. True/False Benchmark

Private Sub cmdTrueFalse_Click()
    Dim intCounter As Long
    Dim lngSalary As Long
    Dim dblStartTime As Double
    Dim dblTime1 As Double
    Dim dblTime2 As Double

    'Execute loop with zero
    dblStartTime = Timer

    For intCounter = 1 To 50000
        If lngSalary <> 0 Then
        End If
    Next intCounter

    dblTime1 = Timer - dblStartTime

    'Execute loop with True/False
    dblStartTime = Timer

    For intCounter = 1 To 50000
        If lngSalary Then
        End If
    Next intCounter

    dblTime2 = Timer - dblStartTime

    'Display time and percent differences
    Me.txtSlow = dblTime1
    Me.txtOptimized = dblTime2
    Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100

End Sub

Note

Although both the Len function, and the use of True/False, provided consistently better results in Access 2000 and earlier versions of Access, they appear to provide inconsistent results in Access 2002. This leads me to believe that there might have been changes to how these functions, or their alternatives, execute in Access 2002. I suggest that you test both the Len function and the use of True/False in your own environments to determine if they result in improved performance.

Use Transactions...Sometimes?

In versions of Access prior to Access 95, transactions dramatically improved performance. Using explicit transactions, the data is written to disk only once, after the CommitTrans. All changes between a BeginTrans and a CommitTrans are buffered in memory. Because disk access has the slowest throughput on a computer, this technique offered major performance benefits when it was introduced. The difference between Access 95 and all subsequent versions of Access is that the Jet 3.0, 3.5, 3.6 and 4.0 engines in the later versions implicitly buffer transactions. Most of the time, Jet’s own transaction-handling offers better performance than your own. At other times, you can improve on what Jet does. The only way you will know what works for your code is to do your own benchmarking. Each situation is different.

Eliminate Unused Dim and Declare Statements

As you modify your subroutines and functions, you often declare a variable and then never use it. Each Dim statement takes up memory, whether or not you are using it. Furthermore, Declare statements, which are used to call external library functions, also take up memory and resources. You should remove these statements if they are not being used.

Eliminate Unused Code

Most programmers experiment with various alternatives for accomplishing a task. This often involves creating numerous test subroutines and functions. The problem is that most people do not remove this code when they are done with it. This dead code is loaded with your application and therefore takes up memory and resources. Several third-party tools are available that can help you find both dead code and variable declarations. One tool that many people use is called Total Access Analyzer, by FMS, Inc.

Use Variables to Refer to Properties, Controls, and Data Access Objects

If you are going to repeatedly refer to an object, you should declare an object and refer to the object variable rather than the actual control, as shown in Listing 17.15.

Example 17.15. Declaring an Object and Referring to the Object Variable

Forms!frmAny.txtHello.FontBold = True
Forms!frmAny.txtHello.Enabled = True
Forms!frmAny.txtHello.Left = 1
Forms!frmAny.txtHello.Top = 1

This is a very scaled-down example, but, if numerous properties are being changed, or if this code is being called recursively, an object variable can make the code more efficient, as Listing 17.16 shows.

Example 17.16. Using an Object Variable to Make Your Code More Efficient

Private Sub cmdChangeObject_Click()
    Dim txt As TextBox
    Set txt = Forms!frmHello.txtHello1
    txt.FontBold = True
    txt.Enabled = True
    txt.Left = 100
    txt.Top = 100
End Sub

The benchmark shown in Listing 17.17 contains two loops. The first loop sets four properties of the same control, explicitly referencing the control as each property is set. The second loop uses an object variable to accomplish the same task. The difference in performance between the two loops is somewhat dramatic.

Example 17.17. Object Variable Benchmark

Private Sub cmdObjectVariable_Click()
    Dim intCounter As Long
    Dim dblStartTime As Double
    Dim dblTime1 As Double
    Dim dblTime2 As Double

    'Execute loop without object variable
    dblStartTime = Timer

    For intCounter = 1 To 1000
        Forms.frmBenchMark.txtOptimized.FontBold = True
        Forms.frmBenchMark.txtOptimized.Enabled = True
        Forms.frmBenchMark.txtOptimized.Locked = False
        Forms.frmBenchMark.txtOptimized.BackStyle = vbNormal
    Next intCounter

    dblTime1 = Timer - dblStartTime

    'Execute loop with object variable
    dblStartTime = Timer

    For intCounter = 1 To 1000
        Dim txt As TextBox
        Set txt = Forms.frmBenchMark.txtOptimized
        txt.FontBold = True
        txt.Enabled = True
        txt.Locked = False
        txt.BackStyle = vbNormal
    Next intCounter

    dblTime2 = Timer - dblStartTime

    'Display time and percent differences
    Me.txtSlow = dblTime1
    Me.txtOptimized = dblTime2
    Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100


End Sub

Use With...End With

Another way to optimize the code in the preceding example is to use a With...End With construct, as shown in Listing 17.18.

Example 17.18. Using With...End With

Private Sub cmdChangeObjectWith_Click()
    With Forms!frmHello.txtHello2
       .FontBold = True
       .Enabled = True
       .Left = 100
       .Top = 100
    End With
End Sub

The code in Listing 17.19 shows two different loops. The first loop explicitly references the text box four different times to set four different properties. The second loop uses a With statement to reference the same control and set the four properties. The code in the second loop executes much more efficiently.

Example 17.19. Object Variable Resolution Benchmark

Private Sub cmdWith_Click()
    Dim intCounter As Long
    Dim dblStartTime As Double
    Dim dblTime1 As Double
    Dim dblTime2 As Double

    'Execute loop without With statement
    dblStartTime = Timer

    For intCounter = 1 To 1000
        Forms.frmBenchMark.txtOptimized.FontBold = True
        Forms.frmBenchMark.txtOptimized.Enabled = True
        Forms.frmBenchMark.txtOptimized.Locked = False
        Forms.frmBenchMark.txtOptimized.BackStyle = vbNormal
    Next intCounter

    dblTime1 = Timer - dblStartTime

    'Execute loop with With statement
    dblStartTime = Timer

    For intCounter = 1 To 1000
        With Forms.frmBenchMark.txtOptimized
            .FontBold = True
            .Enabled = True
            .Locked = False
            .BackStyle = vbNormal
        End With
    Next intCounter

    dblTime2 = Timer - dblStartTime

    'Display time and percent differences
    Me.txtSlow = dblTime1
    Me.txtOptimized = dblTime2
    Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100

End Sub

Resolve Variable Outside a Loop

Although both the object variable reference and the With statement significantly improve performance, Listings 17.17 and 17.19 can be further improved by resolving the object variable outside the loop. Listing 17.20 provides an example.

Example 17.20. Object Variable Resolution Benchmark

Private Sub cmdVariable_Click()
    Dim txtAny As TextBox
    Dim intCounter As Long
    Dim dblStartTime As Double
    Dim dblTime1 As Double
    Dim dblTime2 As Double

    'Execute loop without object resolution
    dblStartTime = Timer

    For intCounter = 1 To 1000
        Forms.frmBenchmark.txtOptimized.FontBold = True
        Forms.frmBenchmark.txtOptimized.Enabled = True
        Forms.frmBenchmark.txtOptimized.Locked = False
        Forms.frmBenchmark.txtOptimized.BackStyle = vbNormal
    Next intCounter

    dblTime1 = Timer - dblStartTime

    'Execute loop with object resolution
    dblStartTime = Timer

    Set txtAny = Forms.frmBenchmark.txtOptimized
    For intCounter = 1 To 1000
        With txtAny
            .FontBold = True
            .Enabled = True
            .Locked = False
            .BackStyle = vbNormal
        End With
    Next intCounter

    dblTime2 = Timer - dblStartTime

    'Display time and percent differences
    Me.txtSlow = dblTime1
    Me.txtOptimized = dblTime2
    Me.txtPercent = (1 - (dblTime1 / dblTime2)) * 100
End Sub

Notice that the object variable is resolved outside the loop. This loop executes significantly faster than the loops in Listings 17.17 and 17.19.

Use the Me Keyword

In the preceding example, you used Forms!frmHello.txtHello to refer to a control on the current form. It is more efficient to refer to the control as Me.txtHello because VBA searches only in the local name space. Although this makes your code more efficient, the downside is that the Me keyword only works within form, report, and class modules. It won’t work within standard code modules. This means that you cannot include the Me keyword in generic functions that are accessed by all your forms.

Use String Functions When VBA Provides a Variant and String Alternative

Several VBA functions come in two forms: one with a dollar sign ($) and one without. An example is Left(sName) versus Left$(sName). Whenever it is acceptable to return a string, it is more efficient to use the version with the dollar sign, which return strings rather than variants. When a string variable is returned, VBA doesn’t need to perform type conversions.

Use Dynamic Arrays

Array elements take up memory, whether or not they are being used. It therefore is sometimes preferable to use dynamic arrays. You can increase the size of a dynamic array as necessary. If you want to reclaim the space used by all the elements of the array, you can use the Erase keyword, as in this example:

Erase aNames

If you want to reclaim some of the space being used by the array without destroying data in the elements you want to retain, use Redim Preserve:

Redim Preserve aNames(5)

This statement sizes the array to six elements. (It is zero-based.) Data within those six elements is retained.

Caution

You must be careful when using dynamic arrays with Redim Preserve. When you resize an array using Redim Preserve, the entire array is copied in memory. If you are running in a low-memory environment, this can mean that virtual disk space is used, which slows performance—or worse than that, the application can fail if both physical and virtual memory are exhausted.

Use Constants When They Are Available

Constants improve both readability and performance. A constant’s value is resolved after compilation. The value the constant represents is written to code. A normal variable has to be resolved as the code is running because VBA needs to obtain the current value of the variable.

Use Bookmarks

A bookmark provides you with the most rapid access to a record. If you are planning to return to a record, set a variable equal to that record’s bookmark, making it very easy to return to that record at any time. Listing 17.21 shows an example that uses a bookmark.

Example 17.21. Using a Bookmark

Sub BookMarkIt()
    Dim rst As ADODB.Recordset
    Set rst = New ADODB.Recordset

    Dim varBM As Variant

    rst.Open "tblProjects", CurrentProject.Connection, adOpenStatic
    varBM = rst.Bookmark
    Do Until rst.EOF
        Debug.Print rst!ProjectID
        rst.MoveNext
    Loop
    rst.Bookmark = varBM
    Debug.Print rst!ProjectID
End Sub

You can find this code in basOptimize of CHAP17EX.MDB. The bookmark is stored in a variable until the Do...Until loop executes. Then, the recordset’s bookmark is set equal to the value contained within the variable.

Using a Bookmark

Set Object Variables Equal to Nothing

Object variables take up memory and associated resources. Their value should be set to Nothing when you are finished using them, for example,

Set oObj = Nothing

This conserves memory and resources.

Use Action Queries Instead of Looping Through Recordsets

Besides being easier to code, executing a stored query is much more efficient than looping through a recordset, performing some action on each record. Listing 17.22 shows an example that loops through a recordset.

Example 17.22. Looping Through a Recordset

Sub LoopThrough()
    Dim rst As ADODB.Recordset
    Set rst = New ADODB.Recordset

    rst.Open "tblProjects", CurrentProject.Connection, adOpenDynamic, _
    adLockOptimistic
    Do Until rst.EOF
        rst!ProjectTotalEstimate = rst!ProjectTotalEstimate + 1
        rst.UPDATE
        rst.MoveNext
    Loop
End Sub

This code, which is located in basOptimize of CHAP17EX.MDB, loops through a recordset, adding 1 to each project’s total estimate. Contrast this with the code in Listing 17.23.

Looping Through a Recordset

Example 17.23. Executing a Stored Query

Sub ExecuteQuery()
    Dim adoCat As ADOX.Catalog
    Dim cmd As ADODB.Command
    Set adoCat = New ADOX.Catalog
    Set cmd = New ADODB.Command

    Set adoCat.ActiveConnection = CurrentProject.Connection
    Set cmd = adoCat.Procedures("qupdLowerEstimate").Command
    cmd.Execute

End Sub

This code uses a command object to execute a stored query called qupdLowerEstimate. The query runs much more efficiently than the Do...Until loop shown in Listing 17.22.

Note

The most efficient method to update records is with a stored procedure. You can utilize stored procedures with a client/server database engine such as Microsoft SQL Server. This is covered in detail in Alison Balter’s Mastering Access 2002 Enterprise Development.

Deliver Your Application with the Modules Compiled

Applications run slower when they are not compiled. Forms and reports load slower, and the application requires more memory. If you deliver your application with all the modules compiled, they do not need to be compiled on the user’s machine before they run.

To easily recompile all modules, choose Debug|Compile with the Visual Basic Editor (VBE) active. This command opens and compiles all code in the application, including the code behind forms and reports. It then saves the modules in the compiled state, preserving the compiled state of the application.

Retaining the Compiled State

Don’tbother choosing the Debug|Compile command if you plan to make additional changes to the application. An application becomes decompiled whenever the application’s controls, forms, reports, or modules are modified. Even something as simple as adding a single control to a form causes the application to lose its compiled state. It therefore is important to choose the Debug|Compile command immediately before you distribute the application.

Caution

Renaming a database file causes the code contained in the database to decompile. It therefore is important to always choose the Compile command after renaming a database.

Distribute Your Application as an MDE

The process of creating an MDE file compiles all modules, removes editable source code, and compacts the destination database. All Visual Basic code will run, but cannot be viewed or edited. This improves performance, reduces the size of the database, and protects your intellectual property. Memory use also is improved. The process of saving an application as an MDE, and the implications of doing so, are covered in Chapter 27, “Database Security Made Easy,” and Chapter 32, “Distributing Your Application.”

Organize Your Modules

VBA code theoretically can be placed in any module within your application. The problem is that a module is not loaded until a function within it is called. After a single procedure in a module is called, the entire module is loaded into memory. Furthermore, if a single variable within a module is used, the entire module is loaded into memory. As you might imagine, if you design your application without much thought, every module in your application will be loaded.

If you place similar routines all in one module, that module will be loaded, and others will not. This means that if people are using only part of the functionality of your application, they will never be loading other code modules. This conserves memory and therefore optimizes your application.

Designing Forms and Reports to Improve Performance

You can do several things to forms and reports to improve your application’s performance. These include techniques to quickly load the forms and reports, tips and tricks regarding OLE objects, and special coding techniques that apply only to forms and reports.

Designing Forms

Because forms are your main interface to your user, making them as efficient as possible can go a long way toward improving the user’s perception of your application’s performance. Additionally, many of the form techniques are extremely easy to implement.

Form-optimization techniques can be categorized in two ways: those that make the forms load more quickly, and those that enable you to more efficiently manipulate objects within the form.

The larger a form and the more controls and objects you have placed on it, the less efficient that form is. Make sure that controls on the form do not overlap. It also is extremely beneficial to group form data onto logical pages. This is especially important if your users have insufficient video RAM. Objects on subsequent pages should not be populated until the user moves to that page.

Forms and their controls should be based on saved queries or embedded SQL statements. Include only fields required by the form in the form’s underlying query. Avoid using Select * queries. Because Access is so efficient at internally optimizing the manipulation of query results, this improves the performance of your forms. To further take advantage of the power of queries, reduce the number of records that the query returns, loading only the records you need to at a particular time.

If you will use a form solely to add new records, set the DataEntry property of the form to Yes so that it opens to a blank record. This is necessary because, otherwise, Access must read all records to display the blank record at the end of the recordset.

Avoid bitmaps and other graphic objects if possible. If you must display an image, it is important to remember that OLE objects take far more resources than images. If an OLE bitmapped object does not need to be changed, convert it to an image. To accomplish this, right-click the object and choose Change To|Image.

Avoid the use of subforms whenever possible. Access treats a subform as a separate form. It therefore takes up significant memory. Make sure that all fields in a subform that are either linked to the main form or used for criteria are indexed. Make sure that only necessary fields are included in the record source of the subform. If the data in the subform does not need to be edited, set its AllowEdits, AllowAdditions, and AllowDeletions properties to No, or set its RecordsetType property to Snapshot.

Make sure that the RowSource for a combo box includes only the columns needed for the combo box. Index on the first field that appears in the combo box. This has a dramatic effect on the speed at which a user can move to an element of the combo box. Also, whenever possible, make the first visible field of a combo box a text field. Access converts numeric fields to text as it searches through the combo box to find a matching value. Finally, don’t base list boxes or combo boxes on linked data if that data rarely, if ever, changes. Instead, make the static table local, updating it whenever necessary.

As a general rule regarding the performance of forms, place all database objects, except data, on each user’s machine. This eliminates the need for Access to constantly pull object definitions over the network.

Close forms that no longer are being used. This is necessary because open forms take up memory and resources, degrading performance.

Another tip that can help you dramatically improve the performance of your forms is to use the default formatting and properties for as many controls as possible. This significantly improves performance because only the form and control properties that differ from the default properties are saved with the form.

Tip

If most controls have a set of properties that are different from those of the default control for the form, you should change the default control and then add controls based on the default. Access saves only the properties of the default control and does not need to store the properties for each control placed on the form. This can result in dramatic performance improvements. Changing the default control is covered in Chapter 9, “Advanced Form Techniques.”

Finally, eliminate the code module from forms that don’t need it. A form without a code module loads more quickly and occupies less disk space. You still can call function procedures from an event property using an expression, or you can navigate about your application from the form using hyperlinks. You can remove the module associated with a form by setting the HasModule property to No.

Designing Reports

Many of the report-optimization techniques are the same as the form-optimization techniques. Reducing the number of controls, avoiding overlapping controls, basing reports on queries, avoiding OLE objects, and converting unbound object frames that display graphics to image controls are all techniques that improve the performance of reports as well as forms.

You can use a few additional techniques to specifically improve the performance of reports. Eliminate any unnecessary sorting and grouping expressions, and index all fields on which you sort or group. Base subreports on queries rather than on tables, and include only necessary fields in the queries. Make sure that the queries underlying the report are optimized and that you index all fields in the subreport that are linked to the main report.

A special technique that you can use to improve the performance of reports was introduced with Access 95. It involves the No Data event and the HasData property. The No Data event is fired when a report is opened, and no data is returned by the record source of the report. The HasData property is used to determine whether a report is bound to an empty recordset. If the HasData property of a subreport is False, you can hide the subreport, thereby improving performance.

Summary

The most attractive application can be extremely frustrating to use if its performance is less than acceptable. Because Access itself requires significant resources, you must take the responsibility of making your code as lean and efficient as possible.

This chapter focused on several techniques for improving performance. Probably one of the easiest ways to improve performance is by modifying the hardware and software environment within which Access operates. You learned about adding RAM, defragmenting a hard disk, and tuning virtual memory and other settings to dramatically improve the performance of your applications. You also looked at using the Performance Analyzer to quickly and easily identify problem areas in your application. Finally, the chapter focuses on data-design fundamentals, coding techniques, and techniques to optimize forms and reports.

By following the guidelines covered in this chapter, you can help ensure that you are not inadvertently introducing bottlenecks into your application. Although any one of the suggestions included in this chapter might not make a difference by itself, the combined effects of these performance enhancements can be quite dramatic.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.193.108