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:
:
.
comment le code fonctionne:
- il commence par désactiver toutes les fonctions Excel
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
- ajoute l'adresse de la cellule à une chaîne tmp dans le format
- À 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:
- méthode ExcelHero: facile à mettre en œuvre, fiable, extrêmement rapide, mais supprime les formules
- méthode NewSheet: facile à implémenter, fiable, et répond à l'objectif
- méthode Strings: plus d'effort à mettre en œuvre, fiable, mais ne répond pas aux exigences
- méthode Array: similaire à Strings, mais redime un array (version plus rapide de Union)
- QuickAndEasy: facile à mettre en œuvre (court, fiable et élégant), mais ne répond pas à l'exigence
- 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
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:
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
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
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
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.
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.
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.
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
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
puis boucle les lignes de données:
While r <= EndRow
pour vérifier si la cellule de la colonne 1 est égale à"test String":
If sht.Cells(r, 1).Text) = "Test String" Then
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