iT邦幫忙

0

改善VBA執行lookup時速度緩慢

  • 分享至 

  • xImage

程式可以執行,但資料有好幾千筆,電腦跑了20分鐘還是沒結果(白畫面),尋求各位的協助完成資料!!

先講想要改的程式碼:
總共有兩張表分別是資料的aaa與資料輸出的表,aaa有3行分別是type(有三種FCST、SO和Ship)、產品ID、值;資料輸出的表有三行,分別是產品ID、N或公司名、要輸出值的行

aaa!A:A= """ & cate1 & """ 第一個條件,即FCST、SO或Ship
aaa!B:B= """ & Bnum & """ 第二個條件,產品ID
aaa!E:E 尋找的值
ifna(,0) 因為會有空值無法比較,故讓空值為0

Application.Evaluate("=ifna(Lookup(1,0/((aaa!A:A= """ & cate1 & """ ) * (aaa!B:B= """ & Bnum & """ )), aaa!E:E),0)")

用for迴圈在輸出表由第二筆ID順著往下找,跑到最後一筆資料為止
ex: 若for到4,尋找輸出表中第一行第四列的ID並與表aaa第二行比對,a得到表aaa的FCST值、b得到表aaa的SO值、c得到表Ship的FCST值,當得到FCST、SO和Ship的值後會進行比較,若輸出表第三行相對的值為N,則會再做比對,若不是則直接傳a也就是FCST的值到輸出表的相對應的第四行。

完整程式碼

i = 1
cate1 = "FCST"   
cate2 = "SO"
cate3 = "Ship"
row1 = Cells(Rows.Count, 1).End(xlUp).Row


'Debug.Print row1
Do Until i >= row1
i = i + 1
Debug.Print i
Bnum = Range("a" & i)
a = Application.Evaluate("=ifna(Lookup(1,0/((aaa!A:A= """ & cate1 & """ ) * (aaa!B:B= """ & Bnum & """ )), aaa!E:E),0)")
b = Application.Evaluate("=ifna(Lookup(1,0/((aaa!A:A= """ & cate2 & """ ) * (aaa!B:B= """ & Bnum & """ )), aaa!E:E),0)")
c = Application.Evaluate("=ifna(Lookup(1,0/((aaa!A:A= """ & cate3 & """ ) * (aaa!B:B= """ & Bnum & """ )), aaa!E:E),0)")



If Cells(i, 3) = "N" Then
If a > b + c Then
Cells(i, 4) = b + c
Else
Cells(i, 4) = a
End If
Else
Cells(i, 4) = a
End If

Loop

測試檔:https://www.mediafire.com/file/u0e4booej78jhdr/Test.xlsx/file
補充:FCST、SO、Ship一個ID只會各出現一次,只會少不會多,除非ID相同公司卻不同

goodnight iT邦研究生 2 級 ‧ 2020-07-24 09:52:51 檢舉
=====請忽略=====
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

2 個回答

3
淺水員
iT邦大師 6 級 ‧ 2020-07-24 11:19:28
最佳解答

這是 Libre Office Calc 可以跑的程式碼,應該可以轉成 Excel 用的
轉換規則我想應該是這樣:

  • ThisComponent.getCurrentController.activesheet => ActiveSheet
  • ThisComponent.Sheets.getByName("aaa") => Sheets("aaa")
  • .getCellByPosition(x, i).string => .Cells(i+1, x+1).Value
  • .getCellByPosition(x, i).Value => .Cells(i+1, x+1).Value
  • 另外我 while 迴圈那邊的 t <> "" ,我不確定 Excel 會怎麼判斷,還是應該要用 Nothing 來判斷,可能也要改一下
Sub Main
	Dim curSheet, dataSheet
	curSheet = ThisComponent.getCurrentController.activesheet
	dataSheet = ThisComponent.Sheets.getByName("aaa")
	
	'先掃描目前資料表(輸出資料表),先建立字典Dict key => index(0開始算)
	Dim dict, count, i, t
	Set dict = CreateObject("Scripting.Dictionary")
	i = 1
	count = 0
	t = curSheet.getCellByPosition(0, i).String
	key = t & "/" & curSheet.getCellByPosition(1, i).string
	while t <> ""
		dict.Add key, count
		count = count + 1
		i = i + 1
		t = curSheet.getCellByPosition(0, i).String
		key = t & "/" & curSheet.getCellByPosition(1, i).String
	wend
	
	'建立陣列Arr,共有 count*3 個 Double 元素(因為有三種)
	Dim arr(count*3) As Double
	
	'開始掃描資料檔,如果符合 key 則存入 Arr[Dict[key]*3+type]
	Dim idx, tpy, val
	i = 1
	t = dataSheet.getCellByPosition(1, i).String
	key = t & "/" & dataSheet.getCellByPosition(3, i).string
	while t <> ""
		if dict.Exists(key) then
			idx = CInt(dict.Item(key))*3
			tpy = dataSheet.getCellByPosition(0, i).String
			val = dataSheet.getCellByPosition(4, i).Value
			if tpy = "FCST" then
				arr(idx) = arr(idx) + val
			elseif tpy = "SO" then
				arr(idx+1) = arr(idx+1) + val
			elseif tpy = "Ship" then
				arr(idx+2) = arr(idx+2) + val
			else
			endif
		endif
		i = i + 1
		t = dataSheet.getCellByPosition(1, i).String
		key = t & "/" & dataSheet.getCellByPosition(3, i).string
	wend
	
	'將 Arr 寫入到目前資料表
	Dim a,b,c
	for i=1 to count
		idx=(i-1)*3
		a=arr(idx)
		b=arr(idx+1)
		c=arr(idx+2)
		if curSheet.getCellByPosition(2, i).String = "N" And a > b + c then
			t=b+c
		else
			t=a
		endif
		curSheet.getCellByPosition(3, i).Value=t
		'curSheet.getCellByPosition(4, i).String= a & "/" & b & "/" & c
	next i
End Sub
看更多先前的回應...收起先前的回應...
h93243 iT邦新手 5 級 ‧ 2020-07-24 13:28:54 檢舉
Dim arr(count*3) As Double

顯示count要是常數,該怎麼轉換呢?

淺水員 iT邦大師 6 級 ‧ 2020-07-24 13:35:40 檢舉

試試看這樣可不可以

Dim arr() As Double
ReDim arr(count*3)

參考自:https://blog.gtwang.org/programming/excel-vba-array/ 裡面動態陣列的部分

h93243 iT邦新手 5 級 ‧ 2020-07-24 13:45:37 檢舉

這段可以了,請問以下應該改成.Cells(i, x).Value就可以了吧?

 .getCellByPosition(x, i).string => .Cells(i+1, x+1).Value 
淺水員 iT邦大師 6 級 ‧ 2020-07-24 13:50:30 檢舉

Excel 好像是從 1 開始算,而 Calc 是從 0 開始算,所以才有那個 +1
例如儲存格 "B5" 在 Excel 是 Cells(5, 2)
但是在 Calc 是 getCellByPosition(1, 4)

淺水員 iT邦大師 6 級 ‧ 2020-07-24 14:30:54 檢舉

如果不確定空值判斷的方式,建議可以把 while t <> "" 先改成 while t <> "" And i<5000 做測試,確認沒問題再拿掉 And i<5000,避免變成無窮迴圈。

h93243 iT邦新手 5 級 ‧ 2020-07-24 14:34:14 檢舉

跑完了...速度快的嚇人,非常感謝!!
請問這個概念是什麼呢? 通常是用在多條件比對的場合嗎?

淺水員 iT邦大師 6 級 ‧ 2020-07-24 14:47:46 檢舉

一般搜尋資料的函式,為了通用性,並不會假設資料有排序。所以每次搜尋都是一個一個比對。這也是原本的狀況。

假設資料有排序,那麼可以用每次都對切一半,這樣找到資料就快很多。這是可以考慮優化的地方。(雖然我不是用這個)

很多程式都有類似 字典(Dictionary) 的資料結構,這種資料結構插入、搜尋的效率至少都有「對切一半」的效能(視內部實作而定),也就是 https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Clog_2%20N ,用 4096筆做比喻,原本要花4096單位的時間,會變成 https://chart.googleapis.com/chart?cht=tx&amp;chl=%5Clog_2%204096%20%3D%2012 單位的時間。

h93243 iT邦新手 5 級 ‧ 2020-07-24 15:04:28 檢舉

了解,謝謝指點!!

淺水員 iT邦大師 6 級 ‧ 2020-07-24 15:14:16 檢舉

有個可能會發生 bug 的地方先跟你說一下,雖然說目前資料格式應該不會發生,但也許未來會有需要修正的時候。

假設你有其中兩筆資料

Product Customer
xxx/yyy zzz
xxx yyy/zzz

因為我的 key 是用 "/" 做字串連接,所以上面兩筆會產生相同的 key ,也就是 "xxx/yyy/zzz",這樣會發生問題。
未來如果發生了,你再改用其他的字串來連接就可。

h93243 iT邦新手 5 級 ‧ 2020-07-24 17:19:38 檢舉

謝謝提醒 !

1
海綿寶寶
iT邦大神 1 級 ‧ 2020-07-23 20:58:16

試試看
如果可用再來看細節
如果不能用或一樣慢就忽略這程式吧

Sub FindInRange()
    i = 1
    cate1 = "FCST"
    cate2 = "SO"
    cate3 = "Ship"
    row1 = Cells(Rows.Count, 1).End(xlUp).Row
    
    
    'Debug.Print row1
    Do Until i >= row1
        i = i + 1
        Debug.Print i
        Bnum = Range("a" & i)
        
        a = 0
        b = 0
        c = 0
        With Worksheets("aaa").Range("b:b")
            Set Rng = .Find(Bnum, LookIn:=xlValues)
            If Not Rng Is Nothing Then
                firstAddress = Rng.Address
                Do
                    sType = Rng.Offset(0, -1).Value
                    If (sType = "FCST") Then
                        a = Rng.Offset(0, 3).Value
                    ElseIf (sType = "SO") Then
                        b = Rng.Offset(0, 3).Value
                    ElseIf (sType = "Ship") Then
                        c = Rng.Offset(0, 3).Value
                    End If
                    
                    Set Rng = .FindNext(Rng)
                Loop While Not Rng Is Nothing And Rng.Address <> firstAddress
            End If
        End With
                
        
        If Cells(i, 3) = "N" Then
            If a > b + c Then
                Cells(i, 4) = b + c
            Else
                Cells(i, 4) = a
            End If
        Else
            Cells(i, 4) = a
        End If
    
    Loop
End Sub
看更多先前的回應...收起先前的回應...
h93243 iT邦新手 5 級 ‧ 2020-07-23 22:12:14 檢舉

程式可以執行,但結果都是空值(補一下頁面),然後還要再加一個限制條件該放哪呢? Bnum2是輸入值的公司,D:D是aaa中的公司行。然後FCST、SO、Ship三個值都要,找不到的話要補零因為最後還是要比較該回傳哪個值

a = Application.Evaluate("=ifna(Lookup(1,0/((aaa!A:A= """ & cate1 & """ ) * (aaa!B:B= """ & Bnum & """ )* (aaa!D:D= """ & Bnum2 & """ )), aaa!E:E),0)")

https://ithelp.ithome.com.tw/upload/images/20200723/20127959A2m0ZZLGwB.png
https://ithelp.ithome.com.tw/upload/images/20200723/201279591QIBlNSVSU.png

h93243 iT邦新手 5 級 ‧ 2020-07-23 23:08:23 檢舉

除了第二、三格正常之外,第四隔開始程式都找不到表aaa b列的值

淺水員 iT邦大師 6 級 ‧ 2020-07-23 23:54:47 檢舉

h93243 要不要給一下測試用檔案阿?不然幫你測試還要自己建立資料。
PS. 我是幫別人說的,我手邊沒有 Excel 沒法幫忙。

h93243 iT邦新手 5 級 ‧ 2020-07-24 00:11:43 檢舉

程式已修改
只加了 a=0 b=0 c=0
執行結果如下
https://ithelp.ithome.com.tw/upload/images/20200724/20001787s9sbV5Mfyd.png

h93243 iT邦新手 5 級 ‧ 2020-07-24 13:42:24 檢舉

謝謝! 執行完約21分鐘(4261筆),是因為判斷的項目太多所以VBA必須要執行這麼久,還是電腦效能不夠呢? 因為目標是希望在10分鐘內完成

每筆處理時間(21*60)/4261=0.29秒左右
應該還好吧

如果這是實務上碰到的情形
應該會把資料放在資料庫裡
再用 SQL 去處理

淺水員 iT邦大師 6 級 ‧ 2020-07-24 14:21:15 檢舉

4261 筆其實不算多,只是這個程式的時間複雜度是 nA * nB
這也是為什麼我不會想用 Set Rng = .Find(Bnum, LookIn:=xlValues) 的原因。
正常來說目前的需求應該可以做到 n Log n 的時間複雜度。

h93243 iT邦新手 5 級 ‧ 2020-07-24 14:36:06 檢舉

謝謝海綿大的協助!!

我要發表回答

立即登入回答