Сравните два листа и выделите различия на каждом листе — является ли цикл единственным способом?

#arrays #excel #vba #performance #loops

#массивы #excel #vba #Производительность #циклы

Вопрос:

Я обращаюсь за советом в отношении повышения производительности для большого набора данных (примерно 175 тыс. строк на каждом листе и 39 столбцов A: AM (сравнение листа 1 и листа 2). Эти листы экспортируются из access, а мой VBA записывается в Access. У меня есть кодирование, в котором используется цикл «для», который проверяет ячейку за ячейкой и подчеркивает, есть ли несоответствие в каждой относительной ячейке.

Мой вопрос — ускорит ли использование функции массива или словаря процесс? Если да, можете ли вы предоставить хлебные крошки, чтобы пролить свет на то, как выполнить? В настоящее время выполнение этого кода занимает около 3 часов. Примерно 2 минуты на экспорт из Access в Excel, а остальное время представляет собой цикл и выделение.

В качестве примечания — я написал код для условного форматирования, и он работает невероятно быстро. Основная проблема заключается в том, что я не могу скопировать / вставить листы с выделенными ячейками в новые листы, оставив условия позади. Мне было бы интересно услышать, нашел ли кто-нибудь способ маневрировать этим минным полем.

Код ниже:

 DoCmd.SetWarnings False

            Dim xlapp As Excel.Application
            Dim xlbook As Excel.Workbook
            Dim xlSheet, xlSheetPre, xlSheetPost As Excel.Worksheet
            Dim SQL As String
            Dim rs1 As DAO.Recordset
            Dim iSheet As Long, iRow As Long, iCol As Long, cols As Long
            Dim MaxLastRow As Long, MaxLastCol As Long
            Dim LastRow1 As Range, LastRow2 As Range
            Dim LastCol1 As Range, LastCol2 As Range
            Dim i As Integer
            

            SQL = "SELECT * From Pre"
            
            Set rs1 = CurrentDb.OpenRecordset(SQL, dbOpenSnapshot)
            Set xlapp = Excel.Application
                xlapp.Visible = True
            Set xlbook = xlapp.Workbooks.Add
                i = 1
            Do
                Set xlSheet = Sheets.Add(after:=Sheets(Sheets.Count))
                i = i   1
            Loop Until i = 2 ' the number 2 represents how many sheets you want to add to the 
            workbook
            
            Set xlSheet = xlbook.Worksheets(1) ' Finds worksheet (1) and begins loading data from SQL 
            table above
            
            With xlSheet
            .Name = "Pre" ' Name the worksheet
            .Range("a1:am1").Font.Bold = True 'Converts headers in row 1 to 
             bold font
            .Range("A2").CopyFromRecordset rs1 'Copies all data from selected 
             table (SQL)into your worksheet
            .Range("a1").AutoFilter ' Adds filter to your columns
            .Cells.Columns.AutoFit ' Adjust worksheet column width to autofit 
             your data
            .Range("a1:am1").Interior.ColorIndex = 37 ' Changes color of cell
            ' This loop reads all headers in your access table and places 
             them on worksheet
            For cols = 0 To rs1.Fields.Count - 1
                .Cells(1, cols   1).Value = rs1.Fields(cols).Name
            Next
            
            
            End With
            
            SQL = "SELECT * From Post"
            Set rs1 = CurrentDb.OpenRecordset(SQL, dbOpenSnapshot)
            Set xlSheet = xlbook.Worksheets(2)
            
            With xlSheet
            .Name = "Post" ' Name the worksheet
            .Range("a1:am1").Font.Bold = True 'Converts headers in row 1 to 
             bold font
            .Range("A2").CopyFromRecordset rs1 'Copies all data from selected 
             table (SQL)into your worksheet
            .Range("a1").AutoFilter ' Adds filter to your columns
            .Cells.Columns.AutoFit ' Adjust worksheet column width to autofit 
             your data
            .Range("a1:am1").Interior.ColorIndex = 37 ' Changes color of cell
            ' This loop reads all headers in your access table and places 
             them on worksheet
            ' This loop reads all headers in your access table and places them on worksheet
            For cols = 0 To rs1.Fields.Count - 1
            .Cells(1, cols   1).Value = rs1.Fields(cols).Name
            Next

            
            End With
            
            Set xlSheetPre = xlbook.Worksheets(1)
            Set xlSheetPost = xlbook.Worksheets(2)
            
            Set LastRow1 = xlSheetPre.Cells.Find("*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious)
            Set LastRow2 = xlSheetPost.Cells.Find("*", SearchOrder:=xlByRows, SearchDirection:=xlPrevious)
            
            If Not LastRow1 Is Nothing Then
                If Not LastRow2 Is Nothing Then
                    If LastRow1.Row > LastRow2.Row Then
                        MaxLastRow = LastRow1.Row
                    Else
                        MaxLastRow = LastRow2.Row
                    End If
                Else
                    MaxLastRow = LastRow1.Row
                End If
            Else
                MaxLastRow = LastRow2.Row
            End If
            
            Set LastCol1 = xlSheetPre.Cells.Find("*", SearchOrder:=xlByColumns, SearchDirection:=xlPrevious)
            Set LastCol2 = xlSheetPost.Cells.Find("*", SearchOrder:=xlByColumns, SearchDirection:=xlPrevious)
            
            If Not LastCol1 Is Nothing Then
                If Not LastCol2 Is Nothing Then
                    If LastCol1.Column > LastCol2.Column Then
                        MaxLastCol = LastCol1.Column
                    Else
                        MaxLastCol = LastCol2.Column
                    End If
                Else
                    MaxLastCol = LastCol1.Column
                End If
            Else
                MaxLastCol = LastCol2.Column
            End If
            
            For iRow = 2 To MaxLastRow 'starting loop on row 2
                For iCol = 4 To MaxLastCol 'starting loop on column 4
                    If xlSheetPre.Cells(iRow, iCol).Value <> xlSheetPost.Cells(iRow, iCol).Value Then
                    xlSheetPre.Cells(iRow, iCol).Interior.ColorIndex = 4
                    xlSheetPost.Cells(iRow, iCol).Interior.ColorIndex = 4
            
                    End If
                    
                Next iCol
            Next iRow
            
            SubExit:
            On Error Resume Next
            
            rs1.Close
            Set rs1 = Nothing
            DoCmd.SetWarnings True
            
            Exit Sub
 

Комментарии:

1. Ваш код помещает заголовки только в листы Excel? Где данные? Ваш код будет выполняться намного быстрее, если вы загружаете данные в массивы и сравниваете массивы: доступ к данным по ячейкам относительно медленный.

2. Также неясно, как LastRow1 может быть etc Nothing — вы уже написали заголовки на листе, так что этого никогда не должно произойти.

3. Спасибо, что назвали это. Я пропустил свои строки форматирования и, похоже, изначально удалил слишком много. С помощью LastRow1 / 2 я подумал, что если LastRow1 отличается от LastRow2, отрегулируйте лист, у которого меньше, чтобы все сравнения были 1 к 1 между листами.

4. Имеют ли таблицы pre и post общий первичный ключ?

5. Да, столбец A имеет ключ, который выравнивается (a1 на pre = a1 на post) в Access перед экспортом в Excel. Если у Pre есть ключ, которого нет у post, тогда будет выделен ключ Pre.

Ответ №1:

Попробуйте уменьшить количество записей, которые вам нужно сравнить, извлекая только те, которые имеют различия. Есть несколько способов, которыми вы могли бы сделать это в SQL, но в качестве доказательства концепции это сравнивает каждый столбец по очереди, создавая временную таблицу ключей, которая используется для фильтрации извлеченных записей.

 Option Compare Database
Option Explicit

Sub DumpToExcel()

    Dim n As Integer, SQL As String, fname
    ' field names
    fname = Array("", "F1", "F2", "F3", "F4", "F5", _
                  "F6", "F7", "F8", "F9", "F10")

    ' identify diff records
    Debug.Print UBound(fname)
    DoCmd.SetWarnings False
    For n = 1 To UBound(fname)
        If n = 1 Then ' create table
            SQL = " SELECT post.ID, """ amp; n amp; """ AS Col INTO tmp"
        Else
            SQL = " INSERT INTO tmp" amp; _
                  " SELECT post.ID, """ amp; n amp; """ AS Col"
        End If
        SQL = SQL amp; _
            " FROM Post LEFT JOIN pre ON Post.id = pre.id" amp; _
            " WHERE NZ([pre].[" amp; fname(n) amp; "],"")<>NZ([post].[" amp; fname(n) amp; "],"");"
        
        DoCmd.RunSQL SQL

    Next
    DoCmd.SetWarnings True

    ' extract data
    Dim rs1 As DAO.Recordset

    SQL = " SELECT * FROM pre" amp; _
          " WHERE (((pre.[ID]) In " amp; _
          " (SELECT DISTINCT(ID) FROM tmp )));"

    Set rs1 = CurrentDb.OpenRecordset(SQL, dbOpenSnapshot)

    ' create excel
    Dim xlapp As Excel.Application, xlBook As Excel.Workbook
    Dim xlSheet As Excel.Worksheet

    Set xlapp = Excel.Application
    xlapp.Visible = True
    Set xlBook = xlapp.Workbooks.Add
    
    'add sheets as required
    Do While xlBook.Sheets.Count < 2
        xlBook.Sheets.Add
    Loop

    ' copy recordset to sheet
    xlBook.Sheets(1).Range("A2").CopyFromRecordset rs1
    MsgBox "Done"

End Sub
 

Комментарии:

1. Спасибо. Я быстро скопировал кодировку, но получаю всплывающие окна с сообщением «Введите значение параметра -«. ИДЕНТИФИКАТОР», за которым следует «pre.id «. Я рассмотрю этот код, чтобы увидеть, смогу ли я понять это, и дам вам знать.

2. @Erik Измените идентификатор на любое ваше поле первичного ключа

Ответ №2:

«Мой вопрос — ускорит ли использование функции массива или словаря процесс?»

Исходя из опыта, ответ: нет, этого не будет. Причина в том, что вам придется читать ячейки на листе, чтобы в первую очередь заполнить массив или словарь, поэтому… На самом деле это цикл, и вам нужно организовать данные (обычно путем правильной сортировки списков, таблиц, диапазонов и т. Д.), Чтобы свести к минимуму поиск совпадающих записей (строк), чтобы ваши циклы выполнялись быстрее.

Если у вас есть Access, вы можете сделать это напрямую с наборами записей, при условии, что сетевая безопасность вашей компании не мешает перемещению внутри объектов набора записей (мой действительно мешает, и при этом очень сильно — Tanium — настоящая угроза!)

Комментарии:

1. Исходя из моего опыта — если бы вы выполняли вложенный цикл для сравнения записей — использование словаря было бы намного быстрее. Это правда, что загрузка словаря сопряжена с некоторыми накладными расходами, но избежать вложенного цикла того стоит.

2. @TimWilliams: Как вы храните записи с несколькими полями с несколькими типами данных в словаре?

3. Есть ли какое-либо преимущество в использовании словаря против массива?

4. @Erik_U — они разные, это зависит от того, что вам нужно и каковы данные; погуглите оба и убедитесь сами, пожалуйста.

5. Вы можете сохранить любой тип данных в значении словарной записи, но при использовании в этом сценарии «избежать вложенного цикла» обычно это просто индекс строки, указывающий на номер строки в массиве

Ответ №3:

Вот сравнение на основе массива.

Скомпилировано, но не протестировано:

 Sub Tester()
    
    Dim xlapp As Excel.Application
    Dim xlbook As Excel.Workbook
    Dim xlSheet, wsPre As Excel.Worksheet, wsPost As Excel.Worksheet
    Dim rowsPost As Long, rowsPre As Long, rowsMax As Long
    Dim colsPre As Long, colsPost As Long, colsMax As Long, flag As Boolean
    Dim r As Long, c As Long, rngPre As Range, rngPost As Range, arrPre, arrPost
    
    DoCmd.SetWarnings False
    
    Set xlapp = New Excel.Application 'forgot "New" here?
    xlapp.Visible = True
    Set xlbook = xlapp.Workbooks.Add()
    Do While xlbook.Worksheets.Count < 2 'how many sheets you need in the Workbook
        xlbook.Sheets.Add
    Loop
    
    Set wsPre = xlbook.Worksheets(1)
    Set wsPost = xlbook.Worksheets(2)
    
    PutInWorksheet "SELECT * From Pre", wsPre, "Pre"
    PutInWorksheet "SELECT * From Post", wsPost, "Post"
    
    Set rngPre = wsPre.Range("A1").CurrentRegion   'data ranges
    Set rngPost = wsPost.Range("A1").CurrentRegion

    arrPre = rngPre.Value   'read data to arrays
    arrPost = rngPost.Value
    
    rowsPre = UBound(arrPre, 1) 'compare array bounds...
    rowsPost = UBound(arrPost, 1)
    rowsMax = xlapp.Max(rowsPre, rowsPost)
    
    colsPre = UBound(arrPre, 2)
    colsPost = UBound(arrPost, 2)
    colsMax = xlapp.Max(colsPre, colsPost)
    
    For r = 2 To rowsMax
        flag = (r > rowsPre) Or (r > rowsPost) 'flag whole row if have run out of data in one set...
        If flag Then
            FlagRanges rngPre.Cells(r, 1).Resize(1, colsMax), _
                       rngPost.Cells(r, 1).Resize(1, colsMax)
        Else
            'have two rows to compare
            For c = 1 To colsMax
                flag = (c > colsPre) Or (c > colsPost) 'run out of cols in one dataset?
                If Not flag Then
                    flag = arrPre(r, c) <> arrPost(r, c) 'compare data
                End If
                If flag Then
                    'no data to compare, or data does not match
                    FlagRanges rngPre.Cells(r, c), rngPost.Cells(r, c)
                End If
            Next c
        End If
    Next r
End Sub

Sub FlagRanges(rng1 As Excel.Range, rng2 As Excel.Range)
    Const CLR_INDX = 4
    rng1.Interior.ColorIndex = CLR_INDX
    rng2.Interior.ColorIndex = CLR_INDX
End Sub

'run a query and put the results on a worksheet starting at A1
Sub PutInWorksheet(SQL As String, ws As Excel.Worksheet, _
    Optional newName As String = "")
    Dim f, c As Excel.Range, rs As dao.Recordset
    
    If Len(newName) > 0 Then ws.Name = newName
    Set rs = CurrentDb.OpenRecordset(SQL, dbOpenSnapshot)
    Set c = ws.Range("A1")
    For Each f In rs.Fields
        c.Value = f.Name
        c.Font.Bold = True
    Next f
    ws.Range("A2").CopyFromRecordset rs
    rs.Close
End Sub

 

Комментарии:

1. Спасибо, Тим. Один вопрос, когда я компилирую, я получаю сообщение об ошибке компиляции: «метод или элемент данных не найден». для «rowsMax = Application.Max (rowsPre, rowsPost)». Это проблема с инструментом> ссылкой?

2. чтобы быть более конкретным, .Max принимает ошибку.

3. Извините, я писал в Excel, поэтому вам понадобится xlapp.Max

4. Отлично, xlApp.Max исправлена ошибка компиляции. Похоже, я столкнулся с еще одной проблемой во вложенной рабочей таблице: «Для каждого f в rs1.fields» указывается, что требуется объект.

5. Для каждого f в rs.fields