Excel VBA Performance - 1 million de lignes - supprimer les lignes contenant une valeur, en moins de 1 min

j'essaie de trouver un moyen de filtrer de grandes données et de supprimer des lignes dans une feuille de travail, en moins d'une minute

Le but:

  • trouver tous les enregistrements contenant du texte spécifique dans la colonne 1, et supprimer la ligne entière
  • conservez tous les formatage de cellules (couleurs, police, bordures, largeurs de colonnes) et les formules comme elles sont

.

Données De Test:

Test data:

.

comment le code fonctionne:

  1. il commence par désactiver toutes les fonctions Excel
  2. Si le classeur n'est pas vide et la valeur texte supprimé existe dans la colonne 1

    • copie la plage utilisée de la colonne 1 à un tableau
    • itère sur chaque valeur dans le tableau à l'envers
    • Lorsqu'il trouve une correspondance:

      • ajoute l'adresse de la cellule à une chaîne tmp dans le format "A11,A275,A3900,..."
      • si la variable tmp la longueur est proche de 255 caractères
      • supprime les lignes en utilisant .Range("A11,A275,A3900,...").EntireRow.Delete Shift:=xlUp
      • réinitialise tmp à empty et passe à l'ensemble suivant de lignes
  3. À la fin, il tourne toutes les fonctionnalités d'Excel de retour Sur

.

le problème principal est L'opération de suppression, et la durée totale devrait être inférieure à une minute. Toute solution basée sur le code est acceptable dans la mesure où elle remplit les conditions suivantes: 1 minute.

Cela réduit la portée de très peu de réponses acceptables. Les réponses déjà fournies sont également très courtes et faciles à mettre en œuvre. effectue l'opération en environ 30 secondes, donc il y a au moins une réponse qui fournit une solution acceptable, et d'autres peuvent le trouver utile

.

Ma principale fonction initiale:

Sub DeleteRowsWithValuesStrings()
    Const MAX_SZ As Byte = 240

    Dim i As Long, j As Long, t As Double, ws As Worksheet
    Dim memArr As Variant, max As Long, tmp As String

    Set ws = Worksheets(1)
    max = GetMaxCell(ws.UsedRange).Row
    FastWB True:    t = Timer

    With ws
        If max > 1 Then
            If IndexOfValInRowOrCol("Test String", , ws.UsedRange) > 0 Then
                memArr = .Range(.Cells(1, 1), .Cells(max, 1)).Value2
                For i = max To 1 Step -1

                    If memArr(i, 1) = "Test String" Then
                        tmp = tmp & "A" & i & ","
                        If Len(tmp) > MAX_SZ Then
                           .Range(Left(tmp, Len(tmp) - 1)).EntireRow.Delete Shift:=xlUp
                           tmp = vbNullString

                        End If
                    End If

                Next
                If Len(tmp) > 0 Then
                    .Range(Left(tmp, Len(tmp) - 1)).EntireRow.Delete Shift:=xlUp
                End If
                .Calculate
            End If
        End If
    End With
    FastWB False:   InputBox "Duration: ", "Duration", Timer - t
End Sub

fonctions D'aide (désactiver les fonctions Excel et sur):

Public Sub FastWB(Optional ByVal opt As Boolean = True)
    With Application
        .Calculation = IIf(opt, xlCalculationManual, xlCalculationAutomatic)
        .DisplayAlerts = Not opt
        .DisplayStatusBar = Not opt
        .EnableAnimations = Not opt
        .EnableEvents = Not opt
        .ScreenUpdating = Not opt
    End With
    FastWS , opt
End Sub

Public Sub FastWS(Optional ByVal ws As Worksheet = Nothing, _
                  Optional ByVal opt As Boolean = True)
    If ws Is Nothing Then
        For Each ws In Application.ActiveWorkbook.Sheets
            EnableWS ws, opt
        Next
    Else
        EnableWS ws, opt
    End If
End Sub

Private Sub EnableWS(ByVal ws As Worksheet, ByVal opt As Boolean)
    With ws
        .DisplayPageBreaks = False
        .EnableCalculation = Not opt
        .EnableFormatConditionsCalculation = Not opt
        .EnablePivotTable = Not opt
    End With
End Sub

trouve la dernière cellule avec des données (merci @ZygD-maintenant je l'ai testé dans plusieurs scénarios):

Public Function GetMaxCell(Optional ByRef rng As Range = Nothing) As Range

    'Returns the last cell containing a value, or A1 if Worksheet is empty

    Const NONEMPTY As String = "*"
    Dim lRow As Range, lCol As Range

    If rng Is Nothing Then Set rng = Application.ActiveWorkbook.ActiveSheet.UsedRange
    If WorksheetFunction.CountA(rng) = 0 Then
        Set GetMaxCell = rng.Parent.Cells(1, 1)
    Else
        With rng
            Set lRow = .Cells.Find(What:=NONEMPTY, LookIn:=xlFormulas, _
                                        After:=.Cells(1, 1), _
                                        SearchDirection:=xlPrevious, _
                                        SearchOrder:=xlByRows)
            If Not lRow Is Nothing Then
                Set lCol = .Cells.Find(What:=NONEMPTY, LookIn:=xlFormulas, _
                                            After:=.Cells(1, 1), _
                                            SearchDirection:=xlPrevious, _
                                            SearchOrder:=xlByColumns)

                Set GetMaxCell = .Parent.Cells(lRow.Row, lCol.Column)
            End If
        End With
    End If
End Function

Renvoie l'index d'un match dans le tableau, ou 0 si aucune correspondance n'est trouvée:

Public Function IndexOfValInRowOrCol( _
                                    ByVal searchVal As String, _
                                    Optional ByRef ws As Worksheet = Nothing, _
                                    Optional ByRef rng As Range = Nothing, _
                                    Optional ByRef vertical As Boolean = True, _
                                    Optional ByRef rowOrColNum As Long = 1 _
                                    ) As Long

    'Returns position in Row or Column, or 0 if no matches found

    Dim usedRng As Range, result As Variant, searchRow As Long, searchCol As Long

    result = CVErr(9999) '- generate custom error

    Set usedRng = GetUsedRng(ws, rng)
    If Not usedRng Is Nothing Then
        If rowOrColNum < 1 Then rowOrColNum = 1
        With Application
            If vertical Then
                result = .Match(searchVal, rng.Columns(rowOrColNum), 0)
            Else
                result = .Match(searchVal, rng.Rows(rowOrColNum), 0)
            End If
        End With
    End If
    If IsError(result) Then IndexOfValInRowOrCol = 0 Else IndexOfValInRowOrCol = result
End Function

.

mise à Jour:

testé 6 solutions (3 tests chacune):Excel Héros solution est la plus rapide jusqu'à présent (supprime les formules)

.

Voici les résultats, plus rapide au plus lent:

.

essai 1. Total de 100 000 enregistrements de, 10 000 à être supprimé:

1. ExcelHero()                    - 1.5 seconds

2. DeleteRowsWithValuesNewSheet() - 2.4 seconds

3. DeleteRowsWithValuesStrings()  - 2.45 minutes
4. DeleteRowsWithValuesArray()    - 2.45 minutes
5. QuickAndEasy()                 - 3.25 minutes
6. DeleteRowsWithValuesUnion()    - Stopped after 5 minutes

.

essai 2. Total de 1 million d'enregistrements, de 100 000 à être supprimé:

1. ExcelHero()                    - 16 seconds (average)

2. DeleteRowsWithValuesNewSheet() - 33 seconds (average)

3. DeleteRowsWithValuesStrings()  - 4 hrs 38 min (16701.375 sec)
4. DeleteRowsWithValuesArray()    - 4 hrs 37 min (16626.3051757813 sec)
5. QuickAndEasy()                 - 5 hrs 40 min (20434.2104492188 sec)
6. DeleteRowsWithValuesUnion()    - N/A

.

Notes:

  1. méthode ExcelHero: facile à mettre en œuvre, fiable, extrêmement rapide, mais supprime les formules
  2. méthode NewSheet: facile à implémenter, fiable, et répond à l'objectif
  3. méthode Strings: plus d'effort à mettre en œuvre, fiable, mais ne répond pas aux exigences
  4. méthode Array: similaire à Strings, mais redime un array (version plus rapide de Union)
  5. QuickAndEasy: facile à mettre en œuvre (court, fiable et élégant), mais ne répond pas à l'exigence
  6. Plage de l'Union: la mise en œuvre de complexité similaire aux 2 et 3, mais trop lent

j'ai aussi fait les données de test plus réaliste en introduisant des valeurs inhabituelles:

  • cellules vides, des plages, des lignes et des colonnes
  • caractères spéciaux, tels que =[`~!@#$%^&*()_-+{}[]|;:'",. / ? distincts et de multiples combinaisons
  • espaces blancs, onglets, formules vides, bordure, police, et autres formatage de cellules
  • grands et petits nombres avec décimales (=12.99999999999 + 0.00000000000000001)
  • hyperliens, règles de formatage conditionnel
  • vide formatage à l'intérieur et à l'extérieur des plages de données
  • autre chose qui pourrait causer des problèmes de données
29
demandé sur Community 2015-06-21 01:01:51

5 réponses

je donne la première réponse comme référence

d'Autres peuvent le trouver utile, si il n'y a pas d'autres options disponibles

  • le moyen le plus rapide d'atteindre le résultat est de ne pas utiliser L'opération de suppression
  • de 1 million d'enregistrements, il supprime 100 000 lignes dans une moyenne de 33 secondes

.

Sub DeleteRowsWithValuesNewSheet()  '100K records   10K to delete
                                    'Test 1:        2.40234375 sec
                                    'Test 2:        2.41796875 sec
                                    'Test 3:        2.40234375 sec
                                    '1M records     100K to delete
                                    'Test 1:        32.9140625 sec
                                    'Test 2:        33.1484375 sec
                                    'Test 3:        32.90625   sec
    Dim oldWs As Worksheet, newWs As Worksheet, rowHeights() As Long
    Dim wsName As String, t As Double, oldUsedRng As Range

    FastWB True:    t = Timer

    Set oldWs = Worksheets(1)
    wsName = oldWs.Name

    Set oldUsedRng = oldWs.Range("A1", GetMaxCell(oldWs.UsedRange))

    If oldUsedRng.Rows.Count > 1 Then                           'If sheet is not empty
        Set newWs = Sheets.Add(After:=oldWs)                    'Add new sheet
        With oldUsedRng
            .AutoFilter Field:=1, Criteria1:="<>Test String"
            .Copy                                               'Copy visible data
        End With
        With newWs.Cells
            .PasteSpecial xlPasteColumnWidths
            .PasteSpecial xlPasteAll                            'Paste data on new sheet
            .Cells(1, 1).Select                                 'Deselect paste area
            .Cells(1, 1).Copy                                   'Clear Clipboard
        End With
        oldWs.Delete                                            'Delete old sheet
        newWs.Name = wsName
    End If
    FastWB False:   InputBox "Duration: ", "Duration", Timer - t
End Sub

.

À haut niveau:

  • il crée une nouvelle feuille de travail, et la référence à la feuille initiale
  • Autofiltres colonne 1 sur le texte recherché:.AutoFilter Field:=1, Criteria1:="<>Test String"
  • Copie toutes les données (visibles) de la feuille initiale
  • colle les largeurs des colonnes, les formats et les données sur la nouvelle feuille
  • supprime la feuille initiale
  • renomme la nouvelle feuille au nom de l'ancienne feuille

il utilise les mêmes fonctions d'aide affichées dans la question

.

il y a quelques limites que j'ai trouvées jusqu'à présent, la première peut être abordée:

  1. S'il y a des lignes cachées sur la feuille initiale, elle les décroche

    • Une fonction distincte est nécessaire de les cacher en arrière
    • selon la mise en oeuvre, cela pourrait augmenter de façon significative la durée
  2. VBA related:

    • il modifie le nom de Code du la feuille; les autres VBA se référant à la feuille Sheet1 est cassé (le cas échéant)
    • Il supprime tout le code VBA associé à la première feuille (le cas échéant)

.

quelques remarques concernant l'utilisation de gros fichiers comme ceci:

  • Le format binaire (.xlsb) réduire la taille du fichier de façon spectaculaire (à partir de 137 Mo à 43 Mo)
  • les règles de formatage conditionnel non gérées peuvent causer des problèmes de performance exponentielle

    • La même chose pour Commentaires et validation des données
  • lire un fichier ou des données du réseau est beaucoup plus lent que de travailler avec un fichier locall

14
répondu paul bica 2015-06-24 05:20:24

un gain significatif de vitesse peut être obtenu si les données source ne contiennent pas de formules, ou si le scénario permettrait (ou voudrait) que les formules soient converties en valeurs dures pendant les suppressions de lignes conditionnelles.

avec la mise en garde ci-dessus, ma solution utilise le filtre avancé de l'objet range. C'est environ deux fois plus rapide que DeleteRowsWithValuesNewSheet().

Public Sub ExcelHero()
    Dim t#, crit As Range, data As Range, ws As Worksheet
    Dim r&, fc As Range, lc As Range, fr1 As Range, fr2 As Range
    FastWB True
    t = Timer

        Set fc = ActiveSheet.UsedRange.Item(1)
        Set lc = GetMaxCell
        Set data = ActiveSheet.Range(fc, lc)
        Set ws = Sheets.Add
        With data
            Set fr1 = data.Worksheet.Range(fc, fc.Offset(, lc.Column))
            Set fr2 = ws.Range(ws.Cells(fc.Row, fc.Column), ws.Cells(fc.Row, lc.Column))
            With fr2
                fr1.Copy
                .PasteSpecial xlPasteColumnWidths: .PasteSpecial xlPasteAll
                .Item(1).Select
            End With
            Set crit = .Resize(2, 1).Offset(, lc.Column + 1)
            crit = [{"Column 1";"<>Test String"}]
            .AdvancedFilter xlFilterCopy, crit, fr2
            .Worksheet.Delete
        End With

    FastWB False
    r = ws.UsedRange.Rows.Count
    Debug.Print "Rows: " & r & ", Duration: " & Timer - t & " seconds"
End Sub
9
répondu Excel Hero 2015-10-17 23:31:44

Sur mes vieux Dell Inspiron 1564 (Win 7 Office 2007):

Sub QuickAndEasy()
    Dim rng As Range
    Set rng = Range("AA2:AA1000001")
    Range("AB1") = Now
    Application.ScreenUpdating = False
        With rng
            .Formula = "=If(A2=""Test String"",0/0,A2)"
            .Cells.SpecialCells(xlCellTypeFormulas, xlErrors).EntireRow.Delete
            .Clear
        End With
    Application.ScreenUpdating = True
    Range("AC1") = Now
End Sub

a pris environ 10 secondes pour s'exécuter. Je suis en supposant que la colonne AA est disponible.

EDIT#1:

Veuillez noter que ce code n' set Calcul Manuel. Les performances s'amélioreront si le mode de calcul est réglé à Manuel après la colonne" helper " permet de calculer.

5
répondu Gary's Student 2015-06-22 00:18:43

je sais que je suis incroyablement en retard avec ma réponse ici, mais les futurs visiteurs pourraient la trouver très utile.

Remarque: Mon approche nécessite une colonne d'index pour les lignes à l'extrémité de la commande d'origine, cependant, si vous n'avez pas l'esprit les lignes dans un ordre différent, puis une colonne d'index n'est pas nécessaire, et la ligne de code supplémentaire peut être retiré.

Mon approche: mon approche était simplement de sélectionner toutes les lignes dans les rang (colonne), les trier dans l'ordre croissant en utilisant Range.Sort et puis la collecte du premier et du dernier index de "Test String" dans la plage sélectionnée (colonne). J'ai ensuite créer une gamme à partir de la première et de la dernière indices et utiliser Range.EntrieRow.Delete pour supprimer toutes les lignes qui contiennent "Test String".

Avantages:

- Il est très rapide.

- Il ne supprime pas le formatage, les formules, les graphiques, les images ou n'importe quoi comme la méthode qui copie à une nouvelle feuille.

contre:

- Une taille décente de code à mettre en œuvre mais tout est simple.

Plage De Test De Génération De Sous:

Sub DevelopTest()
    Dim index As Long
    FastWB True
    ActiveSheet.UsedRange.Clear
    For index = 1 To 1000000 '1 million test
        ActiveSheet.Cells(index, 1).Value = index
        If (index Mod 10) = 0 Then
            ActiveSheet.Cells(index, 2).Value = "Test String"
        Else
            ActiveSheet.Cells(index, 2).Value = "Blah Blah Blah"
        End If
    Next index
    Application.StatusBar = ""
    FastWB False
End Sub

Filtrer Et Supprimer Les Lignes Sub:

Sub DeleteRowFast()
    Dim curWorksheet As Worksheet 'Current worksheet vairable

    Dim rangeSelection As Range   'Selected range
    Dim startBadVals As Long      'Start of the unwanted values
    Dim endBadVals As Long        'End of the unwanted values
    Dim strtTime As Double        'Timer variable
    Dim lastRow As Long           'Last Row variable
    Dim lastColumn As Long        'Last column variable
    Dim indexCell As Range        'Index range start
    Dim sortRange As Range        'The range which the sort is applied to
    Dim currRow As Range          'Current Row index for the for loop
    Dim cell As Range             'Current cell for use in the for loop

    On Error GoTo Err
        Set rangeSelection = Application.InputBox("Select the (N=) range to be checked", "Get Range", Type:=8)    'Get the desired range from the user
        Err.Clear

    M1 = MsgBox("This is recommended for large files (50,000 or more entries)", vbYesNo, "Enable Fast Workbook?") 'Prompt the user with an option to enable Fast Workbook, roughly 150% performace gains... Recommended for incredibly large files
    Select Case M1
        Case vbYes
            FastWB True  'Enable fast workbook
        Case vbNo
            FastWB False 'Disable fast workbook
    End Select

    strtTime = Timer     'Begin the timer

    Set curWorksheet = ActiveSheet
    lastRow = CLng(rangeSelection.SpecialCells(xlCellTypeLastCell).Row)
    lastColumn = curWorksheet.Cells(1, 16384).End(xlToLeft).Column

    Set indexCell = curWorksheet.Cells(1, 1)

    On Error Resume Next

    If rangeSelection.Rows.Count > 1 Then 'Check if there is anything to do

        lastVisRow = rangeSelection.Rows.Count

        Set sortRange = curWorksheet.Range(indexCell, curWorksheet.Cells(curWorksheet.Rows(lastRow).Row, 16384).End(xlToLeft)) 'Set the sort range

        sortRange.Sort Key1:=rangeSelection.Cells(1, 1), Order1:=xlAscending, Header:=xlNo 'Sort by values, lowest to highest

        startBadVals = rangeSelection.Find(What:="Test String", LookAt:=xlWhole, MatchCase:=False).Row
        endBadVals = rangeSelection.Find(What:="Test String", LookAt:=xlWhole, SearchDirection:=xlPrevious, MatchCase:=False).Row

        curWorksheet.Range(curWorksheet.Rows(startBadVals), curWorksheet.Rows(endBadVals)).EntireRow.Delete 'Delete uneeded rows, deleteing in continuous range blocks is quick than seperated or individual deletions.

        sortRange.Sort Key1:=indexCell, Order1:=xlAscending, Header:=xlNo 'Sort by index instead of values, lowest to highest
    End If

    Application.StatusBar = ""                    'Reset the status bar

    FastWB False                                  'Disable fast workbook

    MsgBox CStr(Round(Timer - strtTime, 2)) & "s" 'Display duration of task

Err:
    Exit Sub

End Sub

CE CODE UTILISE FastWB,FastWS et EnableWS par Paul Bica!

fois à 100k entrées (10k à supprimer, FastWB Vrai):

1. 0,2 secondes.

2. 0,2 secondes.

3. 0.21 secondes.

Avg. 0,2 secondes.

Fois à 1 million d'entrées (100k à être supprimé, FastWB Vrai):

1. 2.3 secondes.

2. 2.32 secondes.

3. 2.3 secondes.

Avg. 2.31 secondes.

tournant sous Windows 10, iMac i3 11,2 (À partir de 2010)

EDIT

Ce code a été conçu à l'origine avec le but de filtrer les valeurs numériques à l'extérieur d'une plage de nombres et a été adapté pour filtrer "Test String" donc le code peut être redondante.

1
répondu 2016-11-26 08:59:38

votre utilisation des tableaux dans le calcul de la portée utilisée et le nombre de lignes peut affecter la performance. Voici une autre approche qui dans les essais se révèle efficace à travers 1m + rangées de données - entre 25-30 secondes. Il n'utilise pas de filtres donc supprimera les lignes même si elles sont cachées. La suppression d'une ligne entière n'aura pas d'effet sur le formatage ou la largeur des colonnes des autres lignes restantes.

  1. tout d'abord, vérifiez si la feuille D'activation contient "test String". Puisque vous êtes seulement intéressé par la colonne 1 j'ai utilisé ceci:

    TCount = Application.WorksheetFunction.CountIf(sht.Columns(1), "Test String")
    If TCount > 0 Then
    
  2. au lieu d'utiliser votre fonction GetMaxCell() j'ai simplement utilisé Cells.SpecialCells(xlCellTypeLastCell).Row pour obtenir la dernière ligne:

    EndRow = sht.Cells.SpecialCells(xlCellTypeLastCell).Row
    
  3. puis boucle les lignes de données:

    While r <= EndRow
    
  4. pour vérifier si la cellule de la colonne 1 est égale à"test String":

    If sht.Cells(r, 1).Text) = "Test String" Then
    
  5. supprimer la ligne:

    Rows(r).Delete Shift:=xlUp
    

mise en place du code complet ci-dessous. J'ai mis ActiveSheet à un variable Sht et ajouté tour de ScreenUpdating pour améliorer l'efficacité. Comme il y a beaucoup de données, je m'assure de clarifier les variables à la fin.

Sub RowDeleter()
    Dim sht As Worksheet
    Dim r As Long
    Dim EndRow As Long
    Dim TCount As Long
    Dim s As Date
    Dim e As Date

    Application.ScreenUpdating = True
    r = 2       'Initialise row number
    s = Now     'Start Time
    Set sht = ActiveSheet
    EndRow = sht.Cells.SpecialCells(xlCellTypeLastCell).Row

    'Check if "Test String" is found in Column 1
    TCount = Application.WorksheetFunction.CountIf(sht.Columns(1), "Test String")
    If TCount > 0 Then

        'loop through to the End row
        While r <= EndRow
            If InStr(sht.Cells(r, 1).Text, "Test String") > 0 Then
                sht.Rows(r).Delete Shift:=xlUp
                r = r - 1
            End If
            r = r + 1
        Wend
    End If
    e = Now  'End Time
    D = (Hour(e) * 360 + Minute(e) * 60 + Second(e)) - (Hour(s) * 360 + Minute(s) * 60 + Second(s))
    Application.ScreenUpdating = True
    DurationTime = TimeSerial(0, 0, D)
    MsgBox Format(DurationTime, "hh:mm:ss")
End Sub
0
répondu Andrew Toomey 2015-06-21 12:03:41