PowerShell is a powerful scripting language that works well with Azure DevOps build/release pipelines and general AD/Dynamics 365 user management. When querying data from Active Directory, Enterprise customers may find that common scripts take substantially longer than is acceptable and PowerShell is considered at fault. Below we’ll walk through a common way to pull data from Active Directory, and then another way to do it at scale.

The Task: Filter Active Directory Users by AD Group and Include their Email Address in the Results

A common way to get this information is to use Get-ADGroupMember:

$UsersWithoutEmails = Get-ADGroupMember -Identity "Tiny_AD_Group"

But a problem arises when querying a group larger than 5,000 members:

$usersInGroup = Get-ADGroupMember -Identity "Small_AD_Group" 
Get-ADGroupMember : The size limit for this request was exceeded
At line:1 char:6
+ $usersInGroup = Get-ADGroupMember -Identity "Small_AD_Group"
+      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (Small_AD_Group:ADGroup) [Get-ADGroupMember], ADException
    + FullyQualifiedErrorId : ActiveDirectoryServer:8227,Microsoft.ActiveDirectory.Management.Commands.GetADGroupMember

Often, the workaround for this is to query the AD Group and then loop through the members to get additional properties:

$userGroup = Get-ADGroup -filter { name -eq "Small_AD_Group" } -properties members
$usersWithEmails = $userGroup.members | ForEach { Get-ADUser -Identity $_ -properties mail };

This does work, but there are some caveats. For one thing, it’s slow. For each AD user in the AD Group, it must do a single query to get additional properties.

Get-ADGroup -filter { name -eq "Small_AD_Group" } -properties members | Select -ExpandProperty Members | Measure;
Count      : 9269

Measure-Command -Expression {
    $userGroup = Get-ADGroup -filter { name -eq "Small_AD_Group" } -properties members
    $usersWithEmails = $userGroup.members | ForEach { Get-ADUser -Identity $_ -properties mail };
}
TotalSeconds      : 44.0692102
TotalSeconds      : 44.7685904
TotalSeconds      : 43.0848431

At 9,269 members, it takes 44 seconds to query all members of this group with their email addresses. This continues to scale upwards… with 16,000+ members, we’re at 70+ seconds:

Get-ADGroup -filter { name -eq "Medium_AD_Group" } -Properties Members | Select -ExpandProperty Members | Measure;
Count      : 16096

Measure-Command -Expression {
    $userGroup = Get-ADGroup -filter { name -eq "Medium_AD_Group" } -properties members
    $usersWithEmails = $userGroup.members | ForEach { Get-ADUser -Identity $_ -properties mail };
}
TotalSeconds      : 71.993939
TotalSeconds      : 78.6801553
TotalSeconds      : 75.251051

And again! With nearly 80,000 members, we’re at 360+ seconds (6+ minutes):

Get-ADGroup -filter { name -eq "Large_AD_Group" } -Properties Members | Select -ExpandProperty Members | Measure;
Count      : 79620

Measure-Command -Expression {
    $userGroup = Get-ADGroup -filter { name -eq "Large_AD_Group" } -properties members
    $usersWithEmails = $userGroup.members | ForEach { Get-ADUser -Identity $_ -properties mail };
}
TotalSeconds      : 386.8179244
TotalSeconds      : 367.0947574
TotalSeconds      : 363.0887966

If we were to plot these results based on averages, a representative graph would look like below. You can see how this can quickly become unfeasible, as your script may have dozens of AD Groups to query and manipulate. With an 80,000-member group taking 6 minutes, ten different groups could easily take an hour to query!

active directory

Fear not! There is an alternative method. Instead, an LDAP query can be completed as shown below:

Measure-Command -Expression {
    $usersWithEmails = Get-ADUser -LDAPFilter "(memberOf=CN=Small_AD_Group,OU=Groups,OU=Best Buy,OU=Users and Groups,DC=na,DC=Sample_Domain,DC=com)" -Properties mail; 
}
TotalSeconds      : 11.8649441
TotalSeconds      : 12.0721195
TotalSeconds      : 11.9379321

This method brought our 9,269-member group from 44 seconds down to 12! Very impressive, but does it scale?

Measure-Command -Expression {
    $usersWithEmails = Get-ADUser -LDAPFilter "(memberOf=CN=Large_AD_Group,OU=Groups,OU=Best Buy,OU=Users and Groups,DC=na,DC=Sample_Domain,DC=com)" -Properties mail; 
}
TotalSeconds      : 104.4181624
TotalSeconds      : 102.8956929
TotalSeconds      : 104.1290567

Yes, it does! Our group of 80,000 dropped from 6 minutes to about 100 seconds. If we plot these results based on averages, it looks quite a bit better:

active directory

Querying a group with about 210,000 members using an LDAP filter is done in about the same amount of time required to query just 90,000 individually.

Bonus Tip #1: If you have multiple groups, you can construct a single LDAP query that represents all the groups. This would save even more time!

Bonus Tip #2: If your AD tree has multiple branches, consider setting a basePath in the Get-ADUser command. It identifies where in AD to start searching.

Bonus Tip #3: If you’re going to query that result set multiple times, convert it to a hash using a PowerShell Filter. Using Where-Object on the array is much too slow.

Bonus Tip #4: If you want to get a distinct or unique set of users from multiple AD Groups, use Sort -Unique and not Select -Unique. It’s a magnitude faster due to how it sorts.

Be sure to subscribe to our blog for more tips and tricks!

Happy PowerShell’ing!

Avatar for Maria Valley

Maria Valley