You are on page 1of 49

ips and Techniques for Queries

in Access 2007
Office 2007
This content is outdated and is no longer being maintained. It is provided as a courtesy for individuals
who are still using these technologies. This page may contain URLs that were valid when originally
published, but now link to sites or pages that no longer exist.
Summary: Learn about queries and their uses in Microsoft Office Access 2007. (29 printed pages)
Luke Chung, President of FMS Inc
August 2009
Apples to: 2007 Microsoft Office System, Microsoft Office Access 2007
Contents
Overview
Query Types
Basic Select Queries
Setting Criteria
Advanced Select Queries
Action Queries
Queries in Forms, Reports, and Macros
Running Queries through Macros
Using and Running Queries in VBA Code
Additional Resources from Microsoft
About the Author
Additional Resources from FMS, Inc.
Download the Sample Access Database
Overview
Microsoft Office Access 2007 is the most popular Windows database program. A major reason for its
success is its revolutionary query interface. Once data is collected in a database, analysis and updates
need to be performed. Queries offer the ability to retrieve and filter data, calculate summaries (totals), and
update, move and delete records in bulk. Mastering Microsoft Access queries will improve your ability to
manage and understand your data and simplify application development.
The visual representation of tables and the graphical links between them makes Microsoft Access queries
extremely easy to use. Fortunately, the nice user interface also allows very powerful and advanced
analysis. The entire query engine is modeled on SQL systems and allows switching between the graphical
query design and SQL syntax. Many Microsoft Access users and developers learned SQL from this feature.
Knowing the many features of Microsoft Access queries allows you to perform advanced analysis quickly
without programming. This presentation covers the basics of queries revealing a variety of subtleties. It
quickly moves to more advanced topics with hints and techniques for creating sophisticated queries.
Finally, programmatic use of queries is presented.
Query Types
Microsoft Access supports many types of queries. Here is a description of the major categories:
Select Queries
Retrieve records or summaries (totals) across records. Also includes cross-tabulations.
Make Table Queries
Similar to Select Queries but results are placed in a new table.
Append Queries
Similar to Select Queries but results are added to an existing table
Update Queries
Modify data in the records.
Delete Queries
Records are deleted from a table.
Select queries are the most common queries and can be used for viewing and a data source for forms,
reports, controls, and other queries. The other queries create or change data and are known collectively as
Action queries.
Basic Select Queries
The most basic Select queries retrieve the records you specify from a table. You can choose the fields
from a table to display, and specify the criteria for selecting records. In the most cases, while viewing the
query results you can modify the data and update the original records. These updateable views are
extremely powerful.
Selecting Tables and Fields
The first step in creating a query is to specify the table or tables to use and the fields to display. Selecting
tables is simple. Just choose the table from the list when the query is first created or use the Add
Table command from the Query menu. The selected table is placed on the upper portion of the query
design window. From there you can select the fields for the query by double-clicking on them or selecting
several fields (by using Shift-Click or Ctrl-Click) and dragging them to the bottom portion of the query by
example (QBE) grid. Make sure that the Show option is checked to display the field.
Sorting and Reordering Fields
Once you place the fields on the QBE grid, you can reorder the fields by clicking the column and dragging
it to where you want it. To sort the results, specify the Sort option under the fields to sort. You can choose
Ascending or Descending order. Note that you can turn off the Show setting and sort on a field that does
not appear in the display.
Renaming Fields
A very nice feature of Microsoft Access queries is the ability to rename fields. You might have your data
stored in field names that users do not understand. By using a query expression, you can change the field
name the user sees. For example, you can change a field named CustID to Customer ID by placing the
new name followed by a colon and the original name in the QBE field cell: Customer ID:[CustID].
Using Calculated Fields (Expressions)
In addition to retrieving fields from a table, a Select query can also display calculations (expressions). Of
course, expressions cannot be updated because they do not exist in the original table. Expressions are
extremely powerful and allow you to easily display complex calculations. There is an Expression Builder
that simplifies the selection of fields and functions. By default, expression fields are named Expr1, Expr2,
and so on; therefore, you usually want to rename them to something more understandable.
Setting Query Properties
When you design a query, you can choose View | Properties or right click the top portion of the query
and then choose Properties to see and modify the query properties.
Figure 1. Query properties


Description
This property lets you provide a description of the query. Use the property to help you remember the
purpose of the query.
Default View
Show the results in a datasheet like a table, or a pivot chart or pivot table.
Output All Fields
This option is usually set to No. If it is changed to Yes, all the fields of all the tables in the query are
shown. In general, you should leave this property alone and specify the fields desired in the QBE grid.
Top Values
Instead of retrieving all records, you can specify the top n records or n percent, where n is the value
specified here.
Unique Values
By default, this is set to No and all records are retrieved. If you change this to Yes, every record retrieved
contains unique values (SQL uses the SELECT DISTINCTcommand). That is, no retrieved records are
identical. For example, you can run a query for the State field of the Patient table. With this set to No, the
result is a record for each patient. When set to Yes, only the list of unique states is displayed. When set
to Yes, the query is not updateable.
Unique Records
By default this is set to No and all records are retrieved. For one-table queries, this property is ignored.
For multi-table queries, if it is set to Yes, (similar to using aDISTINCTROW in a SQL statement) only
unique records in the underlying tables are retrieved.
The Unique Records and Unique Values properties are linked and only one can be set to Yes (both can
be No). When Unique Records is Yes, Unique Values is automatically set to No. When both properties
are set to No, all records are returned.
Difference between DISTINCT vs. DISTINCTROW
These options sometimes appear to provide the same results, but there are significant differences.
DISTINCT checks the results of query and eliminates duplicate rows. These queries (Unique Values = Yes)
are not updateable. They are a snapshot of your data and don't reflect subsequent data modifications by
users. This is similar to running a Totals Query (for example, using a Group By clause).
DISTINCTROW checks all the fields in the table and then eliminates the duplicate rows. The results of a
query with DISTINCTROW (Unique Records = Yes) are updateable and reflect changes to retrieved records
(but the query does not automatically run again if the data changes to retrieve different rows).
So the difference is that DISTINCT only checks the fields in the results, while DISTINCTROW checks all the
fields in the underlying tables. If your query joins several tables and only displays records from one, the
DISTINCTROW option lets you view and edit the results.
For more information, see Distinct versus DistinctRow.
SQL Server Properties
There are several properties related to SQL Server tables that are more technical and rarely need to be
modified. For more information, refer to the online Help in Microsoft Access.
Filter, Order By, Filter On Load, Order By On Load
Like a form, you can specify the Filter and Order By settings for a query. However, this is usually part of
the query's SQL statement. By using the Filter and Order By properties, you have the extra advantage of
specifying the Filter On Load and Order By On Load properties to apply them or not.
Subdatasheet Name, Link Fields, and Size
If you want to display a subdatasheet to show a one-to-many relationship between the results in this
query with data from another table, you can specify them here. There is a significant performance cost for
having subdatasheets, so only add them if you really need them.
Setting Field Properties
In addition to query properties, each field has properties that can be set. Move to a field in the QBE grid
and right click. Depending on the field type, different properties are available. The most important
properties are for numeric and date fields. You can specify how the fields are formatted when the query is
run.
Viewing Results and SQL Equivalent
Once the query is completed, you can view its results by switching from Design to DataSheet view. You
can also view the SQL equivalent. You can even edit the SQL syntax directly and view the results and/or
switch to Design view.
Setting Criteria
The bottom section of the QBE grid is several rows for Criteria. These are optional entries to specify which
records are retrieved. If you want all the Patients from the state of Virginia, just type VA in the State's
criteria. To further narrow the scope, you can type criteria for several fields.
Multi-Field Query Criteria
Typing criteria on the same row for several fields performs an AND query between the fields. That is,
records that match the criteria in field 1 AND the criteria in field 2, and so on are retrieved. If criteria are
placed in different rows, an OR query is performed; that is, retrieve all records matching criteria in field 1
OR criteria in field 2, and so on.
Criteria Types
The simplest type is the exact match. Just type the value desired in the field's criteria section. Remember
that by using the Show option to eliminate the field from the display, you can specify criteria in fields the
user never sees.
<>, <, >, Between .. And ..
You can also retrieve records where a field does not have a particular value by using < > followed by the
value that you don't want. Similarly, you can use >, <, >=, or <= for ranges. To select records with values
between two values, use the BETWEEN .. AND .. syntax.
Nulls
To select records with Null values, type Is Null. The opposite is Is Not Null. For text fields, remember that
zero length strings ("") are not nulls.
OR and IN(.., .., ..)
To select records where a field can have one of several values, use the OR command. You can simply
say: "MD" or "DC" or "VA". Alternatively, the IN command performs the same function; for
example, IN("MD", "DC", "VA"). The second syntax is easier if you have many values. Of course, if you
have a very large number of values, it is better to keep those values in a table and link your query to it.
That is easier to maintain than OR or IN clauses inside queries.
Wildcard Searches
Sometimes, you need to search for a particular letter or digit. Combined with the Like command, you can
use wildcards to specify such criteria. Microsoft Access uses the following wildcard characters:
? Single character
* Any number of characters
# Single digit
[..] Character list
[!..] Not in character list
For example, if you are interested in a text field where the second letter is "a", the criteria would be: Like
"?a*". If you were seeking values where the second letter could be an "a" or "e", the criteria would
be: Like "?[ae]*". The opposite of this (all values that do not have "a" or "e" as the second letter) is
performed by adding an exclamation point (!) as follows: Like "?[!ae]*". Finally, to select a range of
letters (say "a" through "e"), add a dash between the letters: Like "?[a-e]*".
To search for a wildcard character, enclose the value in brackets. For example, to find values that end in a
question mark, use this: Like "*[?]"
Advanced Select Queries
Using Parameters
Using Access Functions
Using Custom Functions
Other Types of Select Queries
(Top Records, Total Queries, Crosstabs, Multi-table Queries, Basing queries on queries)
Percent of Total
Frequency Distributions
Using Parameters
Often it is not possible to know in advance the criteria for a query field. In those cases, where the filter
values are not known until runtime, a variable (parameter) can be used. When these queries are run, the
user is prompted for the value of each parameter. (The parameters can also be assigned
programmatically). Using parameters in queries is extremely powerful and converts static "hard-coded"
queries to flexible, dynamic ones. The use of parameters can significantly reduce the number of queries
you need, makes queries more useful, and simplifies database maintenance.
It is easy to add parameters. Instead of typing the value of a criterion, type (between the brackets) the
prompt that you want the user to see when the query is run. The value that the user types replaces the
parameter in the query. In the following example, a parameter [Enter State Name:] is the criteria in
the [State] field, and [Enter Minimum Age:] is the parameter in the [Age] field. When this query is
run, the user is prompted for the state desired and minimum age, and the records matching that value are
retrieved.
Figure 2. Select Query example with two parameters



Parameters work as long as the parameter definition does not conflict with the field name among the
query's tables.
To better define a parameter, you should specify it in the list of parameters. This is an optional step, but
there are good reasons to do so. Right-click the top part of the query and choose Parameters. The
following form appears; use the form to list each parameter name and each parameter type.
Figure 3. Query Parameters dialog box



By explicitly defining parameters, users are forced to type values conforming to the type. While it might
not matter for text fields, it is useful for numeric and date fields. This minimizes data entry errors that
cause a "Can't evaluate expression" error message to appear.
Using Access Functions
One of the most powerful features of Microsoft Access queries is their support for Access functions. This is
most useful in Update queries, but can also be used in Select queries. The Advanced: Access Functions
query is an example of this feature.
Figure 4. Query using a built-in VBA function



This query selects the Country names in descending order of name length. The second field renames
itself to [Length], uses the LEN function to calculate the length of each country name, sorts the length in
descending order, and excludes any records with 10 letters or fewer.
While this might not seem particularly useful, there are many situations where using Access functions is
extremely useful and eliminates the need to program. The string functions in particular
(Left$, Right$, Trim$, Mid$, UCase$, LCase$, and so on.) are useful for manipulating portions of strings
and changing case.
Using Custom Functions
In addition to using Microsoft Access functions, queries also support user defined functions. Functions
defined in VBA modules must return an appropriate value and can be used to manipulate each record.
You can reference field values by passing the field name in brackets.
Here is an example where a function (StripLead) is used to remove the leading word of a phrase if it
starts with "The", "An", or "A". This is useful for sorting phrases such as book titles on "real" words.
Figure 5. Query using a user-defined function



This is the code for the StripLead function. It is passed a string and returns the string without the leading
word (if any).
VB
Public Function StripLead(pstrPhrase As String) As String
' Comments: Get rid of leading A, An, or The in a phrase.
' Used for card catalog sorting.
' In: pstrPhrase Phrase to examine
' Returns: The input phrase without the "useless" first word.
' Returns the same phrase if the first word isn't an issue

Dim strFirstWord As String
Dim strReturn As String
Dim intPos As Integer

strReturn = pstrPhrase
intPos = InStr(pstrPhrase, " ")
If intPos > 0 Then
strFirstWord = Left$(pstrPhrase, intPos - 1)
Select Case strFirstWord
Case "A", "An", "The"
strReturn = Right$(pstrPhrase, Len(pstrPhrase) - intPos)
End Select
End If
StripLead = strReturn
End Function


And this is the result. Notice how the sorting of the [Adjusted] field differs from the [Original] field.
Figure 6. User-defined function results


Other Types of Select Queries
Top Records (number and percent)
Total Queries
Crosstab Queries
Multi-table Queries
Basing Queries on Other Queries
Top Records (number and percent)
Select queries retrieve all the records that meet your criteria. There are occasions where you only want a
subset; the top or bottom number of records. Similarly, you might just want the top or bottom percent of
records.
Just create a regular Select query that retrieves the records you want. By changing the Top Values query
property (right-click the top portion of the query), you can specify the number of records to display. The
example below (query: Other: Top 10 Auto Companies) shows only the top 10 records.
Figure 7. Querying the top values



Notice the query is retrieving records in Descending order so the Top Values option retrieves the largest
values. It simply runs the query and displays the specified number of records in the query output's order.
To display the lowest values, the query should retrieve records in Ascending order.
Top Percent of Records
Sometimes, you want a percentage of records and not a fixed number. To retrieve the top n% of the
query, type a percentage (for example, 10%) instead of a value in the Top Value option.
Total Queries
Up to now, we have only retrieved records. With lots of data, it is important to calculate summaries for
groups of records or totals on the entire table. This is possible by specifying Totals from
the Show/Hide ribbon in Access 2007
Figure 8. Specifying Totals from the Ribbon


or by using the View menu in Access 2003 or earlier.
Figure 9. Specifying Totals by using the View menu



This performs calculations across all the records and creates a summary result. For example, you
can Sum on a numeric field to determine the total for the entire table. Additionally, you can group on
fields to calculate totals for each unique combination of values across the group fields.
When Totals is selected, a new Total row appears in the query design. You can specify the type of
calculation you want in that section.
Figure 10. Query with totals



For this query, the result shows average Age, Weight and Cholesterol for patients by State and Gender.
Crosstab Queries
Crosstabs are a powerful analysis tool that lets you quickly see the relationship of data between two fields.
The view is a spreadsheet-like display with unique values of one field as rows, unique values of another
field as columns, and the summary of another field as the cells in the matrix.
For example, with the previous example, a crosstab can clearly show the average Cholesterol between
State (rows) and Sex (columns).
Figure 11. Viewing crosstab results



The easiest way to create a crosstab is to use the Crosstab Wizard. When creating a new query,
select Query Wizard and then follow the Crosstab Query steps.
Figure 12. New Query Wizard



Crosstab queries can also be manually created by selecting Crosstab from the Query menu and
specifying the Row and Column Headings.
Figure 13. Creating a crosstab query manually


Multi-table Queries
To this point, all the queries shown were for one table only. Microsoft Access queries allow very
sophisticated multi-table queries. Criteria and field selections can be from any of the query's tables.
Linking tables on fields is done visually by dragging a line between the fields to link.
For our previous example, we might want to show the full name of each state instead of its abbreviation.
With a State table that contains the abbreviation and full names, this can be easily performed.
Figure 14. Crosstab of Patients and with State name



Notice the link on the [State] fields and the [Name] field from the States table in the query. To create
multi-table queries, the Table row should be displayed. This can be activated from the View | Table
Names menu. Even better, the default query options should set Show Table Names to Yes.
There are several ways to join tables in a query. The previous example was the most common which is an
exact match between fields, sometimes called an inner join. Another join includes retrieving all records
from one table regardless of whether there are matches in the second table. This is called a left join. If the
logic is reversed (all records from the second table and matching records from the first) it is called a right
join. These options can be selected by double-clicking on the linking line and choose among the three
options.
Left Join Between Tables
Here is an example of a query with a left join and the results.
Figure 15. A left join query



Notice how States that do not have patient data are shown with no value in
the MaxOfAge and AvgOfCholesterol fields.
Figure 16. Results of a left join query


No Joins Between Tables
Queries with multiple tables do not even require a line between the tables. If no lines are specified, a
record by record link is assumed. That is every record in the first table is linked to every record in the
second table. This is most useful when one of the tables only has one record. Finally, tables can be linked
through an expression that establishes a partial match or match based on a range of values. Examples are
shown later.
Basing Queries on Other Queries
So far, the queries presented are only based on tables. However, Microsoft Access lets you also base
queries on other queries. This ability to filter and analyze data across several levels is extremely powerful.
The queries themselves behave identically whether the data comes from tables or queries.
Basing queries on other queries can also simplify maintenance of your database by letting you create
standard queries that other queries can use. This can be particularly useful in reports. Of course, you need
to be careful modifying the "core" queries.
Additionally, when you generate reports in multi-user databases, make sure that you don't use queries
when you should use temporary tables that are generated by Make Table and Append queries. Queries
always retrieve the most current data. If you are printing many reports while others are modifying the
data, and consistency between reports is important (the numbers need to tie), you must create temporary
tables with the data you need prior to printing. You can then base your queries on those "static" tables.
Percent of Total
For a field, calculating the percent of each record to the total for an entire table is useful for determining
the relative size of each group. This can be achieved by creating a summary query and using that in
another query with the original table. In this example, we use the Fortune100 table containing sales and
profits data for 100 large corporations; and two queries (Fortune 100 Totals and Fortune 100
PercentOfTotals). Here is how they work:
Step 1: Create a Query calculating the Totals
This is a simple query that sums the values in the two fields: Sales and Profits. For clarity, the resulting
fields are named TotalSales and TotalProfits.
Figure 17. Totals query with Fortune 100 data



Step 2: Create a Query with the Totals and the Original Table
This is a simple select query that retrieves fields from the Fortune100 table and creates new fields for
the Percent of Total calculations. Notice the two tables are not linked with lines between them. They
only interact in the Percent of Total calculations where the values in the calculated fields
using TotalSales and TotalProfits as divisors.
Figure 18. Fortune 100 percent of Total Sales and Profits



Step 3: Run the Query
Running the query provides the desired results.
Figure 19. Results of the query



Notice how the Profit% field shows data nicely formatted (unlike Sales%). The difference is due to
formatting settings on the Profit% field. While designing the query, right-click the Profit% field and
notice its format is set to Percent. For the Sales% field, it's a standard number. This is the reason that the
formula for Sales% includes a 100 factor that is not in the Profit% column.
Frequency Distributions
Frequency distributions reveal the number of records that contain values within numeric ranges. In this
example, we want to know how many patients fall into different age categories (under 25, 25 to 40, 40 to
50, 50 to 60, and 60+). A simple two-table query calculates these results even when the size of the
numeric ranges are not identical. In this example, we use two tables (Age Groups and Patients), and one
query (Frequency: Age Distribution). Just follow these steps:
Step 1: Create a table defining the groups and numeric ranges
Create a table defining the groups and numeric ranges.
Create a table with four fields: Group ID (counter), Group Name (text), Minimum (number),
and Maximum (number). For each record, define the groups and its low and high values.
Figure 20. Define the high and low values for each group in a Group definitions table



Notice how the Maximum value of one record is smaller than the Minimum value of the next record. They
cannot be identical or else such values would fall into two groups. In our example, the Age data are
integers so using integers in this table is okay. Otherwise, you can use numbers very close to each other
(for example, 24.9999999). You can name and specify as many groups as you like.
Step 2: Create multi-table Select query
Create a Totals Select query with the data table and the Group definition table defined above.
Figure 21. Query to calculate frequency distribution



Notice that the two tables boxes are not linked to each other. The first two fields in the query come from
the group table: the Group ID field controlling the sort order, and the Group Name description. The third
field is the count of the Patient (data) table's ID field (this field is used because it is not null for every
record). The final field defines the link between the two tables. Using the Patient table's Age field, the
criterion is Between [Minimum] And [Maximum]. This "bins" the Patient data into the age groups
defined by the Group table.
Step 3: Run the Query
Running the query provides the results
Figure 22. Frequency distribution results



If the Group table's Group ID field is not used in the query, the results would be shown in Ascending
order by Group Name (Under 25 would be the last record).
Action Queries
To this point, we have only covered Select queries. These queries take existing data and display it to you
either record for record or in a summarized manner. Action queries actually change data. These queries
can modify fields, add records to tables, delete records, and even create tables. To specify the query type,
select among the options presented under the Query menu while designing a query.
Figure 23. Query Type options on the Ribbon



These queries are similar to creating Select queries.
Make Table Queries
Make Table Queries are identical to Select queries except that instead of creating a datasheet display of
the results, a new table is created containing the results. These are particularly useful for creating
temporary tables. For example, if you are generating a series of reports while other people are changing
the data, a Make Table query can create a snapshot of your data and allow your reports to work off that
table. This lets you create a consistent set of reports.
Append Queries
Append Queries are also similar to Select queries, but the results are inserted as new records into another
table. The field names do not need to match and expressions can also be used.
Update Queries
Update Queries allow you to modify fields. This is often used to update a field with an expression or data
from another table.
Delete Queries
Delete Queries let you specify the records to be deleted from a table.
Queries in Forms, Reports, and Macros
Queries can be used in forms and reports in a variety of ways. The most common is the Record Source of
the form or report. Another useful place is the RowSource of aComboBox.
Running Queries through Macros
Running a query with a macro is very simple. Just use the OpenQuery command and specify the query
name to run. If the query has parameters, the user is prompted for the values.
If you are running an Action Query, you can do the same thing; however, Action Queries usually display
warning messages prior to changing data. To eliminate such messages, use the SetWarnings command
to turn this off and on before and after the query.
Using and Running Queries in VBA Code
There are many ways to run queries through modules. Here are a few examples:
Creating and using a RecordSet based on a saved Select query
Assigning parameters in queries
Using a SQL string to open a Recordset
Running a stored Action query
Creating a table with a Make Table Query SQL string
Creating and using a RecordSet based on a saved Select query
RecordSets let you programmatically move through a table or query. By assigning a Select query to a
RecordSet, you can move through the table. Commands such
asMoveNext, MoveFirst, MoveLast, MovePrevious, let you control where you are in the query. By
checking the EOF status, you can make sure that you stop at the end of the table. Field values are
referenced with a ! and field name (in Access 2.0, you could use "." instead of "!").
VB

Public Sub BrowseQuery_DAO()
' Comments: Browse a query and display its fields in the Immediate
' Window using DAO

Const cstrQueryName = "Basics: Top 10 Most Profitable Companies"
Dim dbs As DAO.Database
Dim rst As DAO.Recordset

' Open pointer to current database
Set dbs = CurrentDb

' Open recordset on saved query
Set rst = dbs.OpenRecordset(cstrQueryName)

' Display data from one record and move to the next record until
' finished
Do While Not rst.EOF
Debug.Print "Company: " & rst![Company] & " Sales: " & rst![Sales] & _
" Sales: " & rst![Profits]
rst.MoveNext
Loop

rst.Close
dbs.Close
End Sub

This example prints the company name and sales in the Immediate Window.
Assigning parameters in queries
Earlier we showed how to use parameters in queries. If you want to run a query that has parameters from
within your code, you must specify the parameters in your code. Otherwise, the user is prompted for the
parameter value when the query is executed.
To pass a parameter value to a query, you need to create a QueryDef and reference its parameters
collection. From there you can specify each of the query's parameters. When you are finished, you can
create a recordset from it (if it is a Select query) or execute it if it is an Action query. To learn more
about QueryDefs and the parameters collection, refer to the online Help section under QueryDefs.
VB

Public Sub RunParameterQuery_DAO(pstrState As String)
' Comments: Runs a query that contains parameters
' Input: pstrState Name of state to select records

Const cstrQueryName As String = "Basics: Parameters"
Dim dbs As DAO.Database
Dim qdf As DAO.QueryDef
Dim rst As DAO.Recordset

Set dbs = CurrentDb()
Set qdf = dbs.QueryDefs(cstrQueryName)
qdf.Parameters("State Abbreviation") = pstrState

' Open recordset on the query
Set rst = qdf.OpenRecordset()
Do While Not rst.EOF
Debug.Print ("ID: " & rst![ID] & " State: " & rst![State])
rst.MoveNext
Loop

rst.Close
qdf.Close
dbs.Close
End Sub

Using a SQL string to open a Recordset
Often it is not possible to know a query's specifications in advance. In these situations, the query needs to
be programmatically created. This is done be creating a SQL string containing the query to run. The
example below shows a simple case, but one can easily create a more complex example where the query
string (strSQL) is based on other situations (field values, user selections, and so on.).
VB

Public Sub RecordsetFromSQL_DAO()
' Comments: Browse the results of a SQL string and display it in the
' Immediate Window

Dim dbs As DAO.Database
Dim rst As DAO.Recordset
Dim strSQL As String

strSQL = "SELECT Left([Company],1) AS Letter, " & _
"Count(Company) AS [Count], " & _
"Avg(Sales) AS AvgOfSales, Avg(Profits) AS AvgOfProfits " & _
"FROM Fortune100 " & _
"GROUP BY Left([Company],1)"

' Open pointer to current database
Set dbs = CurrentDb()

' Create recordset based on SQL
Set rst = dbs.OpenRecordset(strSQL)

Do While Not rst.EOF
Debug.Print "Company Letter: " & rst![Letter] & " & _
" Sales: " & rst![AvgOfSales] & " & _
"Profits: " & rst![AvgOfProfits]
rst.MoveNext
Loop

rst.Close
dbs.Close

End Sub


Running your own query string is identical to the previous examples. Just base a RecordSet on the query
string.
Running a stored Action query
To run a saved Action query, use the query Execute command. The simple procedure below lets you
easily run a saved query. Just pass the name of the query and it is performed.
VB

Public Sub RunActionQuery_DAO(pstrQueryName As String)
' Comments: Sample code of running a stored (action) query
' Input: pstrQueryName Name of saved query to run

DoCmd.SetWarnings False
CurrentDb.Execute pstrQueryName
DoCmd.SetWarnings True

End Sub

This procedure sets up a database variable referencing the current database, and creates
a QueryDef based on the query name. The Warning message is temporarily turned off before executing
the query and reset afterwards. DoEvents and DBEngine.Idle commands are used to make sure that the
Jet Engine has completed its tasks and releases its locks.
Creating a table with a Make Table Query SQL string
A Make Table query is an Action query and can be run with the Action Query example shown earlier. The
only wrinkle here is to make sure that the new table is deleted prior to the Make Table query's execution.
Also, the example below shows another way to execute a query without using a QueryDef.
VB

Public Sub MakeTableFromSQL_DAO()
' Comments: Sample code running an action query created in a SQL string
' Includes simple error trapping to handle problems creating
' table

Const cstrNewTableName As String = "Fortune100 LetterSummary"
Dim strSQL As String
Dim strError As String

' SQL string to create a new table
strSQL = "SELECT Left([Company],1) AS Letter, " & _
"Count(Company) AS [Count], " & _
"Avg(Sales) AS AvgOfSales, Avg(Profits) AS AvgOfProfits " & _
"INTO [" & cstrNewTableName & "] " & _
"FROM Fortune100 " & _
"GROUP BY Left([Company],1)"

' Delete table if it exists
On Error Resume Next
DoCmd.DeleteObject acTable, cstrNewTableName

Err.Clear

' Execute (run) the query
CurrentDb.Execute strSQL
If Err.Number <> 0 Then
strError = Err.Description
End If

On Error GoTo 0

If strError = "" Then
MsgBox "Table: [" & cstrNewTableName & "] created"
DoCmd.OpenTable cstrNewTableName
Else
MsgBox "Error creating table: " & strError
End If

End Sub

Additional code is provided in the sample database with this article.
Additional Resources from Microsoft
For more information about Access 2007, see the following resources:
Access Developer Portal
Access 2007 Resource Center
Microsoft Office Developer Center
About the Author
Luke Chung founded FMS in 1986 to provide custom database solutions. He has directed the companys
product development and consulting services efforts as the database industry evolved. In addition to
being a primary author and designer of many FMS commercial products, Luke has personally provided
consulting services to a wide range of clients. A recognized database expert and highly regarded authority
in the Microsoft Access developer community, Luke was featured by Microsoft as an Access Hero during
the Access 10-year anniversary celebration. Luke is a popular speaker at conferences in the US and
Europe, and has published many articles in industry magazines. He is a past president of the Washington,
DC chapter of the Entrepreneurs Organization (EO Network), serves on the Fairfax County School
Superintendent's Community Advisory Council, and is a graduate of Harvard University with Bachelor and
Master Degrees in Engineering and Applied Sciences.
Additional Resources from FMS, Inc.
FMS offers a variety of other resources to help you maximize the value of your Microsoft Access data.
Document and Improve Queries with Total Access Analyzer
Total Access Analyzer documents your entire database to provide detailed information on each object,
where it's used, and recommendations to fix or improve them. Part of the analysis includes Queries. Here
are some examples of the results:
Unused Query Detection Afraid to delete queries because you're not sure they're used? Total
Access Analyzer can help with that.
Query Dictionary Report showing detailed information on each query
Query Cross-Reference Report showing where each query is used
Data Flow Diagram showing how data flows from tables to queries to queries/forms/reports
Duplicate SQL in Queries and Form/Report record sources and row sources
Advanced Numerical Analysis with Total Access Statistics
If you want to extend the power of Access queries with more advanced numerical analysis, learn more
about the Total Access Statistics program from FMS. It is the leadingMicrosoft Access statistical analysis
program. It analyzes your Access table, linked table, or query in an MDB, ACCDB or ADP, and puts its
results in tables. Generate percentiles, frequency distributions, regressions, correlations, rankings, data
normalization, advanced crosstabs, t-Tests, ANOVA, non-parametrics, probabilities, and so on. Interactive
Wizard and VBA programmatic interfaces are included with a runtime distribution library. Adding
advanced numerical analysis couldn't be easier! Get more information including a free trial version.
Recordset Builder in Total Visual CodeTools
If you want to simplify the creation and maintenance of Access/VBA code, learn about the Total Visual
CodeTools program from FMS. It helps VB6/VBA developers create new code, clean up existing code, and
deliver more robust solutions. Two of its builders related to queries are:
The Recordset Builder lets you point to a database, select a table or query/view, select all or some
of the fields, and automatically generate code to browse, edit, or add records to it. Choose
whether you want it to use ADO or DAO, and whether you reference the current database or an
external one.
The Long Text/SQL Builder converts SQL from a query into a quoted string that you can add to
your VBA Modules, while handling quotes and smart line continuations so parts like FROM,
GROUP BY, WHERE, and so on start new lines.
Get more information including a free 30 day trial version.
Database Maintenance and Macro Scheduler
As you add more data to your database, make sure that you compact it regularly for optimal results and
that you make backups. Additionally, check for any tasks that need to run repeatedly, such as a particular
set of queries, data downloads, exports, or batch of reports that you must print. You can automate these
with a macro or some code. To launch this on a regular schedule, use Total Visual Agent from FMS. Total
Visual Agent is a Microsoft Access scheduler to run macros, compact, and other database chores on an
hourly, daily, weekly, monthly, or one time event. Easily manage an unlimited number of databases across
your network. Get more information including a free trial version.

Performance Tips To Speed Up
Your Access 2007 Database
Office 2007
This content is outdated and is no longer being maintained. It is provided as a courtesy for individuals
who are still using these technologies. This page may contain URLs that were valid when originally
published, but now link to sites or pages that no longer exist.
Summary: This article outlines specific performance tips that you can use to improve the speed of your
Microsoft Office Access 2007 applications. Some of the tips might contradict other tips, but they are
offered because bottlenecks differ in each situation. Some tips might make things run faster on your
system, while others degrade performance. You should evaluate each tip as it applies to your specific
application running on your specific hardware. Good luck! (21 printed pages)
Dan Haught, Executive Vice President of FMS, Inc., and Luke Chung, President of FMS, Inc.
August 2009
Applies to: 2007 Microsoft Office System, Microsoft Office Access 2007
Contents
Table Performance Tips
Linked Databases and Linked Tables
SQL Server and ODBC Performance Tips
Database Performance Tips
Query Performance Tips
Form Performance Tips
Report Performance Tips
Macros Tips
Access Module/VBA Performance Tips
Data Access Objects (DAO) Programming Tips
Jet Engine Tuning Tips
Access Startup Tips
Multiuser Performance Tips
Computer Performance Tips
Windows Performance Tips
Conclusion
Additional Resources from Microsoft
About the Authors
Table Performance Tips
Normalize Your Tables
Normalize your data so the same data is stored only in one place. Remember that time is also a
dimension and you might need to store historic data as well. Read Data Normalization Fundamentals for
more detail.
Every Table Should Have a Primary Key
Every table in your database should have a primary key. This allows your database application to quickly
locate specific records. Additionally, you cannot create secondary indexes on a table's fields unless that
table has a Primary Key.
Primary Key Should be One Field and Numeric
The primary key should only be one field and ideally numeric and meaningless. Primary keys define the
uniqueness of each record, which can be accomplished efficiently with a single number. The easiest
method is to use an AutoNumber field in Access or an Identity column in SQL Server. The primary key is
also used in each secondary index for that table, so the smaller the better. Multi-field primary keys and
non-numeric text fields are less desirable.
That said, some tables should use text fields as primary keys because they don't change much and the
tables are relatively small. For example, a list of countries or states is a good candidate because there is no
need to create a separate number for each country or state.
Having a meaningless primary key means that the index is stable even when data changes. Otherwise,
changes in the primary key have a ripple effect through each secondary index and any other tables bound
by referential integrity.
Tables Should Participate in Relationships
Related tables with one-to-one or one-to-many relationships should implement referential integrity with
cascading deletes and/or updates to ensure orphan records are not created. With cascading deletes, the
removal of the parent record automatically deletes the child records in the corresponding table. Access
(the Jet Engine) automatically takes care of this for you without the need to write any code, which is great
for maintaining data integrity.
With a relationship established, a hidden index is created for the foreign key, so links between those
tables are faster in queries. The link is also automatically drawn for you when you put the two tables on
your query designer.
Eliminate Unnecessary Subdatasheets
By default, Access creates subdatasheet views between related tables. This is nice if you want it, but is a
huge performance hit every time you open the table. Set this property to None if you don't want it. Here
is more information on the Subdatasheet Name Property.
Choose the Optimal Data Types
Choose the best data types for your fields. By choosing the optimal data type, you can decrease both the
disk space used to store data, and the time it takes Access to retrieve, manipulate, and write data. The
general guideline is to choose the smallest data type possible to store a particular type of data.
Add Secondary Indexes as Necessary
If your tables are large and you search on a field or use it in a join, create a secondary index on the field(s).
Secondary Indexes offer performance gains on an order of magnitude.
Don't Over-Index
Just as it is important to add indexes to fields that need it, it is important to avoid indexing fields that
don't need it. Every index adds to the time it takes the database engine to update, delete and add records.
Don't Index Fields with Lots of Identical Data
Don't apply indexes to fields that contain much the same data. For example, indexing a Yes/No field is
almost always a performance degrading operation. Similarly, if you have a number field that only contains
two or three values, an index wouldn't be a good idea. To check the number of unique entries in an index,
use the AccessDistinctCount property. Compare this value to the number of records in the table and you
can quickly see if the index is doing you any good.
Keep Indexes As Small As Possible
When creating a multi-field index, index only as many fields as are absolutely necessary.
Linked Databases and Linked Tables
Keep an Open Handle to Every Linked Database
You can significantly increase the performance of opening tables, forms, and reports that are based on
linked tables by having Microsoft Access keep the linked table's database open. To do this, open
a Database variable in Visual Basic code by using the OpenDatabase method. Keep this variable open as
long as your application is running. This forces Access to keep that database open, making access to
linked tables much faster.
For more information, see Increasing the Performance of Linked Databases.
Minimize Record Navigation
Avoid record navigation wherever possible on linked tables. Only use the PageUp and PageDown
movements, and the Move last movements when absolutely necessary.
Use DataEntry Mode
If you are only going to be adding records, use the DataEntry command on the Records menu. This data
access method is more efficient for adding records because existing records are not read from the
database.
Create a Data Entry Form
If you often add new records to a linked table, consider creating an "Add Records" form and set that
form's DataEntry property to Yes. This prevents Access from attempting to retrieve all the existing
records in a linked table when you need to add new records.
Limit the Data Returned by Queries
Limit the number of fields and records returned by using filters or queries. This reduces the amount of
data that needs to be read from the linked table, thereby according faster performance.
Don't Use Domain Aggregate Functions
If a query is based on one or more linked tables, avoid using functions (built-in or user-defined) or
domain aggregate functions in the query criteria. When you use domain aggregate functions such
as DLookup), Access must fetch all records in the function's data source to execute the query.
Release Locks ASAP
To improve multi-user concurrency, assume that other users will try to edit data in the same linked tables
that you use. In other words, keep records locked only as long as is necessary.
SQL Server and ODBC Performance Tips
Link ODBC Tables
If you are going to be accessing a SQL database table, link the table permanently. This makes opening
that table much faster in subsequent attempts. This is because linked tables cache a lot of information
about the source table in your database, making the retrieval of the same structural information
unnecessary after the linked table is created.
Minimize Server Traffic
Minimize the amount of data returned from the server. Do this by structuring your queries to return only
the fields and records needed. Reduce the number of fields returned, and put constraints on the query by
using the WHERE clause.
Use Snapshots When Appropriate
Don't open Dynaset type recordset object on SQL database tables unless you need to add or edit records,
or need to see the changes made by other users. Instead, consider using Snapshot recordsets, which can
be faster to scroll through. Of course, Snapshot recordsets can take longer to open since they require a
full read of the source data.
Use Dynasets for Large Record Sets
If you need to retrieve a large number of records, use a Dynaset instead of a Snapshot. Snapshot type
recordsets must load all records from the data source before becoming available, whereas Dynasets are
available as soon as the first 20 or so records are retrieved. Also, when using a Snapshot against large
ODBC data sources, you run the risk of running out of disk space on your local computer. This is because
all data is downloaded into RAM until the amount of RAM is exhausted. Then, the database engine
creates a temporary database to store the contents of the snapshot. In a nutshell, when you open a
snapshot, you need at least as much disk space as the largest table you are opening.
Take Advantage of the Cache
Use cache memory wherever possible when using external SQL data sources. Microsoft Access forms and
reports have automatic caching mechanisms. When you use recordsets in your Visual Basic code, use
the CacheStart, CacheEnd, and FillCache methods to maximize cache effectiveness.
Don't Force Local Query Processing
Don't use query constructs that cause processing to be done by Access on the local computer. The
following query operations force the Jet database engine to perform local data processing:
Join operations between tables that are linked to different data source (that is, a join between a
SQL table and a linked Access table).
Join operations based on query that uses the DISTINCT keyword, or a query that contains
a GROUP BY clause.
Outer joins that contain syntax that is not directly supported by the remote database server.
The LIKE operator used with Text or Memo fields
Multi-level grouping and totaling operations
GROUP BY clauses that are based on a query with the DISTINCT keyword, or the GROUP
BY clause.
Crosstab queries that have more than one aggregate, or that have field, row, or column headings
that contain aggregates, or that have an ORDER BY clause
User-defined functions, or functions that are not supported by the remote server
Complex combinations of INNER JOIN, LEFT JOIN, or RIGHT JOIN operations in nested queries.
Use FailOnError for Bulk Updates
If you use bulk update queries, optimize performance on the remote server by setting
the FailOnError property of the Querydef object, or query to Yes.
Use ODBCDirect
ODBCDirect gives you almost direct access to server data through ODBC. In many cases, it is a faster and
more flexible way to hit server data than that traditional Jet/Linked table technique.
Database Performance Tips
Split Your Database into Two Databases
You can improve performance and application maintainability by splitting your Access database into two
databases. The "application" part of your database holds all objects except tables, and is linked to the
"data" part that contains the actual tables. For more information, read Splitting Microsoft Access
Databases to Improve Performance and Simplify Maintainability.
Use a Current Workgroup Information File
If you are using a workgroup information file (SYSTEM.MDA) created with a previous version of MS
Access, convert it to the current version of Access for optimum performance.
Use the Access Performance Analyzer
Microsoft Access has a useful performance tool built right into the product. From the Tools menu,
select Analyze, Performance. Use the Performance Analyzer to select all objects or specific objects, and
then run an analysis that looks for potential problems. The Performance Analyzer does not find all of the
items that Total Access Analyzerdoes, but it does offer some tips.
Run the Access Table Analyzer
The Access Table Analyzer makes it easy to properly normalize the data in your tables by breaking tables
with repeated or improperly structured data into two or more tables. This tool is available from the Access
Tools, Analyze menu
Reduce the Size of Graphics in your Access 2007 Databases
If you embed graphics on your forms or reports, Access 2007 can store them much more efficiently.
Access 2007 can convert graphics into much smaller PNG formats to significantly reduce the size of your
databases. This does not affect graphics already on your forms and reports but helps if you add new
graphics or replace existing ones. To activate this feature, change an Access setting. From the Microsoft
Office button, choose Access Options, Current Database. At the bottom of the Application
Optionssection, set the Picture Property Storage Format to Preserve source image format (smaller file
size).
Compact Your Database Often To Reclaim Space
Compacting your database reclaims unused space and makes almost all operations faster. You should do
this on a regular basis. Also, be sure to compact anytime you import or delete objects in your database, or
compile and save VBA code.
Learn more about Total Visual Agent for a system administrative tool to schedule compact and backup
your Access databases on a regular schedule.
Make It Look Faster
If you have exhausted all other performance optimization techniques, consider making your application
"look" faster. Do this by displaying status messages and progress meters as your application loads forms,
runs queries, and performs any other operation that might take a bit of time. While this doesn't make
your application run faster, it appears to run faster.
Query Performance Tips
Compact Often to Update Statistics
Compact the database often. When you compact the database, you reorganize records so that they are
stored in adjacent spaces, making retrieval faster. Additionally, compacting a database updates its data
statistics, which can be used by a query to run faster. You can force a query to recompile (which in turn
causes it to use the latest statistics) by opening it in design view, saving it, and then running it.
You might want to defragment your disk by using a program such as the Disk Defragmenter that is part of
Windows before you compact your database. This leaves contiguous free disk space immediately after the
database file. In theory, this makes future additions to the database occur faster. You might want to
experiment with this on your system.
Index Query Criteria Fields and Join Fields
Index any fields in the query that are used to set criteria. Index the fields on both sides of a join.
Alternatively, you can create a relationship between joined fields, in which case an index is automatically
created.
Search Access Help for Index.
Use Identical or Compatible Datatype in Join Fields
Fields that are joined in a query should have the same data type, or compatible data types. For example,
the Long Integer data type is compatible with the AutoNumberdata type.
Limit Fields Returned by a Query
Where possible, limit the number of fields returned by a query. This results in faster performance and
reduced resource usage.
Avoid Calculated Fields and IIF Functions
Avoid calculated fields, or fields that use expressions in subqueries. Pay special care to avoid the use of
immediate If (IIF) functions in sub-queries.
Don't Use Non-Indexed Fields for Criteria
Avoid using non-indexed fields or calculated fields for criteria restrictions.
Index Sort Fields
Index any fields you use for sorting. Be careful not to over-index.
Use Temporary Tables to Eliminate Running the Same Queries Over and Over
If you are processing data that is used multiple times (on multiple reports for example), it might be faster
to store intermediate results in temporary tables rather than running a series of Select queries many
times. Create a temporary table to store your results. Empty the table and fill it with your data by using an
Append query. You can then use that table for multiple reports and forms.
Avoid Domain Aggregate Functions on Foreign Tables
Do not use domain aggregate functions (DLookup for example) in queries to access data from a table
that is not in the query. Link to the table and set criteria accordingly, or create a separate aggregate
(totals) query.
Use Fixed Column Headings in Crosstab Queries
Wherever possible, use fixed column headings in your crosstab queries with the PIVOT syntax.
Use BETWEEN Rather than >= and <=
Between lets the search engine look for values in one evaluation rather than two.
Use Count (*) To Count Records
If you use the Count function to calculate the number of records returned by a query, use the
syntax Count(*) instead of Count([fieldname]). The Count(*) form is faster because it doesn't have
to check for Null values in the specified field and won't skip records that are null.
Compile Each Query before Delivering Your Application
When you compact your database, its data statistics are updated. When you then run a query, these
updated statistics are compiled in the query's execution plan. This sequence of events results in the fastest
possible query. Before you deliver your application, compact the database, and then force each query to
be recompiled. You can force a query to recompile (which in turn causes it to use the latest statistics) by
opening it in design view, saving it, and then running it.
Take Advantage of Rushmore Query Optimization
Microsoft Jet uses Rushmore query optimization whenever possible. Rushmore is applied to queries run
on native Access data, and on linked FoxPro and dBASE tables. Other linked data types do not support
Rushmore optimization. To ensure that Rushmore optimizations are used, create indexes on all fields that
are used to restrict a query's output. If you have queries that don't contain indexes on fields used in the
query's restriction clause, Rushmore is not used.
Link on Primary Key Indexes Whenever Possible
To make queries run faster, you should have indexes on all fields in the query that join, restrict, or sort the
data. Whenever possible, link on Primary Key fields instead of other fields. Indexes are most critical on
tables with large numbers of records, so you might not see a difference on small tables. You also don't
need to add secondary indexes on fields that are part of referential integrity.
Experiment with One-to-Many Restrictions
If you have a one-to-many join in a query with a restriction, try moving the restriction to the other side of
the join. For example, if the restriction is on the many side, move it to the one side. Compare performance
results for both versions, and choose the fastest one.
De-Normalize If Necessary
Although you should strive to normalize your data for best performance and future flexibility, consider
denormalizing some of your data if you frequently run queries with joins that would benefit from such
data restructuring.
Experiment with Sub Queries Instead of Joins
If you have a query with a join that is not performing adequately, consider replacing the join with a sub
query. In some cases, the sub query might cause the overall query operation to run faster.
Limit the Number of Fields Returned by Each Query
Where possible, queries should use a Where clause to constrain, or limit, the number of records returned.
This results in faster performance and reduced resource usage.
Form Performance Tips
Save the SQL of the Form RecordSource as a Query
We've seen situations where a saved query loads significantly faster than the same SQL string stored as
the RecordSource of a form. Somehow, saved queries are optimized more than the SQL string behind the
report.
Close Unused Forms
Close forms that aren't being used. Every form that is open uses memory that could be used by other
parts of your applications.
Open Forms Hidden
Consider opening your application's most commonly used forms when your application starts. Set
their Visible properties to False, and then make them Visible as needed. This frontloads some
performance hits to the application load event, making forms load faster when needed.
Use the DataEntry Property of a Form
If a form's record source (the table or tables accessed by the form's RecordSource property) contain a
large number of records, and the form is primarily used to add new records, set the DataEntry property
of the form to Yes. This precludes Access from having to retrieve existing records when the form loads.
Don't Sort a Form's Recordset
Avoid sorting records in a form's underlying record source unless a particular presentation order is
absolutely necessary for the form. This makes the form load faster.
Base Forms on Queries-Minimize Fields Returned
Base forms and subforms on queries instead of tables. Then, you can use the query to restrict the number
of fields returned, making the form load faster.
Use Lightweight Forms
Consider replacing Visual Basic code in a form's module with calls to standard modules, or with hyperlink
objects. Then set the form's HasModule property to False. This turns the form into a Lightweight form,
making it load faster. Search Access online help for "Lightweight Forms" for more information. In Access
2007, you can use embedded macros for simple operations.
Index Fields Used to Link SubForms to a Form
Index all fields in the subform that are linked to the main form. Also index all fields in the subform that are
used for criteria.
Set Editing Properties on SubForms
Set the subform's AllowEdits, AllowAdditions, and AllowDeletions properties to No if the records in the
subform aren't going to be edited. Or set the RecordsetTypeproperty of the subform to Snapshot.
Reduce the Number of Fields in ListBox and ComboBox Row Sources
In the RowSource property of list box and combo box controls, include only the fields that are necessary.
Set AutoExpand on ComboBoxes to No
Set the AutoExpand property of combo boxes to No if you don't need the "fill in as you type" feature.
First Field of an AutoExpand ComboBox Should Be Text
In a combo box that has the AutoExpand property set to Yes, the first displayed field should be
a Text data type instead of a Number data type. In order to find matches, Access needs to convert
numeric values to text. If the data type is Text, this conversion can be skipped.
Optimize Bound ComboBoxes
If the bound field in a lookup combobox is not the displayed field, don't use expressions for the bound
field or the displayed field, don't use restrictions (the WHERE clause) in the row source, and use single-
table row sources wherever possible.
Move Linked Data Local for ComboBox and ListBox Controls
If the data that fills a list box or combo box does not change often, and that data comes from a linked
table, consider moving that data's table into the local database. This can be a huge performance boost,
especially if the linked table is located on a network drive.
Group Controls on Multiple Pages
Consider grouping controls on multiple pages. When the form loads, prepare only the controls on the
form's first page. Defer operations on other page's controls, such as setting the record source until the
user moves to that page. This makes the form load faster.
For more information, read Late Binding of Subforms on Tab Controls
Close Forms That Contain Unbound OLE Objects
Close forms that contain unbound OLE Objects when they are not in use. When you activate an unbound
OLE objects, the memory used in that operation is not released until the form is closed.
Convert Subforms to Listbox or Combobox Controls
Where possible, convert subforms to listbox or combobox controls. It is far quicker to load a control than
it is to load an additional form as a subform.
Move Form Module Code to a Standard Module
You can reduce a form's load time by moving its code from the form module to a standard module. When
the form loads, the form's module doesn't need to be loaded. Of course, the standard module needs to
be loaded at some point, but once a standard module is loaded, it stays in memory until you close the
database.
Avoid Unnecessary Property Assignments
Set only the properties that absolutely need to be set. Properties assignments can be relatively expensive
in terms of performance. Review your form's startup code to ensure that you are not setting any form or
control properties that don't need to be set.
Use the Requery Method Instead of the Requery Action
Use the Requery method instead of the Requery action. The method is significantly faster than the
action.
Give Visual Feedback
Give the user some visual feedback during long operations. Consider using status meters to display the
progress of a task. At a minimum, use the Hourglass cursor along with a status message.
Keep Forms Lightweight with Hyperlinks
Hyperlinks in label controls make it easy to open another Access object. So instead of placing command
buttons on your forms to do common operations, investigate the possibility of using a label control with
the Hyperlink properties. This approach eliminates the need for a command button, and its associated
event code.
Split Forms Into Multiple Pages
Consider using multi-page forms, separated by the page-break character. This allows you to present only
the controls needed, and can reduce form-load time. For example, if your form has 10 combobox controls
that take a long time to fill, split the form into multiple pages by using the PageBreak control. Then, pick
the 5 combobox controls the user is most likely to use and place them on the first page. Place the
remaining controls on the second page. Load time for the form should be substantially reduced, especially
if the queries filling those combo box controls are complex.
Minimize the Number of Controls
Minimize the number of controls on your form. Loading controls is the biggest performance hit when
loading a form.
Avoid Overlapping Controls
Avoid overlapping controls. It takes Access more time to render and draw controls that overlap each other
than it does non-overlapping controls.
Use Graphics Sparingly
Use bitmap and other graphic objects sparingly as they can take more time to load and display than other
controls.
Use the Image Control
Use the Image control instead of unbound object frames to display bitmaps. The Image control is a faster
and more efficient control type for graphic images.
Report Performance Tips
Save the SQL of the Report RecordSource as a Query
We've seen situations where a saved query loads significantly faster than the same SQL string stored as
the RecordSource of a report. Somehow, saved queries are optimized more than the SQL string behind
the report.
Don't Sort Report Queries
Don't base reports on queries that use an ORDER BY clause. Access reports use their Sorting and
Grouping settings to sort and group records; the sort order of the underlying record set is ignored.
Avoid Expressions and Functions in Sorting and Grouping
Try to avoid reports that sort or group on expressions or functions.
Index Fields Used In Sorting and Grouping
Index any fields that are used for sorting or grouping.
Base Reports on Queries-Minimize Fields Returned
Base reports and subreports on queries instead of tables. By using a query, you can restrict the number of
fields returned to the absolute minimum number, making data retrieval faster.
Index Fields Used to Link Subreports
Index all the fields used to link a subreport to a report.
Index Subreport Fields Used for Criteria
Index all subreport fields used for criteria. This will cause the subreport to link its records faster. Of course,
remember that over-indexing can cause performance bottlenecks when editing, adding and deleting data.
Avoid Domain Aggregate Functions in a Report's RecordSource
Do not use domain aggregate functions (such as DLookup) in a report's Recordsource property. This can
have a significant performance impact on how long it takes the report to open and display pages.
Use the NoData Event
Use the report's NoData event to identify empty reports. You can then display a message and close the
report. This is easier than running a separate process to see if data exists for the report.
Avoid Unnecessary Property Assignments
Set only the properties that absolutely need to be set. Properties assignments can be relatively expensive
in terms of performance. Review your form's startup code to ensure that you are not setting any form or
control properties that don't need to be set.
Eliminate Unnecessary Reports
If a sub report is based on the same query as its parent report, or the query is similar, consider removing
the sub report and placing its data in the main report. While this is not always feasible, such changes can
speed up the overall report.
Limit the Number of Controls on Reports
Minimize the number of controls on your report. Loading controls is the biggest performance hit when
loading a report.
Avoid Overlapping Controls
Avoid overlapping controls. It takes Access more time to render and draw controls that overlap each other
than it does non-overlapping controls.
Minimize Bitmap Usage
Use bitmap and other graphic objects sparingly as they can take more time to load and display than other
controls.
Use the Image Control
Use the Image control instead of unbound object frames to display bitmaps. The Image control is a faster
and more efficient control type for graphic images.
Macros Tips
Convert Macros to Visual Basic Code
Convert macros to Visual Basic code. In almost all cases, Visual Basic code runs faster than macros.
Access Module/VBA Performance Tips
Make an MDE File
If possible, make an MDE file out of your database. An MDE file cannot become decompiled, so your
Visual Basic code always runs at top speed. Additionally, since no source code is stored in the MDE file,
the database loads faster and uses less memory.
Achieve the Compiled State
Module code is saved in two states in your Access database: the source state, and the compiled state.
The source state consists of the contents of your actual modules, with full text including white space,
procedure and variable names, and comments. The compiled state is the executable version of your code.
All comments and white space have been removed, and a stream of executable instructions has been
produced so that the code is ready to run. The difference between these two states can cause your
application to run very slowly if you don't understand them.
When you run a procedure, VBA checks to see if the module that contains the procedure is compiled. If it
is, VBA simply runs the code. If it is not compiled, VBA compiles it by loading the code into memory,
performing a syntax check, and compiling it into an executable format. If all these operations succeed, it
can then run the code. The problem is that compiling code takes time, and compiling lots of code takes
lots of time.
Thus, if you want your database to run as fast as possible, you must reduce the amount of time Access
spends compiling your code to a minimum. In fact, in an ideal application, all your code should be
compiled and saved in the compiled state. So how do you go about this? Your Access database (or project
in VBA parlance) is said to be in a compiled state when all modules, including form and report modules,
are saved in both states in the database. This means that the original source code is stored, as is the
compiled version. In such a state, Access runs much faster, because it can completely bypass the
compilation process.
Getting your database into the compiled state is actually rather easy:
1. Open any module.
2. From the Debug menu, select Compile and Save All Modules.
Your database is now in the compiled state. This includes form and report modules (called class modules
using Access terminology) and standard modules. All VBA code that is called by your application is
immediately ready for execution. There is no need for compilation. This is all fine and well, but is just as
easy for your database to become decompiled. When you make certain changes to your database, it
automatically becomes decompiled, which means that the compiled state that you created by using the
previous steps no longer exists.
How to Avoid Decompilation
Any of the following actions can decompile your database:
Modify any module code.
Make changes to code-bearing objects, such as form, reports and controls, or create such code-
bearing objects.
To avoid decompilation, then, avoid those actions. Its not as bad as it seems. After all, your database does
not need to be in a compiled state while you are doing development work on it; it only really requires the
performance benefits of the compiled state when it is actually running on your user's workstations.
Therefore, if you follow these guidelines, you can enjoy peak performance from your module code:
During development, do not use Compile All Modules. It is a waste of time because the first time
that you make any changes to the module, it will decompile, or reverse the effect of Compile All
Modules. Use the Compile Loaded Modules option instead; it compiles only the modules that are
called by the modules that you have open. This is a much quicker operation, and results in the
same syntax checking that Compile All Modules does.
When you are ready to deliver your database for testing or live use, put it into the compiled state
by using the steps outlined above.
Decompile to Eliminate Old VBA Compiled States
The Compact feature compacts only the data, not the code portion of an Access database. To remove old
code from the database, shrink the size of your database, and avoid the infamous Bad DLL Calling
Convention error, use the /decompile command occasionally. For more information, see Decompile Your
Microsoft Access Database to Fix the Bad DLL Calling Convention Error.
ActiveX Controls Should Have References
If you are using an ActiveX control, your database should have a Reference to the ActiveX (OCX) file. This
allows you to use early binding to bind variables to the control's objects, making it load and run faster. In
most cases, this is handled for you; when you insert an ActiveX control into a form or report, Access
automatically creates a Reference for that control.
Use Option Explicit
Always explicitly declare variables. Ensure this happens in every module in your application by using
the Option Explicit phrase at the top of each module.
Choose the Most Efficient Variable Type
Use the most efficient variable type possible when declaring variables. For example, don't use a Long
Integer when an Integer will do. Avoid Variant types because they can be inefficient and slow.
Use Early Binding
Avoid using the Object data type. Instead use the specific data type for the object you are working with.
This allows Visual Basic to employ "early binding," which can be substantially faster in many cases.
Assign Things to Object Variables
If you are going to refer to a property, control, object or data access object (DAO) more than once, assign
it to an object variable.
Use the Me Keyword
Use the Me keyword instead of the Form!FormName form to refer to the form of a form's module.
Avoid the Immediate If Function if Parts Run Other Code
Use the IIf (immediate if) statement sparingly. IIf() does not employ "short-circuit" evaluation. This means
that both sides of the expression are always evaluated, which might not be what you want since intuitively
it looks like only the criteria satisfying side would run.
Use Dynamic Arrays
Instead of fixed arrays, use dynamic arrays with the Erase and ReDim statements to make better use of
memory.
Take Advantage of Demand Loading
Organize and structure your modules to take advantage of Visual Basic's demand loading architecture.
When a procedure is loaded from a module, the entire module is loaded into memory. By placing related
procedures in the same module, you can reduce the number of loads the Visual Basic has to make.
Eliminate Dead Code
Eliminate unused procedures and unused variables. These elements use memory unnecessarily, and slow
program load and execution. The FMS Total Access Analyzerprogram finds unused procedures, variables,
and constants, and variables assigned but not used.
Use Constants Instead of Variables
If you are using data that is not going to change, put it in a constant instead of a variable. This allows
Visual Basic to compile the value into the constant when the module is compiled, making the execution of
that code faster.
Avoid Infinite Recursion
Avoid Infinite Recursion. Don't have code that can call itself without having some type of short-circuit
mechanism. This can lead to "Out of Stack Space" errors.
Declare String Data Intelligently
Visual Basic allocates stack and heap memory differently according to the type of strings you create. By
understanding how this works, you can write more efficient string code. String variables in procedures
that are non-static use space on the computer's stack. Use the following information to write code that
minimizes stack memory usage.
Local fixed-length strings less than or equal to 64 characters use 2 bytes for each character in the
string. They don't use heap memory.
Local fixed-length strings longer than 64 characters use 4 bytes of stack memory for a pointer to
the variable in heap memory and 2 bytes of heap memory for each character in
the string.Local variable-length strings use 4 bytes of stack memory for a pointer to the variable
in heap memory, and a variable amount of heap memory according to the length of the string.
If your code used a large number of fixed-length strings of 64 characters or less, you can reduce
stack usage by changing the strings to local variable-length strings or making them static fixed-
length strings.
Minimize OLE References
Every time you reference a Visual Basic object, method or property, you are initiating one or more calls
the OLE's Idispatch interface. Each one of these calls takes time. Minimizing the number of such calls is
one of the best ways to make your code run faster. You can minimize OLE references by doing the
following:
Use object variables instead of directly referring to objects
Use the With statement and the For Each construct to minimize object references.
Move references to properties and methods outside of loops.
When you refer to a member of collection, do so with the object's index number. Referring to a
collections member with a name or expression introduces extra work, and therefore, more time.
Turn Off Screen Painting
Turn off screen painting during repetitive operations that update the screen. Consider using
the Application.Echo property to turn off screen painting. Depending on the type of video card in your
computer, this can have moderate to dramatic effects on performance.
Don't Write Code When a Query Would Be Better
We've seen pages of VBA code written by using various recordset operations when one or a few queries
would do the trick. Queries are not only faster and easier to optimize, they are easier to understand and
maintain. If you're not familiar with how to use Select, Update, Delete, and Append queries in Access or
SQL Server, learn them.
Close Your Database a Couple of Times a Day
VBA dynamically loads code into memory as needed on a module level. If a function is called or a variable
is used, the entire module containing that function or variable is loaded into memory. As you are
developing your application, you keep loading code into memory. Visual Basic for Applications does not
support dynamic unloading of these modules. Because of this, RAM will begin to fill up. To boost
development performance (i.e., to decrease the amount of time you spend as a developer working on
your application), you might want to close the database periodically to unload the modules. Note that you
do not have to close Access itself, just the database itself. However, if you have library database code
loaded, you should exit Access also.
It is especially important to close your database after a Compile All Modules command. The Compile All
Modules command pulls all of your code into memory. Closing and reopening the application will unload
the code and enable you to develop faster because of the additional free memory.
If you are developing your application in a single-user environment, you can improve your development
performance by opening the application exclusively. This allows Visual Basic for Applications to save and
compile faster by eliminating multiple-user save situations.
ActiveX Controls Should Have References
If you are using an ActiveX control, your database should have a Reference to the ActiveX (OCX) file. This
allows you to use early binding to bind variables to the control's objects, making it load and run faster. In
most cases, this is handled for you; when you insert an ActiveX control into a form or report, Access
automatically creates a Reference for that control.
Don't Use Expressions to Determine Loop Boundaries
If you use loop constructs in your VBA code such as For...Next, Do...While, and so on, don't force VBA to
evaluate the boundaries of the loop each time. For example, take a look at the following code example.
VB
For intCounter = 0 To Forms.Count - 1
...
Next intCounter
Instead of writing the code that way, write it more like the following example.
VB
intCount = Forms.Count - 1
For intCounter = 0 To intCount
...
Next intCounter
In the second example, VBA only has to determine the value of Forms.Count once. In the first, example,
the value must be determined for each iteration of the loop.
Data Access Objects (DAO) Programming Tips
Use Seek Instead of Find
Use Seek instead of Find... whenever possible. It uses indexes more efficiently and is the fastest data
access method.
Search Access Help for Seek method.
Use Bookmarks for Record Navigation
Whenever possible, use bookmarks to move among records instead of using the FindNext method. The
Jet engine can navigate to bookmark values quicker than doing the sequential reads required
by FindNext.
Use Indexed Fields for FindRecord and FindNext
If you can't use the Seek method and must use the FindRecord or FindNext methods, use them on
indexed fields. These methods are much more efficient when used on a field that is indexed.
Search Access Help for Find methods.
Don't Use Transactions Unless Necessary
Microsoft Access lets you wrap table update code in transactions so you can rollback incomplete attempts
to save data. Any code operation that adds, modifies, or deletes data can be enclosed in a transaction by
using the BeginTrans...CommitTrans pair.
If you do not need to rollback your updates, you can avoid using transactions and the overhead of
maintaining a rollback log.
Jet Engine Tuning Tips
Use SHOWPLAN to See Query Execution Plans
Microsoft Jet implements a cost-based query optimizer in its query engine. During the compilation
process of the query, Jet determines the most effective way to execute the query. You can view this plan
by using the ShowPlan registry setting.
To use this setting, use the Registry Editor that comes with your operating system and add the following
key to the registry.
\\HKEY_LOCAL_MACHINE\SOFTWARE\MICROSOFT\Jet\3.0\Engines\Debug
Under this key, add a string data type entry named JETSHOWPLAN in all capital letters. To turn ShowPlan
on, set the value of this new entry to ON. To turn the feature off, set the value to "OFF". When the
feature is on, a text file called SHOWPLAN.OUT is created (or appended to if it already exists) in the
current directory. This file contains the query plans.
Tune Database Performance with Jet Registry Settings
Microsoft Jet lets you tune many new parameters to tweak the engine for the best possible performance.
However, this was somewhat difficult because you could only modify these settings from their defaults by
creating keys in the registry, setting them to new values, and restarting Access and/or Jet. Version 3.5 of
the Jet engine makes this process a whole lot easier. These registry keys are in the following location.
\\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Jet\3.5\Engines\Jet 3.5.
Also, you can use the DAO SetOption method to set these parameters at runtime.
Using SetOption causes Jet to apply the changes immediately-the changes are not stored permanently in
the registry, and you do not have to restart Access. If you want to get the maximum performance out of
your data operations, experiment with these settings.
For more information, search Access online help for "Customizing Windows Registry Settings for Data
Access Objects."
Threads
Experiment with the Threads setting in the registry. With this setting you can increase or decrease the
number of operating system threads available to the Jet database engine.
MaxBufferSize
Use the MaxBufferSize registry setting to tune the data buffer used by the Jet database engine.
UserCommitSync
Use the UserCommitSync registry setting to tune performance of explicit transactions.
ImplicitCommitSync
Use the ImplicitCommitSync registry setting to tune the performance of implicit transactions.
FlushTransactionTimeout
Use the FlushTransactionTimeout registry setting to tune the performance of asynchronous write
operations.
ExclusiveAsyncDelay
Use the ExclusiveAsyncDelay registry setting to tune the performance of asynchronous mode writes to
exclusively opened databases.
SharedAsyncDelay
Use the SharedAsyncDelay registry setting to tune the performance of asynchronous mode writes to
databases opened for shared access.
PageTimeout
Use the PageTimeout registry setting to change the delay for checking other user's changes to the
database.
LockDelay
Use the LockDelay registry setting to change how long Microsoft Jet waits between retries on locked
pages in a shared database.
MaxLocksPerFile
Use the MaxLocksPerFile registry setting to tune performance by setting the maximum number of locks
can be placed on a database (MDB) file. For more information, search Access online help for "Customizing
Windows Registry Settings for Data Access Objects", or read this Microsoft Knowledgebase article "File
sharing lock count exceeded" error message during large transaction processing.
RecycleLVs
Use the RecycleLVs registry setting to determine how to memo, OLE and hyperlink data pages are
recycled.
Use ISAMStats to See Engine Detail
Microsoft Jet contains an undocumented function called ISAMStats that shows various internal values.
The syntax of the function is as follows.
VB
ISAMStats ((StatNum As Long [, Reset As Boolean]) As Long
In the code, StatNum has one of the following values:
Table 1. StatNum values
Value Description
0 Number of disk reads
1 Number of disk writes
2 Number of reads from cache
3 Number of reads from read-ahead cache
4 Number of locks placed
5 Number of release lock calls
For example, to see the number of disk reads, use the following code.
VB
Dim lngReads As Long
lngReads = DBEngine.ISAMStats (0, False)
Debug.Print lngReads
To reset the number of disk reads back to 0, use the following code.
VB
lngReads = DDBEngine.ISAMStats (0, True)
Access Startup Tips
Remove Code from Your Startup Form
If you use a Startup form instead of an Autoexec macro, place the Visual Basic code for that form in the
form's module instead of a standard module. Since Access has to load your Startup form, it will
automatically load that form's module, which is generally faster than loading a standard module. This
technique gives your application the appearance that it is loading faster.
Minimize Startup Form Code
Minimize the amount of code in your Startup form. You might want to defer certain operations, such as
opening data access objects in code, or checking objects to a later time. This technique gives your
application the appearance that it is loading faster.
Don't Use ActiveX Controls on Startup Form
Don't use ActiveX controls on your Startup Form. These controls can take long to load than other
controls and will subsequently slow down the load time of your application.
Automate the Deployment of the Latest Version of Your Database
Centrally manage and simplify the automatic deployment of your Access application to each desktop.
When you update your application, you want to distribute it quickly and easily. It's also important to make
sure that each user is using the right version of Access. The FMS Total Access Startup program makes it
easy to manage your database version and Access version by letting you distribute a simple shortcut to
launch your program.
Multiuser Performance Tips
Split Your Database
Split your database into an application and data database. Place only the data database on the server,
keeping the application objects such as forms, reports and queries locally, where they can load and
execute faster.
Keep Static Data Local
Keep static data, such as lookup tables, on the local machine. Update the local tables as necessary from
the server. For example, a lookup table containing the two-letter abbreviations for American states is not
likely to change anytime soon. When such a table is used in a data entry application, it is a performance
bottleneck to retrieve that data from the server every time it is needed. Instead, copy that table to your
application's local database.
Set Options to Avoid Lock Conflicts
Avoid lock conflicts by setting the Refresh Interval, Update Retry Interval, Number of Update Retries,
and ODBC Refresh Interval Settings in the Tools, Options menu.
Tune Jet with the Registry
Investigate the registry settings available for Access and Microsoft Jet for tuning data engine parameters.
Search Access online help for "Performance" for more information on these settings
Computer Performance Tips
Increase RAM
Increase the amount of RAM on your computer. Operating systems, Office, and other programs use a lot
of memory. With the cost of memory so low, you should have at least 1 GB of RAM. With sufficient RAM,
Access can perform its queries entirely in RAM without the need to write temporary data to disk.
Avoid NTFS Compression
If you are using NTFS compression under Windows NT, move your Access application to a non-
compressed drive. NTFS adds a significant amount of overhead during disk operations, and Access
applications are very disk-intensive.
Make Sure Network Speed is Maximized
Most networks and network cards support 10/100 Mbps standards. Make sure that your machine is using
the 100 and not the 10 if you're accessing data across your network.
Keep Enough Local Disk Space Free
Access requires a fair amount of disk space to operate, especially with large databases. Operations such as
running large action queries, adding lots of data, importing data, and compiling and saving module code
can use a lot of additional space on a temporary basis. Additionally, transactions, and compacting the
database can use a lot of disk space. A good rule of thumb is to have roughly 5-10 times the size of your
largest database available in free local storage.
Windows Performance Tips
Keep Databases Local
Whenever possible, keep your databases on a local drive instead of on a network drive. In almost all cases,
local disk access is faster than network access.
Install Access Locally
Do not install Microsoft Access, or its related Microsoft Office components on a network. Most
applications, including Access and Office perform better when run from local drives.
Open Databases Exclusively
If you are opening a database, and no other users need to use the database, open the database in
exclusive mode. To do this, check the Exclusive checkbox in the Open Database dialog box.
Close Unneeded Applications
Free up memory by closing applications that you aren't using. If you are using the System Agent from the
Microsoft Windows Plus Pack, consider turning it off to make more memory available for your access
application.
Optimize Your Disk
Keep your disk in optimal shape by deleting unneeded files and emptying your Recycle Bin. Use a disk
defragmenter such as the one found in the Windows 95 Accessories,System Tools menu.
Close Access Occasionally to Reclaim Leaked Memory
Almost all Windows applications "leak" memory. This is due to data structures, variables, and other
memory consumers that are not correctly released by the application. By closing Access, you allow
Windows to reclaim any leaked memory.
Install Windows Locally
The Windows operating system should be installed on a local fixed drive on your computer. Because of
the number of components involved in running Windows, a network installation of Windows causes poor
performance, especially for database applications.
Conclusion
Microsoft Access has provided an incredibly powerful tool for the rapid development of database systems.
With this power comes complexity, and complexity can sometimes hinder performance. Use the tips in
this article to get your Access applications into top shape. Here are some additional resources that might
help you to create faster Access databases and/or create them in less time.
Automated Database Analysis for Best Practices
For automated analysis of your databases, check out the FMS Total Access Analyzer program, which is
designed to examine every object in your database in detail, provide documentation to help you
understand what's in it, how the objects interact, and offer suggestions for applying Microsoft Access Best
Practices. It finds hundreds of types of errors, suggestions, and performance tips specific to your database
objects.
Simplify Writing, Taking Over, and Delivering VBA Module Code
If you want to write better VBA code and do it in less time, learn about the FMS Total Visual CodeTools
program, which works with Access, Office, and VB6. Total Visual CodeTools includes code builders, tools
to standardize existing code (indentations, variable names, adds error handling, and so on), and tools for
you to deliver more robust solutions. Learn more about the FMS Microsoft Access coding tools.
Microsoft Access Source Code Library
Why write all the code yourself? Get the FMS Total Visual SourceBook code library with 85,000+ royalty-
free lines of code you can insert into your Access, Office and VB6 projects. Written exclusively for this
professional code library, there's code you won't find anywhere else. For more information, see Microsoft
Access code.
Microsoft Access Consulting Services from FMS
FMS also offers custom software development services. If you're in over your head or just don't have time,
contact the FMS team to see how we can help you maintain, enhance, and/or migrate your Microsoft
Access applications. Here's more on the FMS Microsoft Access Consulting Services.
Additional Resources from Microsoft
For more information, see the following resources:
Crosstab query techniques
This article explains a series of tips for crosstab queries.
An example
A crosstab query is a matrix, where the column headings come from the values in a field. In the
example below, the product names appear down the left, the employee names become fields, and the
intersection shows how many of this product has been sold by this employee:

To create this query, open the Northwind sample database, create a new query, switch to SQL View
(View menu), and paste:
TRANSFORM Sum([Order Details].Quantity) AS SumOfQuantity
SELECT Products.ProductID, Products.ProductName, Sum([Order
Details].Quantity) AS Total
FROM Employees INNER JOIN (Products INNER JOIN (Orders INNER JOIN [Order
Details]
ON Orders.OrderID = [Order Details].OrderID)
ON Products.ProductID = [Order Details].ProductID)
ON Employees.EmployeeID = Orders.EmployeeID
GROUP BY Products.ProductID, Products.ProductName
PIVOT [Employees].[LastName] & ", " & [Employees].[FirstName];

Display row totals
To show the total of all the columns in the row, just add the value field again as a Row Heading.
In the example above, we used the Sum of the Quantity as the value. So, we added the Sum of
Quantity again as a Row Heading - the right-most column in the screenshot. (The total displays to the
left of the employee names.)
In Access 2007 and later, you can also show the total at the bottom of each column, by depressing the
Totals button on the ribbon. The button is on the Records group of the Home tab, and the icon is an
upper case sigma ().
Display zeros (not blanks)
Where there are no values, the column is blank. Use Nz() if you want to show zeros instead. Since
Access frequently misunderstands expressions, you should also typecast the result. Use CCur() for
Currency, CLng() for a Long (whole number), or CDbl() for a Double (fractional number.)
Type the Nz() directly into the TRANSFORM clause. For the example above, use:
TRANSFORM CLng(Nz(Sum([Order Details].Quantity),0)) AS SumOfQuantity
Handle parameters
A query can ask you to supply a value at runtime. It pops up a parameter dialog if you enter something
like this:
[What order date]
Or, it can read a value from a control on a form:
[Forms].[Form1].[StartDate]
But, parameters do not work with crosstab queries, unless you:
a) Declare the parameter, or
b) Specify the column headings.
To declare the parameter, choose Parameters on the Query menu. Access opens a dialog. Enter the
name and specify the data type. For the examples above, use the Query Parameters dialog like this:
Parameter Data Type
[What order date] Date/Time
[Forms].[Form1].[StartDate] Date/Time

[ OK ] [ Cancel ]
Declaring your parameters is always a good idea (except for an Access bug in handling parameters of
type Text), but it is not essential if you specify your column headings.
Specify column headings
Since the column headings are derived from a field, you only get fields relevant to the data. So, if your
criteria limits the query to a period when Nancy Davolio made no sales, her field will not be displayed.
If your goal is to make a report from the crosstab, the report will give errors if the field named
"Davolio, Nancy" just disappears.
To solve this, enter all the valid column headings into the Column Headings property of the crosstab
query. Steps:
1. In query design view, show the Properties box (View menu.)
2. Locate the Column Headings property. (If you don't see it, you are looking at the properties of
a field instead of the properties of the query.)
3. Type in all the possible values, separated by commas. Delimit text values with quotes, or date
values with #.
For the query above, set the Column Headings property like this (on one line):
"Buchanan, Steven", "Callahan, Laura", "Davolio, Nancy", "Dodsworth, Anne", "Fuller, Andrew", "King,
Robert", "Leverling, Janet", "Peacock, Margaret", "Suyama, Michael"
Side effects of using column headings:
Any values you do not list are excluded from the query.
The fields will appear in the order you specify, e.g. "Jan", "Feb", "Mar", ...
Where a report has a complex crosstab query as its Record Source, specifying the column headings can
speed up the design of the report enormously. If you do not specify the column headings, Access is
unable to determine the fields that will be available to the report without running the entire query.
But if you specify the Column Headings, it can read the field names without running the query.
An alternative approach is to alias the fields so the names don't change. Duane Hookom has an example
of dynamic monthly crosstab reports.
Multiple sets of values
What if you want to show multiple sets of values at each matrix point? Say the crosstab shows products
at the left, and months across the top, and you want to show both the dollar value and number of
products sold at each intersection?
One solution is to add another unjoined table to get the additional set of columns in your crosstab (a
Cartesian product.) Try this example with the old Northwind sample database:
1. Create a table with one Text field called FieldName. Mark the field as primary key. Save the
table with the name tblXtabColumns.
2. Enter two records: the values "Amt" and "Qty" (without the quotes.)
3. Create a new query, and paste in the SQL statement below:
TRANSFORM Sum(IIf([FieldName]="Qty",[Quantity],[Quantity]*[Order Details]![UnitPrice])) AS TheValue
SELECT Products.ProductName
FROM tblXtabColumns, Products INNER JOIN (Orders INNER JOIN [Order Details]
ON Orders.OrderID = [Order Details].OrderID) ON Products.ProductID = [Order Details].ProductID
WHERE (Orders.OrderDate Between #1/1/1998# And #3/31/1998#)
GROUP BY Products.ProductName
PIVOT [FieldName] & Month([OrderDate]);
The query will look like this:

It generates fields named Amt and the month number, and Qty and the month number:

You can then lay them out as you wish on a report.