在VBA(excel)中循环遍历行的最有效/最快的方法是啥?

Posted

技术标签:

【中文标题】在VBA(excel)中循环遍历行的最有效/最快的方法是啥?【英文标题】:What is the most efficient/quickest way to loop through rows in VBA (excel)?在VBA(excel)中循环遍历行的最有效/最快的方法是什么? 【发布时间】:2011-12-31 23:10:29 【问题描述】:

我知道 Excel 中的 VBA 并不是最快的 - 但我需要最有效(即最快)的方式来循环大量行样本。

目前我有:

For Each c In Range("$A$2:$A$" & Cells(Rows.count, "A").End(xlUp).row
    ' do stuff
Next c

“做事”包括在这里和那里插入一行(所以我需要保持范围的动态查找。)

任何想法(查看 10,000 行以上)?

编辑 我已经在使用了

Application.ScreenUpdating = False
Application.Calculation = xlManual

【问题讨论】:

我在 Excel 中使用 VBA 已经快十年了,所以我有点迷茫。但是,我认为有一种方法可以在执行此类大型流程时关闭屏幕更新,这往往会大大加快速度。不过,我可能记错了。 100% 正确,我正在使用 Application.ScreenUpdating = False 和 Application.Calculation = xlManual(将添加到问题中) 您可以尝试通过跟踪您自己所做的任何插入来摆脱范围,但我倾向于认为对于 for 循环结构本身可以做的优化不多。我当然可能是错的,但是您应该对仅执行循环所需的时间进行基准测试,其中根本没有任何内容,以查看您是否真的可以在那里节省大量时间,或者您是否正在查看错误的地方。 我认为您在编辑中的意思是 xlManual 如果您的代码不依赖于触发的工作表/工作簿事件,Application.EnableEvents = False 也可以成为真正的性能提升器。 【参考方案1】:

如果您只是循环遍历 A 列中的 10k 行,则将该行转储到一个变体数组中,然后循环遍历该数组。

然后,您可以将元素添加到新数组(同时在需要时添加行)并使用 Transpose() 一次性将数组放入您的范围,或者您可以使用迭代器变量来跟踪您所在的行以这种方式添加行。

Dim i As Long
Dim varray As Variant

varray = Range("A2:A" & Cells(Rows.Count, "A").End(xlUp).Row).Value

For i = 1 To UBound(varray, 1)
    ' do stuff to varray(i, 1)
Next

这是一个示例,说明如何在评估每个单元格后添加行。此示例只是在 A 列中包含单词“foo”的每一行之后插入一行。并不是说在插入期间将“+2”添加到变量 i 中,因为我们从 A2 开始。如果我们从 A1 开始我们的数组,那将是 +1。

Sub test()

Dim varray As Variant
Dim i As Long

varray = Range("A2:A10").Value

'must step back or it'll be infinite loop
For i = UBound(varray, 1) To LBound(varray, 1) Step -1
    'do your logic and evaluation here
    If varray(i, 1) = "foo" Then
       'not how to offset the i variable 
       Range("A" & i + 2).EntireRow.Insert
    End If
Next

End Sub

【讨论】:

+1 循环有时是不必要的。将范围转储到数组中,对数组执行所需的操作,然后将其转储。当您需要向原始数据“添加行”时,动态第二个数组(使用 Redim)可以提供帮助。 当然。我自己是第二个数组上的 redim 的忠实粉丝 :) 变体数组+1,JPs评论+1以处理新行 我真的很喜欢新数组的想法,但我遇到了障碍。示例 - Range(...).value 返回一个数组,例如 a(10,10)。假设我有第二个数组 Redim a2 作为变体,如果我想插入一个“行”,我会将 a2 重新调整为 10+1,但是当“a”为时如何将 a(1) 复制到 a2(1)一整行 - 目前我必须遍历 a2(1, 1) = a(1, 1)、a2(1, 2) = a(1, 2) 等的每个值,因为我不能只提取数组的行 克里斯,正是出于这个原因,我建议您只使用 i 变量插入行(如果您从 A2 开始并且需要添加一行,您知道新行应该是 i + 1)。将行添加到新数组是值得的,但复杂性肯定是一种权衡。这个想法通常是您将元素从一个数组传输到另一个数组,同时添加行。【参考方案2】:

编辑总结和建议

使用for each cell in range 构造本身并不慢。 慢的是在循环中重复访问 Excel(读取或写入单元格值、格式等、插入/删除行等)。

什么太慢完全取决于您的需求。一个需要几分钟才能运行的 Sub 如果很少使用可能还可以,但另一个需要 10 秒的 Sub 如果频繁运行可能会太慢。

所以,一些一般性建议:

    首先保持简单。如果结果太慢,无法满足您的需求,请进行优化 专注于循环内容的优化 不要只是假设需要循环。有时会有其他选择 如果您需要在循环内(大量)使用单元格值,请将它们加载到循环外的变量数组中。 避免插入复杂性的一个好方法是从下往上循环范围 (for index = max to min step -1) 如果您无法做到这一点,并且您的“在此处插入一行”并没有太多,请考虑在每次插入后重新加载数组 如果您需要访问除value 以外的单元格属性,您将无法使用单元格引用 要删除多行,请考虑在循环中构建对多区域范围的范围引用,然后在循环后一次性删除该范围

例如(未测试!)

Dim rngToDelete as range
for each rw in rng.rows
    if need to delete rw then

        if rngToDelete is nothing then
            set rngToDelete = rw
        else
            set rngToDelete = Union(rngToDelete, rw)
        end if

    endif
next
rngToDelete.EntireRow.Delete

原帖

传统观点认为循环遍历单元格是不好的,而遍历一个变体数组是好的。一段时间以来,我也一直倡导这一点。你的问题让我思考,所以我做了一些简短的测试,结果令人惊讶(无论如何对我来说):

测试数据集:单元格中的简单列表 A1 .. A1000000(即 1,000,000 行)

测试用例 1:循环数组

Dim v As Variant
Dim n As Long

T1 = GetTickCount
Set r = Range("$A$1", Cells(Rows.Count, "A").End(xlUp)).Cells
v = r
For n = LBound(v, 1) To UBound(v, 1)
    'i = i + 1
    'i = r.Cells(n, 1).Value 'i + 1
Next
Debug.Print "Array Time = " & (GetTickCount - T1) / 1000#
Debug.Print "Array Count = " & Format(n, "#,###")

结果:

Array Time = 0.249 sec
Array Count = 1,000,001

测试用例 2:循环范围

T1 = GetTickCount
Set r = Range("$A$1", Cells(Rows.Count, "A").End(xlUp)).Cells
For Each c In r
Next c
Debug.Print "Range Time = " & (GetTickCount - T1) / 1000#
Debug.Print "Range Count = " & Format(r.Cells.Count, "#,###")

结果:

Range Time = 0.296 sec
Range Count = 1,000,000

因此,循环数组 更快,但速度只有 19% - 远低于我的预期。

测试 3:使用单元格引用循环数组

T1 = GetTickCount
Set r = Range("$A$1", Cells(Rows.Count, "A").End(xlUp)).Cells
v = r
For n = LBound(v, 1) To UBound(v, 1)
    i = r.Cells(n, 1).Value
Next
Debug.Print "Array Time = " & (GetTickCount - T1) / 1000# & " sec"
Debug.Print "Array Count = " & Format(i, "#,###")

结果:

Array Time = 5.897 sec
Array Count = 1,000,000

测试用例 4:带有单元格引用的循环范围

T1 = GetTickCount
Set r = Range("$A$1", Cells(Rows.Count, "A").End(xlUp)).Cells
For Each c In r
    i = c.Value
Next c
Debug.Print "Range Time = " & (GetTickCount - T1) / 1000# & " sec"
Debug.Print "Range Count = " & Format(r.Cells.Count, "#,###")

结果:

Range Time = 2.356 sec
Range Count = 1,000,000

所以事件只有一个简单的单元格引用,循环慢一个数量级,更重要的是,范围循环快两倍!

所以,结论是最重要的是你在循环中做了什么,如果速度真的很重要,请测试所有选项

FWIW,在 Excel 2010 32 位、Win7 64 位上测试 所有带有

的测试 ScreenUpdating 关闭, Calulation手册, Events 已禁用。

【讨论】:

您错过了最重要的测试 - 使用 varray 引用循环 varray。测试 3 没有意义 - 为什么要循环一个数组,但要为每个元素调用 excel 并忽略数组的内容?我认为您会发现循环数组并引用其元素比循环范围和引用单元格要快得多(确保您使用正在测试的单元格内的数据(不是空单元格)进行测试)。 @Issun 抱歉,但您错过了重点:OP 明确指出 “做事”包括在此处和此处插入一行,因此引用循环内的工作表是必须的。教学的选择是故意微不足道的,而不是有意义的。如果不需要在循环内引用工作表,那么使用 zarray 引用是显而易见的答案,并且可以(并且确实)不用说。这里真正有趣的是在单元格上使用循环本身并不是一种昂贵的方法。更重要的是你在循环中做了什么。 在我的测试中,只需将变量 tmp 分配为等于单元格值,1M 的范围也为 2.234 秒,数组为 0.3428 秒。字符串的结果相似(两种方法都长了大约三分之一秒)。 与在r.Cells(n, 1) 中循环通过n 相比,“范围循环速度是两倍”的原因在于,与查找相比,For Each 构造本质上对于遍历集合更快通过他们的索引收集成员,这比较慢。当然,如果您决定将后者称为“循环通过变体”,那么“循环通过变体”是不好的,但事实并非如此;它是通过索引“循环通过 [a collection of] 单元格”,传统观点准确地将其描述为缓慢。 一般来说,空循环是测试速度的一种非常糟糕的方法,因为循环不会什么都不做。当没有引用范围/集合/数组中的每个项目时,for/foreach 循环的时间几乎没有。你这样做的那一刻,for each loops DESTROY the for loops,并且 varrays DESTROY 调用 excel 单元格 8 如此之多,以至于你使用 for 而不是 for each 的事实是无关紧要的)【参考方案3】:

出于某种原因,For Each 比 for I=1 to X 快得多。试试看同一个字典,


对于 dDict 中的每个 Dkey 一次,


还有一次 for Dkey = lbound(dDict.keys) to ubound(dDict.keys)

=>即使您使用相同的构造,您也会注意到巨大的差异。

【讨论】:

以上是关于在VBA(excel)中循环遍历行的最有效/最快的方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

Excel VBA 宏 - 在循环中连接

在 VBA 循环中保存 excel 文件

Excel VBA 宏给出了意想不到的结果?

访问中的 Vba 代码循环遍历文件夹中的所有 excel 文件,打开、保存和关闭它们

准时下班系列!Excel合集之第2集—VBA填充百万行公式,怎么写最快

使用 VBA 循环遍历 Excel 中表格范围的第一列