2017-12-21 3 views
1

Ich habe ein PowerShell-Skript in Windows 2012 R2, das zum Exportieren von Daten aus einer Datenbank in eine CSV-Datei verwendet wird. Ich habe dort einen Check-in, um doppelten Anführungszeichen zu entgehen und benötigte Felder zu qualifizieren. Ich möchte die Leistung des Skripts erhöhen, da es etwas langsamer läuft als ich möchte (20 GB/20 Millionen Zeilen) und nur 10% der CPU verbraucht. Hat jemand Verbesserungsvorschläge?Optimieren von PowerShell-Skript für parallele Verarbeitung

$ConnectionString = "Data Source=server1; Database=Development; Trusted_Connection=True;"; 
$streamWriter = New-Object System.IO.StreamWriter ".\output.csv" 
$sqlConn = New-Object System.Data.SqlClient.SqlConnection $ConnectionString 
$sqlCmd = New-Object System.Data.SqlClient.SqlCommand 
$sqlCmd.Connection = $sqlConn 
$sqlCmd.CommandText = "SELECT * FROM Development.dbo.All_Opportunities WITH(NOLOCK)" 
$sqlConn.Open(); 
$reader = $sqlCmd.ExecuteReader(); 

# Initialze the array the hold the values 
$array = @() 
for ($i = 0 ; $i -lt $reader.FieldCount; $i++) 
    { $array += @($i) } 

# Write Header 
$streamWriter.Write($reader.GetName(0)) 
for ($i = 1; $i -lt $reader.FieldCount; $i ++) 
{ $streamWriter.Write($("," + $reader.GetName($i))) } 

$streamWriter.WriteLine("") # Close the header line 

while ($reader.Read()) 
{ 
    # get the values; 
    $fieldCount = $reader.GetValues($array); 

    # add quotes if the values have a comma or double quote 
    for ($i = 0; $i -lt $array.Length; $i++) 
    { 
    if ($array[$i] -match "`"|,") 
     { 
      $array[$i] = '"' + $array[$i].Replace("`"", "`"`"").ToString() + '"'; 
     } 
    } 

    $newRow = [string]::Join(",", $array); 

    $streamWriter.WriteLine($newRow) 
} 
$reader.Close(); 
$sqlConn.Close(); 
$streamWriter.Close(); 
+0

Starten Sie die Nutzung von RAM. Dafür ist es da. Anstatt jeden Datensatz einzeln in "$ StreamWriter" zu schreiben und den Prozess an die Festplatten-IO zu binden, schreiben Sie in einen 'StringBuilder' als Puffer. Dann, wenn Ihr 'StringBuilder' eine bestimmte Länge erreicht, sagen wir 50-100 MB, konvertieren Sie den' StringBuilder' in einen String, schreiben Sie ihn in '$ StreamWriter', löschen Sie den' StringBuilder' und fahren Sie fort.Achten Sie darauf, den 'StringBuilder' am Ende ein letztes Mal zu löschen. –

+0

Das klingt nach einer wunderbaren Lösung. Ein Beispiel wäre toll :) –

+0

Eigentlich nehme ich es zurück. Wenn ich darüber nachdenke, denke ich an eine andere Situation. 'StreamWriter' selbst hat bereits einen Puffer, so dass es unwahrscheinlich ist, die Leistung zu verbessern, indem ein zweiter hinzugefügt wird. –

Antwort

0

helfen Also, ich hatte ein ähnliches Problem vor etwa einem Jahr, wenn auch mit einem etwas kleineren Tisch (~ 1 GB). Am Anfang habe ich nur:

Import-Module -Name SqlServer -Cmdlet Read-SqlTableData; 
Read-SqlTableData -ServerInstance $SqlServer -DatabaseName $Database -SchemaName $Schema -TableName $Table | 
    Export-Csv -Path $OutputFilePath -NoTypeInformation 

Es funktionierte, aber es verwendet, um eine Tonne Arbeitsspeicher (5+ GB von 16 GB) und dauerte etwa 7-9 Minuten zu laufen. Alle diese Tests waren mit einer sich drehenden Metallscheibe in einem Laptop, also bedenken Sie auch, was folgt.

Ich fragte mich, ob ich es schneller gehen könnte. Ich schrieb es zunächst so aus, die etwa die Hälfte der Zeit in Anspruch nahm, und etwa 100 MB RAM:

$SqlServer = '...'; 
$SqlDatabase = '...'; 
$OutputFilePath = '...'; 
$SqlQuery = '...'; 

$SqlConnectionString = 'Data Source={0};Initial Catalog={1};Integrated Security=SSPI' -f $SqlServer, $SqlDatabase; 

$Utf8NoBOM = New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false; 
$StreamWriter = New-Object -TypeName System.IO.StreamWriter -ArgumentList $OutputFilePath, $Utf8NoBOM; 

$CsvDelimiter = '"'; 
$CsvDelimiterEscape = '""'; 
$CsvSeparator = ','; 

$SQLConnection = New-Object -TypeName System.Data.SqlClient.SqlConnection -ArgumentList $SqlConnectionString; 
$SqlCommand = $SQLConnection.CreateCommand(); 
$SqlCommand.CommandText = $SqlQuery; 

$SQLConnection.Open(); 
$SqlDataReader = $SqlCommand.ExecuteReader(); 

for ($Field = 0; $Field -lt $SqlDataReader.FieldCount; $Field++) { 
    if ($Field -gt 0) { $StreamWriter.Write($CsvSeparator); } 
    $StreamWriter.Write($CsvDelimiter); 
    $StreamWriter.Write($SqlDataReader.GetName($Field).Replace($CsvDelimiter, $CsvDelimiterEscape)); 
    $StreamWriter.Write($CsvDelimiter); 
} 
$StreamWriter.WriteLine(); 

while ($SqlDataReader.Read()) { 
    for ($Field = 0; $Field -lt $SqlDataReader.FieldCount; $Field++) { 
     if ($Field -gt 0) { $StreamWriter.Write($CsvSeparator); } 
     $StreamWriter.Write($CsvDelimiter); 
     $StreamWriter.Write($SqlDataReader.GetValue($Field).ToString().Replace($CsvDelimiter, $CsvDelimiterEscape)); 
     $StreamWriter.Write($CsvDelimiter); 
    } 
    $StreamWriter.WriteLine(); 
} 

$SqlDataReader.Close(); 
$SqlDataReader.Dispose(); 

$SQLConnection.Close(); 
$SQLConnection.Dispose(); 

$StreamWriter.Close(); 
$StreamWriter.Dispose(); 

Wie Sie sehen, es ist im Grunde das gleiche Muster wie bei Ihnen.

Ich fragte mich, ob ich es noch verbessern könnte, also habe ich versucht, einen StringBuilder hinzuzufügen, da ich Erfolg damit hatte mit anderen Projekten. Ich immer noch den Code haben, aber ich fand, dass es nicht schneller funktionierte, und dauerte etwa 200 MB RAM:

$SqlServer = '...' 
$SqlDatabase = '...' 
$OutputFilePath = '...' 
$SqlQuery = '...'; 

$SqlConnectionString = 'Data Source={0};Initial Catalog={1};Integrated Security=SSPI' -f $SqlServer, $SqlDatabase; 

$StringBuilderBufferSize = 50MB; 
$StringBuilder = New-Object -TypeName System.Text.StringBuilder -ArgumentList ($StringBuilderBufferSize + 1MB); 

$Utf8NoBOM = New-Object -TypeName System.Text.UTF8Encoding -ArgumentList $false; 
$StreamWriter = New-Object -TypeName System.IO.StreamWriter -ArgumentList $OutputFilePath, $Utf8NoBOM; 

$CsvDelimiter = '"'; 
$CsvDelimiterEscape = '""'; 
$CsvSeparator = ','; 

$SQLConnection = New-Object -TypeName System.Data.SqlClient.SqlConnection -ArgumentList $SqlConnectionString; 
$SqlCommand = $SQLConnection.CreateCommand(); 
$SqlCommand.CommandText = $SqlQuery; 

$SQLConnection.Open(); 
$SqlDataReader = $SqlCommand.ExecuteReader(); 

for ($Field = 0; $Field -lt $SqlDataReader.FieldCount; $Field++) { 
    if ($Field -gt 0) { [void]$StringBuilder.Append($CsvSeparator); } 
    [void]$StringBuilder.Append($CsvDelimiter); 
    [void]$StringBuilder.Append($SqlDataReader.GetName($Field).Replace($CsvDelimiter, $CsvDelimiterEscape)); 
    [void]$StringBuilder.Append($CsvDelimiter); 
} 
[void]$StringBuilder.AppendLine(); 

while ($SqlDataReader.Read()) { 
    for ($Field = 0; $Field -lt $SqlDataReader.FieldCount; $Field++) { 
     if ($Field -gt 0) { [void]$StringBuilder.Append($CsvSeparator); } 
     [void]$StringBuilder.Append($CsvDelimiter); 
     [void]$StringBuilder.Append($SqlDataReader.GetValue($Field).ToString().Replace($CsvDelimiter, $CsvDelimiterEscape)); 
     [void]$StringBuilder.Append($CsvDelimiter); 
    } 
    [void]$StringBuilder.AppendLine(); 

    if ($StringBuilder.Length -ge $StringBuilderBufferSize) { 
     $StreamWriter.Write($StringBuilder.ToString()); 
     [void]$StringBuilder.Clear(); 
    } 
} 

$SqlDataReader.Close(); 
$SqlDataReader.Dispose(); 

$SQLConnection.Close(); 
$SQLConnection.Dispose(); 

$StreamWriter.Write($StringBuilder.ToString()); 
$StreamWriter.Close(); 
$StreamWriter.Dispose(); 

Egal, was ich versuchte, konnte ich es nicht scheinen, unter ~ 4 : 30 für ca. 1 GB Daten.

Ich habe nie über Parallelität nachgedacht, weil Sie Ihre Anfrage in 4 gleiche Teile aufteilen müssten, so dass Sie sicher sein könnten, dass Sie den kompletten Datensatz bekommen oder ansonsten eine ziemlich schwierige Prozessverwaltung mit Runspace Pools durchführen. Selbst dann müssten Sie in vier verschiedene Dateien schreiben und schließlich die Dateien wieder kombinieren. Vielleicht würde es funktionieren, aber zu diesem Zeitpunkt war es für mich kein interessantes Problem mehr.

Schließlich habe ich gerade ein Paket mit dem Import-Export-Assistenten erstellt, es als Paket gespeichert und es mit DTExec.exe ausgeführt. Dies dauert ca. 45-60 Sekunden für 1 GB Daten. Die einzigen Nachteile sind, dass Sie beim Erstellen des Pakets die Tabelle angeben müssen, die Spalten jedoch nicht dynamisch ermittelt werden und dass die Ausgabedatei UTF8 ist.

Ich fand, dass bcp.exe und sqlcmd.exe waren schneller. BCP war extrem schnell und dauerte 20-30 Sekunden. Die Ausgabeformate sind jedoch extrem begrenzt, und insbesondere BCP ist unnötig schwierig zu verwenden.

+0

Vielen Dank für die gründliche Antwort. Ich stimme zu, ich habe versucht, die Dosierung und es hat die Zeit nicht verbessert. Ich wünschte, ich könnte eine der anderen schnelleren Methoden verwenden, aber ich war nicht in der Lage, diese richtig zu arbeiten, um den Anführungszeichen in meinen Daten zu entkommen und praktisch jedes ASCII-Zeichen ist irgendwo in den Daten vorhanden, so dass ich nicht wählen kann ein obskures Trennzeichen. –

Verwandte Themen