Creating More Efficient Microsoft Active Directory-Enabled Applications

 

Microsoft Corporation

November 2001

Summary: This article discusses how to create efficient queries, determine query timing, and track expensive and inefficient searches using Microsoft Active Directory. (23 printed pages)

Application developers who want to allow the most efficient use of the Microsoft® Active Directory™ directory service in their applications will want to consider the following issues, which are discussed in this paper:

  • Creating Efficient Queries
  • Attributes
  • Determining Query Timing With the STATS Control
  • Tracking Expensive and Inefficient Searches

The Microsoft Visual C++® and Microsoft Visual Basic® code examples assume that you are familiar with the following technology, utility, APIs and process:

  • Active Directory
  • LDP Utility
  • Lightweight Directory Access Protocol (LDAP)
  • Active Directory Service Interfaces (ADSI)
  • Building directory queries

For more information about developing applications with Active Directory, visit Active Directory Developer Resources.

For more information about using the LDP utility, see Using Ldp.exe to Find Data in the Active Directory.

For more information about LDAP, ADSI, and building directory queries, visit the Active Directory Programmer's Guide in the Platform SDK.

Creating Efficient Queries

In general, Active Directory is self-tuning. However, performance gains can be achieved through careful query creation and execution.

The efficiency of a query has a lot to do with choosing the right base object for the search. You should also consider the scope of the search, the search filter, and the attribute list to be returned when the search is successful. These issues are discussed in the following sections:

  • Using the Right Base Object for a Query
  • Getting the Distinguished Name of a Naming Context
  • Connecting to the Global Catalog
  • Search Scope
  • Creating Efficient Filters and Other Tips

Using the Right Base Object for a Query

When performing queries against a directory, you generally have to specify the location in the directory where you want to begin the search. This location can be a container, or the head of one of the three partitions: domain, schema, or configuration. Performing a one-level search on a container will always be more efficient than a subtree search over an entire partition.

Here are the best practices for choosing a base object for a query, in order of precedence:

  1. Use the distinguished name of the container and perform a one-level search (when you know the name of the container where the objects you want are located).

  2. Use the distinguished name of the container and perform a subtree search (when you know the name of the top-level container where the objects you want are located).

  3. Use the distinguished name of the partition and perform a subtree search (when you know the name of the partition where the objects you want are located).

  4. Use the global catalog (when you do not know what domain the object is in, or when the object is in a domain that does not have local replicas).

    Note   After the object is found, you may need to bind to the object using its distinguished name to access some of its attributes that may not be in the global catalog.

Getting the Distinguished Name of a Naming Context

Getting the distinguished name (DN) of a directory partition, also called a naming context requires only that you read the path from the rootDSE. The following table lists the naming contexts supported in Active Directory. This table is followed by Visual C++ and Visual Basic code examples that demonstrate how to retrieve a schema naming context. To get the DN for any of the naming contexts in the table, replace "schemaNamingContext" with a naming context from the table.

Naming contexts Description
DefaultNamingContext    Root of the current domain head
RootDomainNamingContext    Forest root domain head
ConfigurationNamingContext    Configuration container
SchemaNamingContext    Schema container

Visual C++ code example

HRESULT hr;
IADs*    pObject = NULL;
LPOLESTR szPath = new OLECHAR[MAX_PATH];
VARIANT var;

hr = ADsOpenObject(L"LDAP://rootDSE", NULL, NULL,
                    ADS_SECURE_AUTHENTICATION,
                    IID_IADs, (void**)&pObject);

if (SUCCEEDED(hr))
{
   hr = pObject->Get(L"schemaNamingContext",&var);

   if (SUCCEEDED(hr))
   {
     wcscpy(szPath,L"LDAP://");
     wcscat(szPath,var.bstrVal);
   }
   VariantClear(&var);
}
if (pObject)
   pObject->Release();

Visual Basic code example

On Error Resume Next

'Bind to the rootDSE.
Set root= GetObject("LDAP://rootDSE")

If (Err.Number = 0) Then
 'Get the DN for the Schema.
 sSchema = root.Get("schemaNamingContext")
 sPath = "LDAP://" & sSchema

 WScript.echo sPath
End If

Connecting to the Global Catalog

There are several ways to connect to a global catalog. If you are using LDAP, then use port 3268 in the ldap_open or ldap_init calls. The following list of LDAP ports can be used with these calls. This list is followed by Visual C++ and Visual Basic code examples that demonstrate how to connect to a global catalog.

LDAP Ports

Standard—389

LDAP SSL—636

Global catalog—3268

Global catalog SSL—3269

Visual C++ code example

HRESULT GetGC(IDirectorySearch **ppDS)
{
HRESULT hr;
IEnumVARIANT *pEnum = NULL;
IADsContainer *pCont = NULL;
VARIANT var;
IDispatch *pDisp = NULL;
ULONG lFetch;

// Set IDirectorySearch pointer to NULL.
*ppDS = NULL;

// First, bind to the global catalog: namespace container object. 
hr = ADsOpenObject(TEXT("GC:"),
                NULL,
                NULL,
                ADS_SECURE_AUTHENTICATION, //Use Secure Authentication.
                IID_IADsContainer,
                (void**)&pCont);
if (FAILED(hr)) {
    _tprintf(TEXT("ADsOpenObject failed: 0x%x\n"), hr);
    goto cleanup;
} 
// To enumerate the contents of the global catalog container, 
// get an enumeration interface. The "real" global catalog is the
// only child of the global catalog container.
hr = ADsBuildEnumerator(pCont, &pEnum);
if (FAILED(hr)) {
    _tprintf(TEXT("ADsBuildEnumerator failed: 0x%x\n"), hr);
    goto cleanup;
} 
// Now enumerate. There is only one child of the global catalog: object.
hr = pEnum->Next(1, &var, &lFetch);
if (FAILED(hr)) {
    _tprintf(TEXT("ADsEnumerateNext failed: 0x%x\n"), hr);
    goto cleanup;
} 

// Get the IDirectorySearch pointer.
if (( hr == S_OK ) && ( lFetch == 1 ) )     
{    
    pDisp = V_DISPATCH(&var);
    hr = pDisp->QueryInterface( IID_IDirectorySearch, (void**)ppDS); 
}

cleanup:

if (pEnum)
    ADsFreeEnumerator(pEnum);
if (pCont)
    pCont->Release();
if (pDisp)
    (pDisp)->Release();
return hr;
}

Visual Basic code example

Set gc = GetObject("GC:")
For each child in gc
    Set entpr = child
Next
' Now the entpr object can be used to search the entire forest.
' The ADsPath from this object should be used when performing searches
' with ADO.

Search Scope

The scope of a query greatly impacts its performance. Three scopes can be selected for a query: base, one-level, and subtree. The following sections describe each, in order of precedence.

  • Base
    The base scope is the most efficient scope to use for retrieving a single object. When you know the Fully Qualified Domain Name (FQDN) of the object, use the base scope to return the attributes for one object.
  • One-level
    The one-level scope is the most efficient way to retrieve multiple objects. When the objects you want are located in a single known container, use the one-level scope to return the attributes for one or more objects.
  • Subtree
    The subtree scope allows you to retrieve data from multiple containers. When you want to return the attributes for one or more objects contained in multiple containers in the same hierarchy, use the subtree scope.

Creating Efficient Filters and Other Tips

Here is a list of best practices to follow when designing queries. These recommendations are explained in the following sections.

  • Use indexed attributes in the search operation
  • Search for the objectCategory attribute instead of the objectClass attribute
  • Query only the attributes that you need
  • Do not use multiple ambiguous name resolution attributes in the same filter
  • Do not perform medial searches on attributes without medial indices
  • Specify limits on your attributes
  • Avoid sorting if the result set does not need to be sorted
  • Properly place logical AND operators and logical OR Operators
  • Remove redundant clauses, logical AND operators, and logical OR operators
  • Avoid using bitwise AND operators and bitwise OR operators
  • Avoid using the logical NOT operator
  • Properly use the greater than and less than operators
  • Use paging when returning a large result set
  • Bind to an object only once
  • Query an object only once
  • Store references to objects as GUIDs
  • Avoid unnecessary SearchResultReference referral chasing

Use indexed attributes in the search operation

When creating an LDAP dialect, the query filter should have at least one indexed attribute. For example, where objectCategory is indexed and objectClass is not indexed, use the following statement. This example assumes that employeeNumber is not indexed.

(&(objectCategory=Contact)(givenName=John))

Do not use this statement:

(&(objectClass=Contact)(givenName=John))

Search for the objectCategory attribute instead of the objectClass Attribute

Searching for an indexed attribute is more efficient than searching for an unindexed attribute. The objectCategory attribute is indexed, which improves search performance. The objectClass attribute is not indexed and contains multiple values, which results in slow searches.

If you need to return every class in a query, then use (objectClass=*) as the filter.

Query only the attributes that you need

When performing a query, you can either specify the attributes that you want returned or return all attributes. To improve performance, specify only the attributes you need returned in the query.

Do not use multiple ambiguous name resolution attributes in the same filter

An ambiguous name resolution (ANR) attribute in a filter will expand to a much larger filter internally. Using multiple ANR attributes will result in a very large filter, resulting in poor performance.

Do not perform medial searches on attributes without medial indices

Place wildcards at the end of, rather than at the beginning of, the search string. For example, use cn=smi* instead of cn=*hill* or cn=*mith. The standard indexes that were introduced with Windows 2000 are only useful for substring or exact match queries. If you want to perform medial searches, then you need to create a medial index on the attribute that will be part of a filter. The creation of a medial index is described later in this document.

Specify limits on your attributes

Specifying limits in your filter constricts the search scope, which reduces the number of return values. For example, if you want to return objects that begin with the letter "s", use (cn=s*) instead of (cn=*).

Avoid sorting if the result set does not need to be sorted

Sorting consumes a great deal of processor time on the server. Avoid sorting the result set whenever possible. This is especially true for a large result set because the server cannot return any objects until a completed result set is constructed.

Properly place logical AND operators and logical OR operators

Placing logical AND operators inside a nested filter improves performance because the query processor can then take advantage of index intersections.

The AND operator in the following filter is not properly placed:

(& 
    (objectCategory=person)
    (| 
        (givenName=A*)
        (givenName=C*)
    ) 
)

The AND operator in this filter is more effective:

(|
    (&
        (objectCategory=person)
        (givenName=A*)
    )
    (&
        (objectCategory=person)
        (givenName=C*)
    )
) 

Remove redundant clauses, logical AND operators, and logical OR operators

A redundant, or nested, clause is a clause that occurs within a clause. Although the query processor can collapse redundant clauses into more optimal clauses, a filter without redundant clauses or logical operators is much more efficient because the query processor does not have to collapse these redundant clauses every time the filter is used.

Here is a filter that uses redundant clauses or logical operators. The salary and yearsEmployed filters are nested inside the surname filter.

(&
    (surname>=A)
    (&
        (salary>=10000)
        (yearsEmployed>=4)
    )
)

A more efficient filter does not use redundant clauses or logical operators:

(&
    (surName>=A)
    (salary>=10000)
    (yearsEmployed>=4)
)

Avoid using bitwise AND operators and bitwise OR operators

Using bitwise operators prevents an index from being used on an attribute. If you must use bitwise operators, use an indexed attribute in conjunction with the attribute you are performing the bitwise operation on to limit the performance impact of the query.

Avoid using the logical NOT operator

Avoid using the logical NOT operator because the query processor returns objects that you do not have access to or specific attributes that do not have a value. The query processor considers those objects and attributes as satisfying the query.

For example, if you want to find all the users that have a salary less than $10,000 and you think you may not have sufficient rights or a salary value may be missing, avoid using a filter that uses the logical NOT operator:

(&(objectCategory=person)(!salary>=10000))

Instead, define the result set more precisely:

(&(objectCategory = person)(salary <= 9999))

Properly use the greater than less than operators

Because the LDAP specification does not allow the use of the greater than (>) and less than (<) operators, consider using the greater than or equal to (>=) or less than or equal to (<=) operators by first incrementing or decrementing a value. For example, the following filter includes a less than operator:

(salary<10000)

Edit the filter to use a less than or equal to operator instead. It will perform the same operation:

(salary<=9999)

Use paging when returning a large result set

Some searches, especially subtree searches, can result in a large amount of data being returned. This will cause the client, server, and network to have to handle this data. By using paging you get the following benefits:

  • Lower network traffic
  • The client can be more responsive to the user because the client returns only one page of data rather than all of the data in the result set.
  • Fewer resources are needed by the client and server
  • The client can abandon the search before completion

For more information about paging, see the Paging in the Specifying Other Search Options topic of the Platform SDK.

Bind to an object only once

Obtain and use a single object handle for the rest of your session. Do not bind and unbind for each call. If you are using ADO or OLE DB, try to create only one connection object.

Query an object only once

If you have a software module that retrieves multiple attributes from an object, read all the attributes in the initial query. Store and use the attributes locally, as you need them. Storing the attributes locally is more efficient than retrieving each attribute from the server as needed. The exception to this is when attributes that are not always used are being retrieved. If you are retrieving attributes that are only used occasionally, then you are wasting bandwidth. In general, only retrieve the attributes that are used 80 percent of the time and retrieve the remainder as they are needed.

Store references to objects as GUIDs

After retrieving an object for the first time, store the GUID for the object and use that GUID to retrieve the object in subsequent queries. This is more efficient than using the name attribute because the name attribute of an object can change many times over the life of the object. For example, a person could change their name. Objects can also be moved to different containers or even deleted.

The following Visual C++ and Visual Basic code examples demonstrate how to retrieve an object by using the GUID of the object.

  • Visual C++ code example

    IADs *pADs;
    LPWSTR pszFilter = L"LDAP://<GUID=63560110f7e1d111a6bfaaaf842b9cfa>";
    hr = ADsGetObject(pszFilter, IID_IADs, (void**)&pADs);
    
  • Visual Basic code example

    Dim myObject as IADs
    Set myObject = GetObject("LDAP://<GUID=63560110f7e1d111a6bfaaaf842b9cfa>")
    

Avoid unnecessary SearchResultReference referral chasing

With referral chasing enabled, your code could go from domain to domain in the Active Directory tree trying to satisfy the request if the query cannot be satisfied by the initial domain. This method can be extremely time-consuming. When performing a query for objects and the domain for the objects is unknown, use the global catalog as a base for the search instead of using referral chasing. For example, the following path uses the global catalog:

GC://mydomain.com

This path uses a domain, which is less efficient:

LDAP://mydomain.com

Attributes

Attributes on an object can be indexed to improve the performance of queries. When an attribute is indexed, the index applies to each object to which the attribute is associated. You cannot place an index on an attribute and have it apply to only one class of object. To specify the index for an attribute, you must modify the searchFlags property for the attribute in the schema.

The searchFlags Property

The searchFlags property is part of the definition for a schema attribute and is composed of bit values. These bit values determine how an attribute is handled by Active Directory. The first three bits control how an attribute is indexed. By setting the first bit of the property to one, a database-wide index is created for the attribute. Setting the second bit to one creates an index on each container that holds an object that uses the attribute. This kind of index helps one-level searches, and Windows XP defines the supported sort orders for one-level VLV searches.

If the third bit is set, then the attribute is included in the filter expansion of ANR searches of the ANR attribute set only if the attribute is already normally indexed by setting the first bit to one. In the Windows Server 2003 family, if the sixth bit is set, then a substring index is created on the attribute, allowing efficient medial substring queries to be performed. Note that a substring index is significantly more expensive to create and maintain, so update speeds will be affected.

Ambiguous name resolution

Ambiguous name resolution (ANR) helps find a user object when a unique identifying value is not known. For example, if you know that the name of a person is Sam but are not sure whether Sam is a first name, last name, e-mail alias, or an abbreviation, you can use the filter, (anr=sam) to perform a search for every object where any of the attributes are part of the ANR index and begin with sam. Although ANR was designed for use with user objects, it can be used on any type of object. Therefore, you may need to indicate the class in the search as well. For example, use (&(objectClass=user)(anr=sam)).

Determining when the index has changed

When the searchFlags property is modified, a background task starts to create the index. This task may take a while to complete depending on which attribute is indexed and how many objects are using the attribute. To be certain that the index has been created, look at the event log for the following message:

An index on the attribute <index ID> (index name) was successfully created.

When one of the bit values is set to zero to turn off the index, a background task is started for removing the index. To be certain that the index has been removed, look at the event log for the following message:

Deleted unneeded index <index name> (Internal ID <index ID>).

Determining Query Timing with the Statistics Control

Query timing can be discovered by using the statistics control, which is an LDAP control supported by the Microsoft Windows® Server 2003 family and used to retrieve information about a particular LDAP search operation. The statistics control reports the following information:

  • Number of actual database operations generated by the LDAP search operation.
  • Number of entries evaluated by the optimizer.
  • Time needed by Directory Services to perform the operation.
  • Filter used.
  • Indexes used by the optimizer for filter evaluation.

Setting Up the Statistics Control

The object identifier (OID) for the statistics control is: 1.2.840.113556.1.4.970. This control is available to any application that uses LDAP. Ldp.exe is the only executable deployed by Microsoft that uses this control.

To start the STATS control

  1. Run Ldp.exe, and connect to a server.

  2. On the Options menu, click Controls.

  3. In the Object Identifier box, enter 1.2.840.113556.1.4.970.

    Figure 1. Object Identifier value

  4. Click Check in to move the value to the Active Controls box.

    Figure 2. Moving Object Identifier value to the Active Controls box

  5. Click OK.

Using the STATS control, the server returns the following information:

  • Thread Count: <thread count>
  • Core Time: <core time>
  • Call Time: <call time>
  • Subsearch ops: <sub search operations>
  • Entries Returned: <entries returned>
  • Entries Visited: <entries visited>
  • Used Filter: <filter (octet string)>
  • Used Indexes: <indexes used (octet string)>

To retrieve all of the above information, the account that issues the LDAP request should have debug privileges in its token.

Examples That Use the Statistics Control

The following examples use the statistics control to analyze some popular queries.

Example 1. Finding all groups

The following filter finds all groups:

(&
    (!
        (groupType:1.2.840.113556.1.4.803:=1)
    )
    (groupType:1.2.840.113556.1.4.804:=14)
)

Here is a sample report from the statistics control:

Used filter

( & 
    ( ! 
        (groupType & <bit_val>)
    ) 
    (groupType | <bit_val>) 
) 

Used indexes

INDEX_000902EE:22:N;

Comments

Because this filter evaluated in the traversal of an index, this filter is an optimal filter.

The format of the Used Indexes value is INDEX_x:y:z where x is a number that represents the attribute used for the index, y is the estimated number of entries in the index, and z represents one of the following index types.

Index type Description
N Normal attribute index
P PDNT index (attribute index in a container)
L Either the link or backlink index on the linked attribute table
I Index intersection (used when the result set is in a temporary table)
T Tuple index (a medial substring index)

Example 2. Finding all users with a specific account type

The following filter finds all users with a specific account type:

(samAccountType=805306368)

Here is a sample report from the statistics control:

Used filter

(samAccountType = <val>) 

Used indexes

INDEX_0009012E:38:N;

Comments

Because this filter evaluated in the traversal of an index, the filter is an optimal filter.

Example 3. Finding all groups and users

The following filter finds all groups and users (this filter combines the above two filters):

(|
    (&
        (!
            (groupType:1.2.840.113556.1.4.803:=1)
        )
        (groupType:1.2.840.113556.1.4.804:=14)
    )
    (samAccountType=805306368)
)

Here is a sample report from the statistics control:

Used filter

(| 
    (samAccountType = <val>) 
    (& 
        (!
            (groupType & <bit_val>) 
        ) 
        (groupType | <bit_val>) 
    ) 
) 

Used indexes

INDEX_000902EE:60:N;

INDEX_0009012E:38:N;

Comments

Because this filter evaluated in the traversal of two indexes, this filter is an optimal filter. One index was used for the first part of the filter (samAccountType); the other index was used for the second part (groupType).

When multiple indexes are used in processing a query, the top index definition lists the total number of entries for all the indexes. For example, the value 60 represents the estimated number of entries in both indexes.

Example 4. Finding a person

The following filter finds a person in the directory. The filter is used after you complete the Find Person dialog box:

(& 
    (|
        (mail=username)
        (anr=username)
    )
    (|
        (objectCategory=person)
        (objectCategory=group)
    )
)

Note This filter contains an ambiguous name resolution (ANR) component; as a result, it will expand (depending on the configuration of the server) to the appropriate set of filters.

Here is a sample report from the statistics control:

Used filter

(&
   (|
       (mail = <val>) 
       (displayName = <startSubstr>*) 
       (givenName = <startSubstr>*) 
       (legacyExchangeDN = <val>)
       (physicalDeliveryOfficeName = <startSubstr>*) 
       (proxyAddresses = <startSubstr>*) 
       (name = <startSubstr>*) 
       (samAccountName = <startSubstr>*) 
       (sn = <startSubstr>*) 
       (uid = <startSubstr>*) 
   ) 
   (|
       (objectCategory = <val>) 
       (objectCategory = <val>) 
   ) 
) 

Used indexes

INDEX_00150001:91:N;
INDEX_00000004:81:N;
INDEX_000900DD:71:N;
INDEX_00090001:61:N;
INDEX_000200D2:51:N;
INDEX_00000013:41:N;
INDEX_0009028F:31:N;
INDEX_0000002A:21:N;
INDEX_0002000D:11:N;
INDEX_00150003:1:N;

Comments

Because this filter used an ANR component, it was expanded to use nine additional expressions, one for each attribute that is marked as ANR: displayName, givenName, legacyExchangeDN, physicalDeliveryOfficeName, proxyAddresses, name, samAccountName, sn, and uid.

When numerous attributes are marked as ANR, filter expansion can lead to performance problems.

Example 5. Tri-state logic

The following tables show filters that are evaluated based on tri-state logic; TRUE, FALSE, or UNKNOWN.

  • If the filter represents every object inspected, then the state is set to TRUE.
  • If the filter represents none of the objects inspected, then the state is set to FALSE.
  • If the outcome cannot be determined, then the state is set to UNKNOWN.

If the filter represents a specific set of objects, then the filter is used. In the TRUE or UNKNOWN state, all objects are returned.

Filter evaluated as TRUE

Original filter Used filter Used indexes
(! (MyFilter=*) ) (TRUE) Ancestors_index:5544:N;

Comments

Assume this attribute is not defined in the schema.

In accordance to tri-state logic, this filter evaluates to TRUE.

As a result, the whole subtree is returned, which might take considerable time.

Note that the index used is Ancestors, which contains a large number of entries. If your filter evaluates in this index, consider rewriting your filter or debugging it.

Filter evaluated as FALSE

Original filter Used filter Used indexes
(MyFilter=*) (FALSE) DNT_index:1:N;

Comments

Assume this attribute is not defined in the schema.

In accordance to tri-state logic, this filter evaluates to FALSE.

Filter evaluated as UNKNOWN

Original filter Used filter Used indexes
(MyFilter=test) (<UNKNOWN>) Ancestors_index:5544:N;

Comments

Assume this attribute is not defined in the schema.

In accordance to tri-state logic, this filter evaluates to UNDEFINED.

Note that the index used is Ancestors, which contains a large number of entries. If your filter evaluates in this index, consider rewriting your filter or debugging it.

Tracking Index Use

To verify that a server is using an index to process a query, set the following registry key to the value 4 on a domain controller:

HKEY_LOCAL_MACHINE\SYSTEM\Current Control Set\Services\NTDS\Diagnostics\ 9 Internal Processing

Then perform a query on that domain controller and look in the Directory Services event log for information about the indexes, if any, used to process the query.

Here is an example of log file outputs after running the query (objectCategory=serviceConnectionPoint):

Internal event: Active Directory might use the following index to optimize 
         a query. The approximate record count for using this index is as follows. 
 
Index:
INDEX_0009030E 
Record count:
1159

Internal event: Active Directory might use the following index to optimize 
         a query. The approximate record count for using this index is as follows. 
 
Index:
PDNT_index 
Record count:
3999

Internal event: Active Directory will use the following index as the 
         optimal index for this query. 
 
Index:
INDEX_0009030E

Tracking Expensive and Inefficient Searches

Expensive searches are searches that visit a large number of entries. The efficiency of a search is measured by the number of entries returned against the number of entries visited. For example, a search that goes through 500 entries could be considered an expensive search. If the search returns 500 entries after searching through 500 entries, then you have an efficient search. An inefficient search returns five entries after searching through 500 entries.

To track searches, you can enable the diagnostic event logging for Active Directory Services. Event logging allows you to determine if you have expensive or inefficient searches.

These event log messages are logged in the Directory Services event log using the Field Engineering category. The Directory Services event log is generated every time the garbage collector runs.

The following is an example of an event log message of an inefficient search:

Windows 2000 Server log

The Search operation based at DC=MyTest,DC=microsoft,DC=com
 using the filter:
  (attr(0xd)=<substr>) 
 visited 237 entries and returned 6 entries.

Windows Server 2003 log

Internal event: A client issued a search operation with the following options.

Client:
127.0.0.1
Starting node:
 DC=MyTest,DC=microsoft,DC=com
Filter:
  (objectCategory=<val>) 
Visited entries:
237
Returned entries:
6

This search is considered an inefficient search because only six entries are returned after going through 237 entries.

Potentially, there can be numerous event log messages, so the messages are masked by using a severity level other than the default:

  • DS_EVENT_SEV_VERBOSE
    To log a message about the number of expensive and inefficient search operations performed in the last collection period, set the Field Engineering logging severity level to 4 (DS_EVENT_SEV_VERBOSE).
  • DS_EVENT_SEV_INTERNAL
    To log a message about the number of expensive and inefficient search operations performed in individual searches, set the Field Engineering logging severity level to 5 (DS_EVENT_SEV_INTERNAL). This event logs the exact filter used for each search operation that was expensive or inefficient, immediately after any expensive or inefficient search completes.

You can set the severity levels by setting the following registry key:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\ Diagnostics\15 Field Engineering

For information about how to enable diagnostic event logging for Active Directory Services, see the Microsoft Knowledge Base article Q314980 How to configure Active Directory diagnostic event logging in Windows Server Services.

To categorize search operations as expensive or inefficient, two DWORD registry keys are used:

  • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters\
    Expensive Search Results Threshold
  • HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters\
    Inefficient Search Results Threshold

These DWORD registry keys have the following default values:

  • Expensive Search Results Threshold: 10000
  • Inefficient Search Results Threshold: 1000

Using the default values, a search is considered expensive if it visits more than 10,000 entries. A search is considered inefficient if the search visits more than 1,000 entries and the returned entries are less than 10 percent of the entries that it visited.