質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
VBA

VBAはオブジェクト指向プログラミング言語のひとつで、マクロを作成によりExcelなどのOffice業務を自動化することができます。

Q&A

解決済

4回答

6441閲覧

配列を使用しているのにForループより処理速度が遅い理由

quark87139

総合スコア6

VBA

VBAはオブジェクト指向プログラミング言語のひとつで、マクロを作成によりExcelなどのOffice業務を自動化することができます。

0グッド

0クリップ

投稿2021/07/15 16:18

以前より皆様の投稿を拝見しており、勉強させていただいております。
ありがとうございます。

早速ですが、初めて質問させていただきます。

最初はFor文を使った処理を行っていたものの、
処理時間が遅いことや配列を使用した処理の方が速くなることを知ったため、
配列を使用した処理を検討しています。

データ量:1万行以上
検索値:文字列&日付
配列Key:文字列&日付
配列Item:Arrayで配列格納

現状としてFor文処理の場合、8-10分程度の処理時間が掛かっており、
理想としては1分以内に処理が完了するように対応を行いたいと考えています。

こちらのサイトで
過去の投稿含め、参考にしつつ配列のコードを繰り返し作成したのですが、
なぜかFor文より処理が遅くなってしまいます。

配列を使用したらなんでもかんでも早くなるというわけではなく、
For文を使った方が早いパターンもあるということでしょうか...?

以下にコードを記載しますので、
改善点や他により早く処理を行う方法ありましたら、
ご教示頂けると大変助かります。

VBA

1Sub 一致転記() 2 3Dim ST As Single, ET As Single 4 5Dim wb As Workbook 6Set wb = ThisWorkbook 7 8Dim ws1 As Worksheet, ws2 As Worksheet 9Set ws1 = wb.Sheets("sheets1") 10Set ws2 = wb.Sheets("sheets2") 11 12 13Dim j, k, x 14Dim lRow1 As Long, lRow2 As Long 15 16Dim list As String 17Dim Arry As Object 18 19Set Arry = CreateObject("Scripting.Dictionary") 20 21lRow1 = ws1.Cells(Rows.count, "B").End(xlUp).Row 22lRow2 = ws2.Cells(Rows.count, "B").End(xlUp).Row 23 24Application.ScreenUpdating = False 25Application.DisplayAlerts = False 26 27ST = Timer 28 29With ws2 30For j = 2 To lRow2 31 32 list = .Cells(j, "G") & .Cells(j, "W") 33 34 ARY.Add list, Array(.Cells(j, "Y"), .Cells(j, "Z"), .Cells(j, "AA"), .Cells(j, "AB")) 35 36Next j 37End With 38 39Dim str, aKey, aItem 40 41With ws1 42For k = 3 To lRow1 43 str = .Cells(k, "B") & .Cells(k, "C") 44 45 For x = 0 To Arry.count - 1 46 aKey = Arry.Keys()(x) 47 aItem = Arry.Items()(x) 48 49 If str = dcKey Then 50 51 .Cells(k, "E") = aItem(0) 52 53 End If 54 55 Next x 56 57Next k 58 59ET = Timer 60ET = ET - ST 61 62Application.ScreenUpdating = True 63Application.DisplayAlerts = True 64 65MsgBox Round(ET / 60, 0) & "." & Round(ET Mod 60, 0) 66 67End Sub 68 69

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答4

0

ベストアンサー

まず

VBA

1 2Arry.Add list, Array(.Cells(j, "Y"), .Cells(j, "Z"), .Cells(j, "AA"), .Cells(j, "AB")) 3

ですが,これだとRangeオブジェクトのまま操作していることになります。

VBA

1 2Arry.Add list, Array(.Cells(j, "Y").value, .Cells(j, "Z").value, .Cells(j, "AA").value, .Cells(j, "AB").value) 3

とすることで値での処理になります。

まぁ。これ自体は、今回の処理の遅さにはそれほど影響しないと思います。

VBA

1With ws1 2For k = 3 To lRow1 3 str = .Cells(k, "B") & .Cells(k, "C") 4 5 For x = 0 To Arry.count - 1 6 aKey = Arry.Keys()(x) 7 aItem = Arry.Items()(x) 8 9 If str = dcKey Then 10 11 .Cells(k, "E") = aItem(0) 12 13 End If 14 15 Next x 16 17Next k

こちらの方は、二つ問題があります。
一つ目の問題は、辞書。Dictionaryを使っているのに、検索を自前で行っていること
検索をDictionaryに任せると以下のようになります。

VBA

1With ws1 2For k = 3 To lRow1 3 str = .Cells(k, "B") & .Cells(k, "C") 4 If Arry.Exists(str) Then 5 .Cells(k, "E") = Arry.Item(str)(0) 6 End If 7Next k 8End With 9

これである程度早くなります。

次に、ヒットしない場合の動作が変わってしまって良ければですが

VBA

1With ws1 2Dim tV As Variant 3ReDim tV(3 To lRow1, 0) 4For k = 3 To lRow1 5 str = .Cells(k, "B") & .Cells(k, "C") 6 If Arry.Exists(str) Then 7 tV(k,0) = Arry.Item(str)(0) 8 End If 9Next k 10.Range("E3:E" & lRow1).Value = tV 11End With 12

このように配列で一括で、セルにセットすることでもある程度早くなります。
ヒットしない場合の動作を変えたくなければ

VBA

1With ws1 2Dim tV As Variant 3tV = .Range("E3:E" & lRow1).Value 4For k = 3 To lRow1 5 str = .Cells(k, "B") & .Cells(k, "C") 6 If Arry.Exists(str) Then 7 tV(k - 2, 1) = Arry.Item(str)(0) 8 End If 9Next k 10.Range("E3:E" & lRow1).Value = tV 11End With

となるでしょう。
何が違うかと言えば、ヒットしなかったとき、値をそのままにしている。という点です。
用途によって使い分けるといいかと思います。

以上で、処理速度に関しては、早くなるかと思います。

(補足)
(1)
処理速度とは関係ありませんが
Dim str
というのは、str関数を上書きしてしまう(str関数が使いにくくなる)ので使わない方が良いと思います。

(2)
Arryの値を配列で持っていますが一つ目の値しか使っていないですよね。
これは、想定通りなのでしょうか。
想定通りであるのであれば、良いのですが。

(3)
質問文では

ARY.Add list, Array(.Cells(j, "Y"), .Cells(j, "Z"), .Cells(j, "AA"), .Cells(j, "AB"))

となってますが

Arry.Add list, Array(.Cells(j, "Y"), .Cells(j, "Z"), .Cells(j, "AA"), .Cells(j, "AB"))

の記述ミスですよね。

(4)
Arry.Add list, Array(.Cells(j, "Y"), .Cells(j, "Z"), .Cells(j, "AA"), .Cells(j, "AB"))

とありましたがよく見ると連続するセルですね。
この為

Arry.Add list, .Range("Y" & j & ":AB" & j).value

とした方が若干早いかもですね。
こちらの場合にすると処理の書き方が変わるので変えた内容も記載しておきます。

VBA

1With ws2 2For j = 2 To lRow2 3 list = .Cells(j, "G") & .Cells(j, "W") 4 Arry.Add list, .Range("Y" & j & ":AB" & j).Value 5Next j 6End With 7 8Dim str, aKey, aItem 9 10With ws1 11Dim tV As Variant 12ReDim tV(3 To lRow1, 0) 13For k = 3 To lRow1 14 str = .Cells(k, "B") & .Cells(k, "C") 15 If Arry.Exists(str) Then 16 tV(k, 0) = Arry.Item(str)(1, 1) 17 End If 18Next k 19.Range("E3:E" & lRow1).Value = tV 20End With

投稿2021/07/15 20:45

編集2021/07/15 21:07
xail2222

総合スコア1508

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

quark87139

2021/07/16 12:29 編集

xail2222様 丁寧でなおかつ分かりやすいご回答ありがとうございます。 配列自体は作成できているものの、配列を活用できていない状態= 単に1つずつ代入して転記させるFor文の処理と変わりになかったのですね・・・。 またExistsの部分もネット上で検索して自分なりの解釈でいくと 登録を行う時に利用するもので、検索、取り出し、取り入れに使用するものではないと 意味を取り違っておりました。 補足の点について 1.仰るとおり、str関数があるから変数として使うのはあまり好ましくないですね、  今後は関数と被らないよう意識してコード作成したいと思います。 2.質問内のコードでは単一のデータを取得転記させる目的として書かれていますが、  実際に行いたい処理は複数のデータを取得転記させる予定なのでArrayを採用した次第です。 3.仰るように僕のミスです。正しくはArry(create object)です。 4.連続する・しないの違いで使い分けることで処理速度が少し異なるのは知りませんでした。  今後はその辺も含めて意識しながら作成したいと思います。 以上を踏まえていくつか気になったのですが、 登録済の配列にあるitemをとある表(マスタ)一致検索して取り替えることは可能なのでしょうか? 感覚としては、配列を2回行うイメージで少し手間が掛かる印象なのですが、、、。 1.itemとして格納される分を配列登録 2.一致したものをあるセルへ転記 3.配列登録 4.一致転記
xail2222

2021/07/16 13:30

>登録済の配列にあるitemをとある表(マスタ)一致検索して取り替えることは可能なのでしょうか? それは可能です。 Arry.Item(str)=??? の形で取り換える事ができますが、こういうことでしょうか。 >感覚としては、配列を2回行うイメージで少し手間が掛かる印象なのですが、、、。 …ちょっと意味が分からないです。すみません。
quark87139

2021/07/16 14:11 編集

分かりづらい説明ですみません。 配列を2回行うイメージというのは 下記のような手順で考えていたためです。 配列に格納(1回目の格納)されているitemを転記を行い、 転記させた内容(item)を配列に格納(2回目の格納)を行って一致検索・itemの上書きを行う。 より簡潔に言えば、itemをどこかに出力させてから一致検索・転記と2度手間を 掛けずとも1回で完結できるか知りたかった次第です。
xail2222

2021/07/16 14:26

各単語が何を指しているのか解らないです。 何となくでも良いので具体的なコードにして頂ければ簡略化したコードもわかるかもしれません
quark87139

2021/07/17 01:46

具体的なコードとなると以下のような感じになると思うのですが、 意図伝わりますでしょうか...。 ※簡略化できず申し訳ございません。 Dim sRow1 Set ws1 = wb.Sheets("出力") Set ws2 = wb.Sheets("データ元") Set ws3 = wb.Sheets("マスタ") sRow1 = ws1.Cells(Rows.count, "B").End(xlUp).Row sRow2 = ws2.Cells(Rows.count, "B").End(xlUp).Row sRow3 = ws3.Cells(Rows.count, "B").End(xlUp).Row '①マスタの内容を配列格納 With ws3 For i = 3 To sRow3 list = .Cells(i, "B") dc.Add list, .Cells(i, "F") Next i End With 'マスタと一致する内容(item)を転記 With ws2 For k = 2 To sRow2 fnd = .Cells(i, "B") & .Cells(i, "D") If dc.exists(fnd) Then .Cells(k, "N") = dc.Item(fnd) End If Next k dc.RemoveAll '②データ元を配列格納 For i = 2 To sRow2 list = .Cells(i, "B") & .Cells(i, "D") dc.Add list, .Cells(i, "N").Value Next i End With '③転記先シートへ転記 With ws1 For k = 3 To sRow1 fnd = .Cells(k, "B") & .Cells(k, "C") If dc.exists(fnd) Then .Cells(k, "F") = dc.Item(fnd) End If Next k End With
xail2222

2021/07/17 02:16 編集

dc.RemoveAllしてから、また追加であれば、確かに二度手間な感じがしますね。 セルへの入出力は、配列を使った方が早いですが とりあえず辞書 Dicrionaryの扱いの部分だけ記載します。 マスタと一致する内容(item)を転記の部分ですが 現状のソースが実行できるデータ内容であれば 'マスタと一致する内容(item)を転記等 With ws2 For k = 2 To sRow2 fnd = .Cells(i, "B") & .Cells(i, "D") If dc.exists(fnd) Then .Cells(k, "N") = dc.Item(fnd) Else 'マスタ登録外のデータを登録 dc.Add fnd, .Cells(k, "N").Value End If Next k End With という感じになるでしょうか。 全データが、マスタに登録されているのであれば 再登録は不要になるのですが、マスタ未登録のデータがあると仮定すると 上記のように、登録外のデータだけ追加ということになりますね。
quark87139

2021/07/17 03:18

考えとしては、 マスタと一致する内容をわざわざ転記せず(見える化)に配列上?で 処理を行って転記を行いたいということ なのですが、 結論としてはマスタと一致する内容転記させてから転記・・・の流れが 考え方としては間違っていないということですかね..? 一旦最初に質問させて頂いた内容自体は解決しているので、 解決済とさせて頂きます。 お忙しい中時間を割いていただきありがとうございました。
xail2222

2021/07/17 03:33

>考え方としては間違っていないということですかね..? ん~。マスタは、その命名で意味は分かります。 データ元の役割が、意味が良くわかりません。 この為、間違っているかどうかよくわからないです。
guest

0

<追記>

VBA

1Sub 一致転記2() 2 3 '開始時間 4 Dim ST As Single, ET As Single 5 ST = Timer 6 7 '表示抑制 8 With Application 9 .ScreenUpdating = False 10 .DisplayAlerts = False 11 End With 12 13 '配列作成 14 Dim ws1rng As Range, ws1data As Variant 15 With ThisWorkbook.Sheets("出力") 16 Set ws1rng = .Range("A1", .Cells.SpecialCells(xlCellTypeLastCell)) 17 ws1data = ws1rng.Value 18 End With 19 Dim ws2rng As Range, ws2data As Variant 20 With ThisWorkbook.Sheets("データ元") 21 Set ws2rng = .Range("A1", .Cells.SpecialCells(xlCellTypeLastCell)) 22 ws2data = ws2rng.Value 23 End With 24 Dim ws3rng As Range, ws3data As Variant 25 With ThisWorkbook.Sheets("マスタ") 26 Set ws3rng = .Range("A1", .Cells.SpecialCells(xlCellTypeLastCell)) 27 ws3data = ws3rng.Value 28 End With 29 30 '辞書作成 31 Dim m, i, k, v 32 Set m = CreateObject("Scripting.Dictionary") 33 For i = 3 To UBound(ws3data, 1) 34 k = ws3data(i, 2) 35 v = ws3data(i, 6) 36 m(k) = v 37 Next 38 39 '辞書適用 40 Dim d 41 Set d = CreateObject("Scripting.Dictionary") 42 For i = 2 To UBound(ws2data, 1) 43 k = ws2data(i, 2) & ws2data(i, 4) 44 If m.Exists(k) Then 45 ws2data(i, 14) = m(k) 46 d(k) = ws2data(i, 14) 47 End If 48 Next 49 For i = 2 To UBound(ws1data, 1) 50 k = ws1data(i, 2) & ws1data(i, 3) 51 If d.Exists(k) Then 52 ws1data(i, 6) = d(k) 53 End If 54 Next 55 56 '配列出力 57 ws1rng.Value = ws1data 58 ws2rng.Value = ws2data 59 60 '抑制解除 61 With Application 62 .ScreenUpdating = True 63 .DisplayAlerts = True 64 End With 65 66 '終了時間 67 ET = Timer - ST 68 MsgBox Round(ET / 60, 0) & "." & Round(ET Mod 60, 0) 69 70End Sub 71

<追記前>
他の方とそんなに変わらないので載せる意味も乏しいですが。

VBA

1Sub 一致転記() 2 3 '開始時間 4 Dim ST As Single, ET As Single 5 ST = Timer 6 7 '表示抑制 8 With Application 9 .ScreenUpdating = False 10 .DisplayAlerts = False 11 End With 12 13 '配列作成 14 Dim ws1rng As Range, ws1data As Variant 15 With ThisWorkbook.Sheets("sheets1") 16 Set ws1rng = .Range("A1", .Cells.SpecialCells(xlCellTypeLastCell)) 17 ws1data = ws1rng.Value 18 End With 19 Dim ws2rng As Range, ws2data As Variant 20 With ThisWorkbook.Sheets("sheets2") 21 Set ws2rng = .Range("A1", .Cells.SpecialCells(xlCellTypeLastCell)) 22 ws2data = ws2rng.Value 23 End With 24 25 '辞書作成 26 Dim d, i, k, v 27 Set d = CreateObject("Scripting.Dictionary") 28 For i = 3 To UBound(ws2data, 1) 29 k = ws2data(i, 7) & ws2data(i, 23) 30 v = Array(ws2data(i, 25), ws2data(i, 26), ws2data(i, 27), ws2data(i, 28)) 31 d(k) = v 32 Next 33 34 '辞書適用 35 For i = 3 To UBound(ws1data, 1) 36 k = ws1data(i, 2) & ws1data(i, 3) 37 If d.Exists(k) Then ws1data(i, 5) = d(k)(0) 38 Next 39 40 '配列出力 41 ws1rng.Value = ws1data 42 43 '抑制解除 44 With Application 45 .ScreenUpdating = True 46 .DisplayAlerts = True 47 End With 48 49 '終了時間 50 ET = Timer - ST 51 MsgBox Round(ET / 60, 0) & "." & Round(ET Mod 60, 0) 52 53End Sub 54

投稿2021/07/17 00:39

編集2021/07/17 03:13
jinoji

総合スコア4592

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

Xail2222様の回答と被る部分がありますが、、、、

エクセルのVBAで遅くなる原因は、セルを一つずつ参照、更新することです。
また、2重ループも件数が多いと遅くなります。

今回のコードでの重い処理は、主に下記の三つです。

  1. セルを一つずつ参照している。
  2. セルを一つずつ更新している。
  3. 2重ループで検索している。

高速化対策としてそれぞれに下記になります。

  1. セル範囲を配列に格納して配列を参照する。
  2. 配列を更新して、更新した配列をセル範囲に一気に代入する。
  3. 連想配列(Dictionary)で検索する。

上記の方針で質問のコードを書き換えると下記のようになります。

vba

1Sub 一致転記() 2 3 Dim ST As Single, ET As Single 4 5 Dim wb As Workbook 6 Set wb = ThisWorkbook 7 8 Dim ws1 As Worksheet, ws2 As Worksheet 9 Set ws1 = wb.Sheets("sheet1") 10 Set ws2 = wb.Sheets("sheet2") 11 12 13 Dim lRow1 As Long, lRow2 As Long 14 15 Dim Dic As Object 16 17 Set Dic = CreateObject("Scripting.Dictionary") 18 19 lRow1 = ws1.Cells(Rows.Count, "B").End(xlUp).Row 20 lRow2 = ws2.Cells(Rows.Count, "B").End(xlUp).Row 21 22 Application.ScreenUpdating = False 23 Application.DisplayAlerts = False 24 25 ST = Timer 26 27 '検索値の文字列、日付、また、転記データをそれぞれ配列に格納 28 With ws2 29 Dim aryStr, aryDate, aryValue 30 aryStr = .Cells(2, "G").Resize(lRow2 - 1).Value 31 aryDate = .Cells(2, "W").Resize(lRow2 - 1).Value 32 aryValue = .Cells(2, "Y").Resize(lRow2 - 1, 4).Value 33 End With 34 35 'Dictionaryに検索値(key)と転記データ(Item)を格納 36 Dim j As Long 37 For j = 1 To UBound(aryStr) 38 Dic.Add aryStr(j, 1) & aryDate(j, 1), Array(aryValue(j, 1), aryValue(j, 2), aryValue(j, 3), aryValue(j, 4)) 39 Next j 40 41 42 '転記先の検索値とセル範囲を配列にそれぞれ格納 43 Dim aryKey1, aryValue1, rngValue1 As Range 44 aryKey1 = ws1.Cells(3, "B").Resize(lRow1 - 2, 2).Value 45 Set rngValue1 = ws1.Cells(3, "E").Resize(lRow1 - 2, 4) '転記先セル範囲 46 aryValue1 = rngValue1.Value 47 48 49 Dim k As Long, Key As String 50 For k = 1 To UBound(aryKey1) 51 'Dictionaryを検索して一致したら、対応する配列データを更新 52 Key = aryKey1(k, 1) & aryKey1(k, 2) 53 If Dic.Exists(Key) Then 54 55 aryValue1(k, 1) = Dic.Item(Key)(0) 56 aryValue1(k, 2) = Dic.Item(Key)(1) 57 aryValue1(k, 3) = Dic.Item(Key)(2) 58 aryValue1(k, 4) = Dic.Item(Key)(3) 59 60 End If 61 Next k 62 63 '更新した配列をセル範囲に代入 64 rngValue1.Value = aryValue1 65 66 67 ET = Timer 68 ET = ET - ST 69 70 Application.ScreenUpdating = True 71 Application.DisplayAlerts = True 72 73 MsgBox Round(ET / 60, 0) & "." & Round(ET Mod 60, 0) 74 75End Sub

複数列をどのように転記するか不明なので、とりあえず、
Y列からAB列までをsheet1のE列以降に代入するようにしています。

投稿2021/07/16 18:37

hatena19

総合スコア34075

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

0

配列の速さについての基本的なことは、こちらの解説がわかりやすいとおもいます。
http://officetanaka.net/excel/vba/speed/s11.htm

Xail2222様も言うとおり、セルへの代入を一個ずつやっていたら配列使っても大して変わらないということですね。一括で代入することで一気に速くなります。

投稿2021/07/15 23:15

Usirow

総合スコア364

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問