Share via

Fastest VBA search for each instance (unsorted data across mult columns)?

Anonymous
2012-10-17T20:36:14+00:00

I have a worksheet of data (3 non-contiguous columns, approximately 10,000 rows). The data is pre-sorted for other workbook functions and it would be desirable not to have to (re)sort the data for this search.

In each of these three target columns, I may or may not have cells (one or more) that match a search term. I need to find every instance of a match, then look at contiguous columns to see if the data should be brought in to my final analysis.

One option is to use application.match(searchterm,Range1,false) and if I find a match, re-run the application.match against the same column after adjusting the range to start right after the previous match. Then repeat for the other two columns.

That seems clunky, so I figured I'd ask of there is a way to return a range or array or individual cells that match the search term (similar to "Find All"), which I could then just loop to check the adjoining cell values.

Many thanks,

Keith

Fake Example: I might search the following table for all instances of Apple, after which I will need to look at the price and only select those <= 1.00

FruitMarket1   Price  Availability  FruitMarket2  Price Availability FruitMarket3 Price Availability

Apples             .79      300               Pears             1.10    120            Kiwi             .35       90

Grapes             .90        20               Strawberry    1.50    100            Apples         .85       50

Apples             1.10       60              Blueberry       3.50    35             Peaches       1.20      55

Pineapple         3.50     10               Dragonfruit    2.00     10            Cherries        .75       40

Apples             .85         80              Kiwi                 .40      60            Pears             1.25    75

Pears               1.10       90              Apples             .75      200          Grapes          .85        45

Microsoft 365 and Office | Excel | For home | Windows

Locked Question. This question was migrated from the Microsoft Support Community. You can vote on whether it's helpful, but you can't add comments or replies or follow the question.

0 comments No comments

Answer accepted by question author

Andreas Killer 144.1K Reputation points Volunteer Moderator
2012-10-18T10:08:24+00:00

A faster search as my FindAll function for your special case (one column search) is my MatchAll function, but if you plan to use a loop to check the adjoining cell values, your process will slow down much much more as FindAll need to find all the cells. :-)

IMHO the fastest way for this is to read all data into an array and search the values in there. Even with 9 columns and 10.000 rows in each, I guess this will be so fast that you did not notice that the macro was executed.

But if you want to select all prices <= 1 we have to combine the found positions (the referring cells), this need some time. 

UNION slows down as more cells are in that range, but we can use a trick and use a dictionary and an algorithm to make it much more faster, see my function FastUnion below.

How long does it need to execute?

Andreas.

Option Explicit

#Const ShowTimings = True

#If ShowTimings Then

'The QueryPerformanceFrequency function retrieves the frequency of the high-resolution _

  performance counter, if one exists.

Private Declare Function QueryPerformanceFrequency Lib "kernel32" (cyFrequency As Currency) As Long

'The QueryPerformanceCounter function retrieves the current value of the high-resolution _

  performance counter, if one exists.

Private Declare Function QueryPerformanceCounter Lib "kernel32" (cyTickCount As Currency) As Long

Private Function MilliTimer() As Double

  'return containing seconds uses Windows API calls to the high resolution timer

  Dim Ticks As Currency

  Static Frequency As Currency

  MilliTimer = 0

  ' get frequency

  If Frequency = 0 Then

    QueryPerformanceFrequency Frequency

    If Frequency = 0 Then Exit Function

  End If

  ' get ticks

  QueryPerformanceCounter Ticks

  ' calc seconds

  MilliTimer = Ticks / Frequency

End Function

#End If

Sub FindApples()

  Dim Data()

  Dim i As Long, j As Long

  Dim All As Range

#If ShowTimings Then

  Dim dtime As Double

  dtime = MilliTimer

#End If

  'Read all data into an array

  Data = Range("A1").CurrentRegion

  'Visit all columns, 3 columns are 1 dataset

  For j = 1 To UBound(Data, 2) Step 3

    'Visit each row

    For i = 1 To UBound(Data)

      'Compare the values

      If StrComp(Data(i, j), "Apples", vbTextCompare) = 0 Then

        If Data(i, j + 1) <= 1 Then

          'Remember the refering cell

          FastUnion All, Cells(i, j + 1)

        End If

      End If

    Next

  Next

  'Build a range from all found cells

  Set All = FastUnion(All)

#If ShowTimings Then

  dtime = (MilliTimer - dtime)

  Debug.Print "FindApples:  " & Format(dtime, "0.000000") & " seconds"

#End If

  If All Is Nothing Then

    Debug.Print "Nothing found"

  Else

    All.Select

    Debug.Print All.Count & " cells.Found"

  End If

End Sub

Private Function FastUnion(ByRef All As Range, Optional ByRef R As Range = Nothing) As Range

  '© Andreas Killer, 2011

  Static Stack As Object 'Dictionary

  Dim Temp() As Variant

  Dim i As Long, j As Long

  'Initialize our internal stack if necessary

  If Stack Is Nothing Then Set Stack = CreateObject("Scripting.Dictionary")

  'Should we return the result?

  If R Is Nothing Then

    'Did we have something?

    If Stack.Count = 0 Then

      Set FastUnion = All

      Exit Function

    End If

    'Get all cells as fragments

    Temp = Stack.Items

    'Combine each fragment with the next one

    j = 1

    Do

      For i = 0 To UBound(Temp) - j Step j * 2

        Set Temp(i) = Union(Temp(i), Temp(i + j))

      Next

      j = j * 2

    Loop Until j > UBound(Temp)

    'At this point we have all cells in the first fragment

    'Combine with the previous result from the outside if any

    If All Is Nothing Then

      Set FastUnion = Temp(0)

    Else

      Set FastUnion = Union(All, Temp(0))

    End If

    'Remove all items from the stack

    Stack.RemoveAll

  Else

    'Add the range to our internal stack

    Stack.Add Stack.Count, R

  End If

End Function

Was this answer helpful?

0 comments No comments

Answer accepted by question author

Paul Edstein 82,861 Reputation points Volunteer Moderator
2012-10-18T07:22:54+00:00

You might consider using autofilter:

Sub GetApples()

Dim i As Long, xlCell As Range, lRow As Long

With ActiveSheet

  lRow = .Cells.SpecialCells(xlCellTypeLastCell).Row

  .Rows("1:1").AutoFilter

  For i = 1 To 7 Step 3

    With .Columns(i)

      .AutoFilter Field:=i, Criteria1:="Apples"

      For Each xlCell In .Range(Columns(2).Rows(2), Columns(2).Rows(lRow)).SpecialCells(xlCellTypeVisible)

        If xlCell.Value <= 1 Then MsgBox xlCell.Value & vbTab & xlCell.Offset(0, 1).Value

      Next

      .AutoFilter Field:=i

    End With

  Next

  .Rows("1:1").AutoFilter

End With

End Sub

Was this answer helpful?

0 comments No comments

2 additional answers

Sort by: Most helpful
  1. Anonymous
    2012-10-18T21:47:05+00:00

    As Andreas says: a Variant array is probably the fastest approach: see http://fastexcel.wordpress.com/2011/10/26/match-vs-find-vs-variant-array-vba-performance-shootout/

    for a comparison of FIND,MATCH and a VARIANT array

    Was this answer helpful?

    0 comments No comments
  2. Anonymous
    2012-10-18T16:13:48+00:00

    Thank you both for your responses. I will experiment with both of your recommendations.

    > How long does it need to execute?

    Total time will depend on user selections. The entire search may be repeated 10-15 times in a row (if user selects an option that has 15 search terms, e.g. apples/pears/grapes/etc), but most of the time the selection will average about 5 selections. This may happen multiple times in a session as the user changes their selections, so response time is a concern. I'm working somewhat blind, because the machine I use is quite a bit faster than my end users. I started with my less elegant approach of application.match to find the first instance in rows 1:10,000, then moving the range after each match until no more matches are found (e.g. if match is found on row 628, then the subsequent search is rows 629:10,000). On my machine this is fast enough, but I was looking for even faster approaches so that I don't run into problems later when we push this to slower machines.

    Thank you!

    Was this answer helpful?

    0 comments No comments