【问题标题】:Performing MATCH/INDEX on large dataset在大型数据集上执行 MATCH/INDEX
【发布时间】:2020-08-07 04:43:29
【问题描述】:

我有一个包含 55.000 行的数据集,其中包含 35.000 个电子邮件地址,其中 31.000 个是唯一的,因此几个用户占据了多行。我需要找到这些用户的行并将它们添加到类对象中。

将电子邮件列加载到数组中并执行 MATCH/INDEX 查找需要 200 秒。目前可以接受,但对于 200-500K 数据集的预期用途来说,速度肯定不够快。

Dim StartTime As Double
Dim SecondsElapsed As Double
StartTime = Timer

Dim dict As Dictionary
Set dict = CreateObject("scripting.dictionary")

LastRow = Cells(Rows.Count, 1).End(xlUp).Row

Set UserRange = Range(Cells(2, 11), Cells(LastRow, 11))
For Each cell In UserRange
    dict(cell.value) = dict(cell.value) + 1
Next

Debug.Print "Number of users: " & dict.Count

UserArray = Range(Cells(2, 11), Cells(LastRow, 11))
UserArray = WorksheetFunction.Transpose(WorksheetFunction.Transpose(WorksheetFunction.Transpose(UserArray)))

For Each User In dict
    Dim UserIndex() As Variant
    ReDim UserIndex(1 To dict(User))
    For i = 1 To dict(User)
        Row = WorksheetFunction.Match(User, UserArray, 0)
        UserIndex(i) = Row
        UserArray(Row) = Empty
    Next
    For i = LBound(UserIndex) To UBound(UserIndex)
        Debug.Print User, UserIndex(i)
    Next
Next

SecondsElapsed = Round(Timer - StartTime, 2)
Debug.Print "This code ran successfully in " & SecondsElapsed & " seconds"

我可能会基于源构建一个基于块的索引(每个导入的文件 +-10.000 个条目都有一个 start- 和 endrow),并通过仅查看适当的块来加速它。但也许还有其他方法?

【问题讨论】:

  • 您致力于使用 Excel 吗?如果您将数据转储到 Access 中,您可以轻松/快速地运行一些 SQL 来返回重复项。
  • Excel VBA 是我现在所拥有的。这是一段冗长而复杂的代码的最后一步。
  • 工作表不是为此而制作的,您需要一个实际的数据库。 Application.Transpose 也有局限性:此解决方案无法扩展。您可以做的一件事是在第一个循环中,使用For 循环迭代Range(...).Value,而不是使用Range 并为源范围中的每个单元格拉动Range.Value 两次:您将访问整个源数据的工作表一次,而不是UserRange 中的每个单元格一次。请注意,其中有许多隐式 ActiveSheet 引用,可能会在以后引起问题。
  • 如果您不使用 debug.print 而是在最后将整个内容写入文本文件或类似的东西,您可以节省一点时间。将 20k+ 转储到即时窗口似乎没有必要。循环内的 redim 可能也无济于事。
  • 字典在 0.42 秒内构建,因此访问工作簿不是这里的瓶颈。 Debug.Print 我已经关闭了整个操作(它只在我不循环时才出现)。真正需要时间的是循环和匹配/索引。

标签: excel vba indexing match vlookup


【解决方案1】:

感谢蒂姆,我得到了一个解决方案:

Dim dict As Dictionary
Set dict = CreateObject("scripting.dictionary")

LastRow = Cells(Rows.Count, 1).End(xlUp).Row

Set UserRange = Range(Cells(2, 11), Cells(LastRow, 11))
For Each cell In UserRange
    dict(cell.value) = dict(cell.value) + 1
Next

Debug.Print "Number of users: " & dict.Count

t = Timer

For Each User In dict
    Set Profile = New UserProfile
    Profile.Count = dict(User)
    Dim UserIndex() As Variant
    ReDim UserIndex(1 To dict(User))
    For i = 1 To dict(User)
        Row = WorksheetFunction.Match(User, UserRange, 0)
        UserIndex(i) = Row
    Next
    For i = LBound(UserIndex) To UBound(UserIndex)
        Dim Purchase() As Variant
        ReDim Purchase(1 To LastCol) As Variant
        Purchase = Range(Cells(UserIndex(i) + 1, 1), Cells(UserIndex(i) + 1, LastCol))
        Profile.Add Purchase
    Next
Next

Debug.Print "Match/Index loop completed in ", Timer - t

事实证明,在范围而不是数组上进行匹配要快得多。从范围读取而不是在数组上执行 WorksheetFunction.Index 也是如此。这些结果都出乎我的意料,因为我认为读/写工作簿通常会减慢速度。我还在我的 UserProfile 类中的 thePurchase 数组读数中添加了一个 (1 , 以放弃转置。

整个 55K 数据集的分析仅在 23 秒内完成!

【讨论】:

  • 我认为 Match/Index 已针对工作表上的数据进行了优化(因为这是它们通常使用的地方)。对数组使用它们很方便,但是这样做时相对性能很差。不过,通常您不会对这么多数据进行操作,因此不会引起注意。
【解决方案2】:

这是另一种相当快的方法:

Sub Lister()

    Dim t, i, m, arr, rng, dict As Object, dictDupes As Object, usr, v
    Set dict = CreateObject("scripting.dictionary")
    Set dictDupes = CreateObject("scripting.dictionary")

    Set rng = Range("A1:A500000")
    'create some dummy data (0.5M rows)
    With rng
        .Formula = "=""USER_"" & ROUND(RAND()*5000,0) & ""_"" & ROUND(RAND()*3000,0)"
        .Value = .Value
    End With

    t = Timer

    arr = rng.Value
    For i = 1 To UBound(arr, 1)
        usr = arr(i, 1)
        If Not dict.exists(usr) Then
            dict.Add usr, i
        Else
            If Not dictDupes.exists(usr) Then dictDupes.Add usr, dict(usr)
            dictDupes(usr) = dictDupes(usr) & "|" & i
        End If
    Next i

    For Each usr In dictDupes
        v = dictDupes(usr)
        'Debug.Print "----" & usr & "---"
        'Debug.Print Join(Split(v, "|"), ", ")
    Next usr

    Debug.Print dict.Count, dictDupes.Count
    Debug.Print "Done in", Timer - t

End Sub

在大约 20-25 秒内完成

另一个说明:

如果您想使用 Match,那么将数据留在工作表上比对数组运行 Match 要快得多。

Sub TestMatch()

    Dim t, i, m, arr, rng

    Set rng = Range("A1:A50000")
    With rng
        .Formula = "=ROUND(RAND()*30000,0)"
        .Value = .Value
    End With

    t = Timer
    For i = 1 To 10000
        m = Application.Match(i, rng, 0)
    Next i
    Debug.Print "sheet", Timer - t

    arr = rng.Value
    t = Timer
    For i = 1 To 10000
        arr = rng.Value
        m = Application.Match(i, arr, 0)
    Next i
    Debug.Print "array", Timer - t

End Sub

输出:

sheet          3.644531 
array          131.9453 

所以数组的速度大约是 35 倍

【讨论】:

  • 有趣的发现蒂姆。我将我的用户放在 UserRange 中,并在 23 秒内完成了比赛,而使用 UserArray 则在 200 秒内完成了比赛。现在看来,真正的瓶颈是我循环中的第二个循环。需要进行更多测试。
  • 这是 Worksheet.Index 永远占用的时间。我会尝试找出更好的方法。
猜你喜欢
  • 2013-12-25
  • 1970-01-01
  • 1970-01-01
  • 2019-09-27
  • 2018-11-28
  • 2014-10-05
  • 2021-07-16
  • 2011-07-25
  • 1970-01-01
相关资源
最近更新 更多