PowerShell: Array sortieren --> Binäre-Suche-Zugriff

Squicky

Lt. Commander
Registriert
Sep. 2002
Beiträge
1.422
Hallo zusammen


PowerShell:

Es gibt zwei Arrays. In beiden Arrays befinden sich Objekte.

ProdukteArray: In diesem Array befinden sich ca. 20.000 Objekte der Klasse Produkt.
Die Klasse Produkt hat mehre Eigenschaften / Attribute.
Ein String-Attribute ist "id" und ist ein Schlüssel.
Die Produkt-Objekte in diesem ProdukteArray sind nicht (nach id) sortiert.

AuftraegeArray: In diesem Array befinden sich ca. 3.000 Objekte der Klasse Auftrag.
Die Klasse Auftraghat mehre Eigenschaften / Attribute.
Es gibt ein String-Attribute "ProduktId" in der Klasse Auftrag.


Nun müssen alle Auftraege bearbeitet werden. Hierfür wird das AuftraegeArray in einer Schleife durchlaufen:

Code:
Foreach ($Auftrag in $AuftraegeArray) {

	$Produkt = $ProdukteArray | ? {$_.id -eq $Auftrag.ProduktId }

	# …. Auftrag bearbeiten

}

In der 3. Zeile wird das Produkte des Auftrages aus dem ProdukteArray herausgesucht. Diese Zeile braucht ca. 1 Sekunde und ist für eine sehr hoch Laufzeit der Schleife verantwortlich.

Meine Optimierungs-Idee ist:
1. Schritt: Produtke im ProdukteArray nach id sortieren.
2. Schritt: Zugriff auf das richtige Produkt aus dem ProdukteArray per Binäre-Suche.


Lösung für Schritt 1: $ProdukteArray = $ProdukteArray | Sort-Object -Property id

Für Schritt 2 habe ich noch kein cmdlet gefunden. :-(

Wer kann mir da helfen?

Danke
 
Der Zugriff über Pipe und "?" ist sehr langsam. Du schaust dir bei jedem Auftrag jedes einzelne Produktobjekt im Array an und vergleichst die ID.
Wie wäre es, wenn du dein Produktarray in eine Hashtable umwandelst? Dann hast du die ID als eindeutigen Index und kannst sehr schnell auf die Produkte zugreifen.

Code:
$ProdukteHashtable = @{}

Foreach ($Produkt in $ProdukteArray) {
  $ProdukteHashtable.Add($Produkt.ID, $Produkt)
}

# Zugriff:
Foreach ($Auftrag in $AuftraegeArray) {
 
	$Produkt = $ProdukteHashtable[$Auftrag.ProduktId]
 
	# …. Auftrag bearbeiten
 
}
 
Zuletzt bearbeitet: (Copy/Paste Fehler behoben)
Renegade334 schrieb:
Code:
$ProdukteHashtable = @{}

Foreach ($Produkt in $ProdukteArray) {
  $ProdukteHashtable.Add($Produkt.ID, $Produkt)
}
Warum so umständlich über eine Schleife?

Geht viel einfacher:
Code:
$ProdukteHashtable = $ProdukteArray | Group-Object id -AsHashTable

Wichtig ist halt, dass jede ID im Array einmalig ist.
 
Ich habe bisher meistens schlechte Erfahrungen mit der Performance von Pipes gemacht.
Ob das bei Group-Object auch so ist müsste man mal mit >10000 Objekten testen :)
 
Renegade334 schrieb:
Ich habe bisher meistens schlechte Erfahrungen mit der Performance von Pipes gemacht.
Ob das bei Group-Object auch so ist müsste man mal mit >10000 Objekten testen :)

An diesen Ansatz hatte ich bisher echt nicht gedacht bzw. ich hab nie die Performance zwischen diesen zwei Varianten getestet.
Dachte die nehmen sich kaum was. Aber...

Danke für den Tipp ;)



Wer testen will:
Code:
$Stopwatch = [System.Diagnostics.Stopwatch]::new()

write-host 'Creating Array' -ForegroundColor Red
$Stopwatch.Start()
$Array = 0 .. 10000 | select @{ n = 'ID' ; e = { $_ } } , @{ n = 'Time' ; e = { get-date -Format 'hh:mm:ss.ffff' } } 
write-host "Time elapsed $($Stopwatch.Elapsed.TotalSeconds) seconds`n`n" -ForegroundColor Red


write-host 'Creating Hashtable by Group-Object' -ForegroundColor Red
$Stopwatch.Restart()
$HashTableByGroupObject = $Array | Group-Object ID -AsHashTable
write-host "Time elapsed $($Stopwatch.Elapsed.TotalSeconds) seconds`n`n" -ForegroundColor Red


write-host 'Creating Hashtable by ForEach' -ForegroundColor Red
$Stopwatch.Restart()
$HashTableByForEach = @{ } 

Foreach ($Object In $Array) { 
	$HashTableByForEach.Add($Object.ID , $Object) 
} 
write-host "Time elapsed $($Stopwatch.Elapsed.TotalSeconds) seconds" -ForegroundColor Red

Ergebnis:
Code:
Creating Array
Time elapsed 2.1224002 seconds


Creating Hashtable by Group-Object
Time elapsed 21.8473127 seconds


Creating Hashtable by ForEach
Time elapsed 0.0336326 seconds

Ist für mich aber echt eine misslungene Umsetzung seitens Microsoft.
Weil das Ergebnis dasselbe ist. Alle Typen, etc sind identisch im Ergebnis, sofern mein kurzer Schnelltest alles abgedeckt hat.

Auf Hashtables mit Group-Object verzichte ich wohl in Zukunft erstmal ;)
Pipes haben aber in Bezug auf Select-Object enorme Vorteile was den Aufwand und besonders die Übersichtlichkeit anbelangt.
Als Beispiel ein kleiner Ausschnitt aus einem alten Projekt:
Code:
$SelectionStep1 = @(
	@{ n = 'CUSTOM9' ; e = { $_.ObjectGUID } } #UI-Label: GUID                                                                                      
	@{ n = 'USERNAME' ; e = { $_.UserPrincipalName } } 
	@{ n = 'SECURITY_DOMAIN' ; e = { $Domain } } 
	@{ n = 'LNAME' ; e = { $_.sn } } 
	@{ n = 'FNAME' ; e = { $_.GivenName } } 
	@{ n = 'HOME_COMPANY' ; e = { '' } } 
	@{ n = 'TERMINATED_ON' ; e = { '' } } 
	@{ n = 'MANAGER' ; e = { $Manager = $_.Manager ; If ($Manager -ne $Null) { $MangerUPN = $GroupedUser[$Manager].UserPrincipalName ; If ($MangerUPN) { $MangerUPN } Else {(Get-ADUser -Filter { DistinguishedName -eq $Manager } -Properties 'UserPrincipalName' @globalQueryParameter).UserPrincipalName } } } } # Find Manager by DistinguishedName in $GroupMember-Variable; if not available in variable -> query AD                                                                         
	@{ n = 'STATUS' ; e = { '1- Active' } } 
	@{ n = 'IS_MANAGER' ; e = { $GroupedManager.ContainsKey($_.DistinguishedName) } } # If a person is a manager of another person then $true else $false                                                                                     
	@{ n = 'CUSTOM3' ; e = { $_.department } } #UI-Label: Department                                                                            
	@{ n = 'EMAIL' ; e = { If ($EmptyMail) { '' } Else { $_.mail } } } 
	@{ n = 'SECURITY_ROLE1' ; e = { 'LH Basic Privileges Security Role' } } 
	@{ n = 'DOMAIN1' ; e = { 'Common' } } 
	@{ n = 'SECURITY_ROLE2' ; e = { 'LH Basic Privileges Security Role' } } 
	@{ n = 'DOMAIN2' ; e = { 'Corporate' } } 
	
	@{ n = 'SECURITY_ROLE3' ; e = { If ($SingleGroupAddSecurityRole1) { 'LH Basic Privileges Security Role' } Else { '' } } } 
	@{ n = 'DOMAIN3' ; e = { If ($SingleGroupAddSecurityRole1) { $SingleGroupAddSecurityRole1Domain } Else { '' } } } 
	
	@{ n = 'SECURITY_ROLE4' ; e = { If ($SingleGroupAddSecurityRole2) { 'LH Basic Privileges Security Role' } Else { '' } } } 
	@{ n = 'DOMAIN4' ; e = { If ($SingleGroupAddSecurityRole2) { $SingleGroupAddSecurityRole2Domain } Else { '' } } } 
	
	@{ n = 'SECURITY_ROLE5' ; e = { If ($SingleGroupAddSecurityRole2) { 'LH Basic Privileges Security Role' } Else { '' } } } 
	@{ n = 'DOMAIN5' ; e = { If ($SingleGroupAddSecurityRole3) { $SingleGroupAddSecurityRole3Domain } Else { '' } } } 
) 

$Process1 = $GroupMember | Select-Object $SelectionStep1
 
Danke für den Test.
Ca. 650-fache Zeit für Pipe hätte ich jetzt nicht erwartet, aber das bestätigt meine Erfahrungen :freak:
Dann meide ich Pipes weiterhin, außer für schnelle Einzeiler :D
 
Zurück
Oben