As part of this article, we will look into various approaches for counting unique in VBA and also see what time do they take to determine the best approach on the basis of "Time Taken". I have used Charles William's MicroTimer for timing the time.
https://msdn.microsoft.com/en-us/library/aa730921%28v=office.12%29.aspx
We will see performance of these approaches with following number of records
– 100000
– 50000
– 33000
– 10000
– 1000
– 100
The Excel file related to this article can be downloaded from Article-30-Count-Unique-in-VBA. The VBA codes can be looked into Two Modules – Timer and CodeforCountUnique.
Sub TimerLog calls corresponding Procedure 10 Times and logs the result in the Excel Sheet. The Excel takes the averages of these results to arrive at the final timing.
Private Declare Function getFrequency Lib "kernel32" _
Alias "QueryPerformanceFrequency" (cyFrequency As Currency) As Long
Private Declare Function getTickCount Lib "kernel32" _
Alias "QueryPerformanceCounter" (cyTickCount As Currency) As Long
Function MicroTimer() As Double
' Returns seconds.
Dim cyTicks1 As Currency
Static cyFrequency As Currency
MicroTimer = 0
' Get frequency.
If cyFrequency = 0 Then getFrequency cyFrequency
' Get ticks.
getTickCount cyTicks1
' Seconds
If cyFrequency Then MicroTimer = cyTicks1 / cyFrequency
End Function
Sub TimerLog()
Dim Timer As Double
Dim RWs As Worksheet
Dim LastRow As Long, i As Long
Dim ColLetter As String: ColLetter = "Aj"
Set RWs = Worksheets("Results")
For i = 1 To 10
LastRow = RWs.Range(ColLetter & "13").End(xlUp).Row
Timer = MicroTimer()
Call CountUniqueTransferToArrayLooping
Cells(LastRow + 1, ColLetter) = 1000 * (MicroTimer() - Timer)
Next i
End Sub
Approach 1 – Collection Approach
Collection approach uses Collection object in VBA to count Unique.
Sub CountUniqueCollection()
Dim Ws As Worksheet
Dim Col As New Collection
Dim Cell As Range, Rng As Range
Dim UniqueCount As Long
Set Ws = Worksheets("100")
Set Rng = Ws.Range("A2:A" & Ws.Range("A" & Rows.Count).End(xlUp).Row)
For Each Cell In Rng
On Error Resume Next
Col.Add Cell, Key:=Cell
Next Cell
UniqueCount = Col.Count
Debug.Print UniqueCount
End Sub
The time taken by this Procedure is as below –
Approach 2 – Dictionary Approach – Late Binding
We have used Late Binding first.
Sub CountUniqueDict()
Dim Ws As Worksheet
Dim Dict As Object
Dim Cell As Range, Rng As Range
Dim UniqueCount As Long
Set Dict = CreateObject("Scripting.Dictionary")
Set Ws = Worksheets("100")
Set Rng = Ws.Range("A2:A" & Ws.Range("A" & Rows.Count).End(xlUp).Row)
For Each Cell In Rng
Dict(Cell.Value) = Cell.Value
Next Cell
UniqueCount = Dict.Count
Debug.Print UniqueCount
End Sub
The time taken by this Procedure is as below –
Approach 3 – Dictionary Approach – Early Binding
We have used Early Binding and as expected it does offer performance gain over late binding.
Sub CountUniqueDictEB()
Dim Ws As Worksheet
Dim Cell As Range, Rng As Range
Dim UniqueCount As Long
Dim Dict As Scripting.Dictionary
Set Dict = New Scripting.Dictionary
Set Ws = Worksheets("100000")
Set Rng = Ws.Range("A2:A" & Ws.Range("A" & Rows.Count).End(xlUp).Row)
For Each Cell In Rng
Dict(Cell.Value) = Cell.Value
Next Cell
UniqueCount = Dict.Count
Debug.Print UniqueCount
End Sub
The time taken by this Procedure is as below –
Approach 4 – Formula Approach
This approach uses following SUMPRODUCT formula to count.
SUMPRODUCT((A2:A100<>"")/COUNTIF(A2:A100,A2:A100&""))
The above formula is upto 100. The formula was updated every time before the procedure was called to take care of the range. Hence, when sheet having 100000 records was called, the formula was updated to –
SUMPRODUCT((A2:A100000<>"")/COUNTIF(A2:A100000,A2:A100000&""))
Sub CountUniqueFormula()
Dim Ws As Worksheet
Dim LastRow As Long
Dim Cell As Range, Rng As Range
Dim UniqueCount As Long
Set Ws = Worksheets("1000")
UniqueCount = Ws.[SUMPRODUCT((A2:A100<>"")/COUNTIF(A2:A100,A2:A100&""))]
Debug.Print UniqueCount
End Sub
The time taken by this Procedure is as below –
Approach 5 – Advanced Filter Approach
This formula applies advanced filter for unique records and counts unique.
Sub CountUniqueAdvFilter()
Dim Ws As Worksheet
Dim Rng As Range
Dim UniqueCount As Long
Set Ws = Worksheets("100")
Ws.Columns("A").AdvancedFilter Action:=xlFilterInPlace, Unique:=True
Set Rng = Ws.Range("A2:A" & Ws.Range("A" & Rows.Count).End(xlUp).Row).SpecialCells(xlVisible)
UniqueCount = Rng.Cells.Count
Debug.Print UniqueCount
Ws.ShowAllData
End Sub
The time taken by this Procedure is as below –
Approach 6 – Remove Duplicates Approach
This formula used Remove Duplicates and then counts the unique.
Sub CountUniqueRemoveDup()
Dim Ws As Worksheet
Dim UniqueCount As Long
Set Ws = Worksheets("100000")
Application.ScreenUpdating = False
Ws.Columns("A").Copy Ws.Range("B1")
Ws.Columns("B").RemoveDuplicates Columns:=1, Header:=xlYes
UniqueCount = Ws.Range("B" & Rows.Count).End(xlUp).Row - 1
Ws.Columns("B").Clear
Application.ScreenUpdating = True
Debug.Print UniqueCount
End Sub
The time taken by this Procedure is as below –
Approach 7 – Looping within Worksheet Range Approach
The formula first sorts the records and then reads each and every record. Whenever it finds a new record, it counts them for unique records.
Sub CountUniqueLooping()
Dim Ws As Worksheet
Dim Cell As Range, Rng As Range
Dim UniqueCount As Long
Set Ws = Worksheets("100")
Application.ScreenUpdating = False
Ws.Columns("A").Copy Ws.Range("B1")
Ws.Columns("B").Sort Key1:=Ws.Range("B1"), order1:=xlAscending, Header:=xlYes
Set Rng = Ws.Range("B2:B" & Ws.Range("B" & Rows.Count).End(xlUp).Row)
For Each Cell In Rng
If Cell.Offset(-1, 0) <> Cell Then
UniqueCount = UniqueCount + 1
End If
Next Cell
Ws.Columns("B").Clear
Application.ScreenUpdating = True
Debug.Print UniqueCount
End Sub
The time taken by this Procedure is as below –
Approach 8 – Looping within Array by Transferring the Range to Array Approach
We know that for large numbers it is better to transfer to an Array and then loop through array rather than looping within the Worksheet Range itself. Hence, we transfer the contents to an Array and perform looping on that. The formula first sorts the records and transfers the range to an Array. Then it reads each and every record in the array. Whenever it finds a new record, it counts them for unique records.
Sub CountUniqueTransferToArrayLooping()
Dim Ws As Worksheet
Dim Cell As Range, Rng As Range
Dim UniqueCount As Long, i As Long
Dim Arr
Set Ws = Worksheets("100")
Application.ScreenUpdating = False
Ws.Columns("A").Copy Ws.Range("B1")
Ws.Columns("B").Sort Key1:=Ws.Range("B1"), order1:=xlAscending, Header:=xlYes
Set Rng = Ws.Range("B2:B" & Ws.Range("B" & Rows.Count).End(xlUp).Row)
Arr = Rng
UniqueCount = 1
For i = LBound(Arr) + 1 To UBound(Arr)
If Arr(i - 1, 1) <> Arr(i, 1) Then
UniqueCount = UniqueCount + 1
End If
Next i
Ws.Columns("B").Clear
Application.ScreenUpdating = True
Debug.Print UniqueCount
End Sub
The time taken by this Procedure is as below –
Below is the result on the basis of above timings.
Least Time is marked in Green.
Second Least is marked in Yellow.
Third Least is marked in Orange.
Highest time is marked in Red.
Following is the conclusion –
1. Looping Array approach is best for Large Numbers. It is no. 1 >= 10000 records
2. Dictionary Early Binding approach is best for smaller numbers like 1000.
3. For very small numbers like 100, Collections is the best.
If you want to choose one single approach for any number of records, go for Looping Array
Now, below is a twist. We have seen Looping Array approach is the best to process records and our Collections / Dictionary approaches were looping through Worksheet range. Let's combine Looping Array with Collection / Dictionary and see whether there is a change.
Approach 9 – Collection combined with Looping Array
Sub CountUniqueCollectionArray()
Dim Ws As Worksheet
Dim Col As New Collection
Dim Cell As Range, Rng As Range
Dim UniqueCount As Long, i As Long
Dim Arr
Set Ws = Worksheets("100")
Set Rng = Ws.Range("A2:A" & Ws.Range("A" & Rows.Count).End(xlUp).Row)
Arr = Rng
For i = LBound(Arr) To UBound(Arr)
On Error Resume Next
Col.Add Arr(i, 1), Key:=Arr(i, 1)
Next i
UniqueCount = Col.Count
Debug.Print UniqueCount
End Sub
Results
Approach 10 – Dictionary – Late Binding combined with Looping Array
Sub CountUniqueDictArray()
Dim Ws As Worksheet
Dim Dict As Object
Dim Rng As Range
Dim Arr
Dim UniqueCount As Long, i As Long
Set Dict = CreateObject("Scripting.Dictionary")
Set Ws = Worksheets("100")
Set Rng = Ws.Range("A2:A" & Ws.Range("A" & Rows.Count).End(xlUp).Row)
Arr = Rng
For i = LBound(Arr) To UBound(Arr)
Dict(Arr(i, 1)) = Arr(i, 1)
Next i
UniqueCount = Dict.Count
Debug.Print UniqueCount
End Sub
Results
Approach 11 – Dictionary – Early Binding combined with Looping Array
Sub CountUniqueDictEBArray()
Dim Ws As Worksheet
Dim Rng As Range
Dim UniqueCount As Long, i As Long
Dim Arr
Dim Dict As Scripting.Dictionary
Set Dict = New Scripting.Dictionary
Set Ws = Worksheets("100")
Set Rng = Ws.Range("A2:A" & Ws.Range("A" & Rows.Count).End(xlUp).Row)
Arr = Rng
For i = LBound(Arr) To UBound(Arr)
Dict(Arr(i, 1)) = Arr(i, 1)
Next i
UniqueCount = Dict.Count
Debug.Print UniqueCount
End Sub
Results
FINAL RESULTS
Below is the final results tabulated. Dictionary – Early Binding – Looping Array wins in all approaches.
But Early Binding demands that Microsoft Scripting Library should be added to VBA References and if you are working in Multi user environment, then it may not be possible. In this case, the best approach is Dictionary – Late Binding – Looping Array whereas Collections Looping Array approach is best for smaller records like <1000.













