Страницы: 1 2 След.
RSS
Ускорить работу макроса сопоставления двух таблиц, Оптимизация кода VBA
 
Добрый день, уважаемые коллеги!
Столкнулся со следующей проблемой. Есть 2 листа (№1 и №2) Excel с разным (но фиксированным) количеством столбцов и очень большим (и не фиксированным) количеством строк. В листе №1 количество строк 50000-150000, в листе №2 - до 100000. В каждом листе есть заполненный столбец уникальных идентификаторов (строка из 12 цифр), уникальность и наличие значения в нужном формате в этом столбце гарантируются.
Есть макрос, который проверяет наличие идентификаторов из Листа№2 в Листе№1 и при наличии совпадения пишет сводные данные в Лист №3.
Макрос работает, но адски медленно: при 118000 строк в Листе №1 и 50000 строк в Листе №2 выполнение макроса занимает у меня около 1.5-2 часов.
Я попробовал переписать его так, чтобы в цикле заполнялся двумерный динамический массив, и потом этот массив вставлялся целым куском в Лист №3, однако, сколько-нибудь заметного прироста производительности это не дало, т.к. из-за невозможности динамического изменения первой размерности динамического массива с сохранением данных, получившийся массив приходится транспонировать перед вставкой - тоже в цикле, т.к. Application.transpose() с такими количествами строк не работает.
Пробовал отключать/останавливать фоновые процессы вроде обновления экрана - вот до 1,5-2 часов получилось упасть, было дольше. Вопрос, можно ли разогнать макрос ещё больше, хотя бы в 2-3 раза?
В этой теме http://www.planetaexcel.ru/forum/index.php?PAGE_NAME=message&FID=1&TID=77861&... подсмотрел идею поделить массив на блоки и работать с ними, но в моём случае это не прокатит, т.к. строки между собой не группируются, и все являются уникальными.

Буду признателен за любые дельные советы по ускорению алгоритма. Сам сейчас думаю в направлении того, чтобы отсортировать самый большой из списков идентификаторов и проходить по нему алгоритмом быстрого поиска, а не перебором.

P.S. В примере, разумеется, все данные вымышленные и длина списков минимальная. Макрос там же.
Изменено: Irregular Expression - 13.11.2017 13:23:54
 
Попробуйте на 'боевых' такой вариант (время не замерял)
Код
Sub CompareTwoSheets()
Dim arr1(), arr2(), arr3(), I&, dic, N&
On Error Resume Next
With Worksheets("Лист №1")
    lRow = .Cells(.Rows.Count, 2).End(xlUp).Row
    lClm = .Cells(1, .Columns.Count).End(xlToLeft).Column
    arr1 = .Range(.Cells(2, 1), .Cells(lRow, lClm)).Value
End With
Set dic = CreateObject("Scripting.Dictionary")
For I = 1 To UBound(arr1)
    dic.Add CStr(arr1(I, 2)), arr1(I, 1) & "^" & arr1(I, 3) & "^" & arr1(I, 4)
Next
With Worksheets("Лист №2")
    lRow = .Cells(.Rows.Count, 2).End(xlUp).Row
    lClm = .Cells(1, .Columns.Count).End(xlToLeft).Column
    arr2 = .Range(.Cells(2, 1), .Cells(lRow, lClm)).Value
End With
For I = 1 To UBound(arr2)
    If dic.Exists(CStr(arr2(I, 2))) Then
        ReDim Preserve arr3(7, N)
        iStr = Split(dic(CStr(arr2(I, 2))), "^")
        arr3(0, N) = iStr(0)
        arr3(1, N) = Format(CStr(arr2(I, 2)), "000000000000")
        arr3(2, N) = iStr(1)
        arr3(3, N) = arr2(I, 1)
        arr3(4, N) = arr2(I, 3)
        arr3(5, N) = arr2(I, 4)
        arr3(6, N) = iStr(2)
        N = N + 1
    End If
Next
Worksheets("Лист №3").Range("A2").Resize(UBound(arr3, 2) + 1, 7) = Application.Transpose(arr3)
End Sub
Согласие есть продукт при полном непротивлении сторон
 
Application.Transpose() может не потянуть большие объёмы.
И ReDim Preserve ворует время, если часто и помногу...
Обычно если возможно мы используем тот же исходный или проверяемый массив, складывая всё в начало массива, затем эту шапку легко выгрузить.
Т.к. файл взять не могу - без кода...
 
Код
Sub qq()
    Dim ar1, ar2, ar3
    With Worksheets("Лист №1").Cells(1, 1).CurrentRegion: ar1 = .Offset(1).Resize(.Rows.Count - 1).Value: End With
    With Worksheets("Лист №2").Cells(1, 1).CurrentRegion: ar2 = .Offset(1).Resize(.Rows.Count - 1).Value: End With
    With CreateObject("Scripting.Dictionary")
        If UBound(ar1) > UBound(ar2) Then
            ReDim ar3(1 To UBound(ar1), 1 To 7)
            For i = 1 To UBound(ar1)
                .Item(ar1(i, 2)) = i
            Next
            For i = 1 To UBound(ar2)
                If .exists(ar2(i, 2)) Then
                    k = k + 1
                    ar3(k, 1) = ar1(.Item(ar2(i, 2)), 1)
                    ar3(k, 2) = "'" & ar2(i, 2)
                    ar3(k, 3) = ar1(.Item(ar2(i, 2)), 3)
                    ar3(k, 4) = ar2(i, 3)
                    ar3(k, 5) = ar2(i, 1)
                    ar3(k, 6) = ar2(i, 4)
                    ar3(k, 7) = ar1(.Item(ar2(i, 2)), 4)
                End If
            Next
        Else
            ReDim ar3(1 To UBound(ar2), 1 To 7)
            For i = 1 To UBound(ar2)
                .Item(ar2(i, 2)) = i
            Next
            For i = 1 To UBound(ar1)
                If .exists(ar1(i, 2)) Then
                    k = k + 1
                    ar3(k, 5) = ar2(.Item(ar1(i, 2)), 1) '5
                    ar3(k, 2) = "'" & ar1(i, 2)
                    ar3(k, 4) = ar2(.Item(ar1(i, 2)), 3) '4
                    ar3(k, 3) = ar1(i, 3) '3
                    ar3(k, 1) = ar1(i, 1) '1
                    ar3(k, 7) = ar1(i, 4) '7
                    ar3(k, 6) = ar2(.Item(ar1(i, 2)), 4) '6
                End If
            Next
        End If
    End With
    Worksheets("Лист №3").Cells(2, 1).Resize(k, 7) = ar3
End Sub
Изменено: RAN - 13.11.2017 14:42:29
 
Sanja, спасибо, идея со словарём мне нравится. Во всяком случае, замена итераций в цикле поиском по коллекции может дать некоторый бонус в скорости. Как протестирую - напишу результат.

Однако, Application.Transpose(), к сожалению, не вариант, т.к. не работает более чем с 65536 элементами (писал об этом выше). А я не могу быть уверен в том, сколько строк у меня совпадёт - может ни одной, может все ~100000. Реализовать транспонирование динамического массива собственной функцией, а не Transpose() пробовал, но это ещё один перебор циклом, который почти сводит на нет преимущества замены записи прямо на лист вставкой динамического массива. Ну, или каким-то образом попытаться вставлять по <= 65536 элементов %)...
Изменено: Irregular Expression - 13.11.2017 14:37:03
 
А если это сделать  с помощью MQ средствами SQL.
Изменено: skais675 - 13.11.2017 14:48:02
 
Доброе время суток
Версия на Power Query и до кучи - ещё одна на SQL (для SQL сохранить в папку C:\Path)
Успехов.
 
RAN, skais675, спасибо, попробую - потом отпишусь о результатах теста.

Hugo, самого посещают такие мысли, и в связи с этим думаю: а если сразу объявить динамический массив максимальной возможной длины (у меня, например, по определению, не может быть больше строк вывода, чем строк в самом маленьком списке) и потом его один раз обрезать до заполненной части (или даже не обрезать, хоть это и грязновато, зато сэкономим на транспонировании), не получится ли гораздо быстрее, чем с ReDim Preserve 100k раз?
Изменено: Irregular Expression - 13.11.2017 14:55:02
 
Получится быстрее.
Я написал что можно не объявлять, и не обрезать - кладём в любой существующий, выгружаем  только что положили.
 
Код
Option Explicit
Sub test()
Dim dic As Object
Dim arr(), i&, j&, x&
Set dic = CreateObject("Scripting.Dictionary")
arr = Лист1.UsedRange.Value
For i = 1 To UBound(arr)
    dic.Item(CStr(arr(i, 2))) = 0
Next i
arr = Лист2.UsedRange.Value
For i = 1 To UBound(arr)
    If dic.Exists(CStr(arr(i, 2))) Then
        j = j + 1
        For x = 1 To UBound(arr, 2)
            arr(j, x) = arr(i, x)
        Next
    End If
Next i
With Лист3
    .Cells.ClearContents
    .Columns(2).NumberFormat = "@"
    .Range("a1").Resize(j, UBound(arr, 2)) = arr
    .Cells.EntireColumn.AutoFit
End With
End Sub
Изменено: Nordheim - 13.11.2017 15:26:11
"Все гениальное просто, а все простое гениально!!!"
 
С нетерпением ждем от автора темы публикации результатов измерения быстродействия на реальных данных.
Владимир
 
Я ставлю на Nordheim :)
 
sokol92, +1.
Сделал пример с 100к строк на 1 листе и 50к на 2 листе (4 столбца) около 3 секунд :)
Хотелось бы услышать результат на боевом файле
Изменено: Nordheim - 13.11.2017 15:41:05
"Все гениальное просто, а все простое гениально!!!"
 
Nordheim Если Вы сравнивали одни и те же строки (100k), то к чему это все, на боевом согласен - нужно проверить и после этого радоваться.
Не проверял, но думаю sql все равно быстрее будет.
Изменено: skais675 - 13.11.2017 15:44:07
 
Строки сделал все разные поэтому цикл пробегал по всему массиву.
Изменено: Nordheim - 13.11.2017 15:47:49
"Все гениальное просто, а все простое гениально!!!"
 
Тоже сделал. 200000 в одной таблице и столько же в другой. Общих по Идентификатору 99551. Excel 2010 32bit. Power Query создал таблицу за 11 секунд, SQL 24 секунды.
На 100000 и 50000 (общих 12420), PQ 3 секунды, SQL 5 секунд.
 
Excel 2010
На 115000 и 52000 (общих 34420) 2.25 сек.  :D  
Изменено: Nordheim - 13.11.2017 15:54:42
"Все гениальное просто, а все простое гениально!!!"
 
А где же автор?
"Все гениальное просто, а все простое гениально!!!"
 
Андрей, а как мой на тех-же данных?
 
RAN,
На моих 2.41
"Все гениальное просто, а все простое гениально!!!"
 
В принципе разницы практически никакой.
"Все гениальное просто, а все простое гениально!!!"
 
Но всёж медленнее. И памяти занято больше :(
 
Цитата
Hugo написал:
Но всёж медленнее
И всеж итоговый массив содержит данные с двух листов.   ;)
 
Для "чистоты эксперимента" код в сообщении #10 должен быть приведен в соответствие с условиями #1 (набор столбцов на листе Лист3)
Владимир
 
Цитата
Irregular Expression написал:
Есть макрос, который проверяет наличие идентификаторов из Листа№2 в Листе№1 и при наличии совпадения пишет сводные данные в Лист №3.
- вот ведь условия. Что не так?
 
Макрос в #10 формирует не все требуемые столбцы листа Лист3 (см. прилагаемый файл в #1)
Владимир
 
А, тогда да, я пас - файл глянуть не могу.
Тогда время чуть добавится, если чуть больше в массив нужно собирать, и выгружать.
 
Код из четвертого сообщения в столбец район вставляет фамилии. Если таблицы идентичны, то итоговый лист должен быть с идентичными столбцами, тогда и сложный макрос не нужен
"Все гениальное просто, а все простое гениально!!!"
 
Вот еще вариант решения без использования дополнительных средств и надстроек.
При количестве записей "Лист №1" ~100000, "Лист №2" ~ 50000  - на доступных машинах результат был от 1 до 2 секунд.
 
Итак, отчитываюсь по порядку. Для теста мною была взята копия рабочей базы, где в Листе №1 было 157899 строк, а в листе №2 - 30991 строка.

Макрос от Nordheim
1) 5,88 секунд (это круто, имхо!)
2) Найдено 1167 совпадений (все имеющиеся).
3) Выгружены не все данные (только 2 из 6 столбцов), + потёрлась шапка, но это мелочи :).

Макрос от RAN,
1) 5,5 секунд (глазам не поверил :)!)
2) Найдено 1167 совпадений (все имеющиеся).
3) Выгружены данные по всем 7 столбцам, шапка листа №3 уцелела :).

Макрос от Sanja,
1) 1,84 секунды :)
2) Не найдено и не выгружено ничего :(. Отключив команду On Error Resume Next, видно, что строка    
Код
dic.Add CStr(arr1(I, 2)), arr1(I, 1) & "^" & arr1(I, 3) & "^" & arr1(I, 4)
кидает Subscript out of range error. В деталях надо разбираться

Решение от skais675,
1) 34 секунды (засекать пришлось по часам, а не таймером VBA, так что погрешность выше)
2) Данные выгрузились полностью, правда, идентификаторы по 2 раза, но это мелочи :).

Решение от Андрей VG протестировать не удалось Excel стал ругаться и требовать админский доступ, которого у меня на работе нет. Впрочем, по ряду причин (сводимые базы насквозь кривые и надо иметь возможность быстро адаптировать к ним код, а не их к листу с соединением) решение нужно именно макросом.

Таким образом, самый быстрый и качественный результат показал макрос RAN, которому от меня большой респект и искренняя признательность :)!
Всем спасибо!
Изменено: Irregular Expression - 13.11.2017 20:41:02
Страницы: 1 2 След.
Наверх