2017-12-21 10 views
1

데이터베이스의 데이터를 CSV 파일로 내보내는 데 사용되는 Windows 2012 R2의 PowerShell 스크립트가 있습니다. 큰 따옴표를 이스케이프하고 텍스트를 필수 입력란으로 사용할 수있는 체크가 있습니다. 내가 원하는 것보다 조금 느리게 실행되기 때문에 스크립트의 성능을 향상시키고 자하는데 (20GB/2 천만 개의 행 내보내기) CPU의 약 10 % 만 사용합니다. 누구든지 개선을위한 제안이 있습니까?병렬 처리를위한 PowerShell 스크립트 최적화

$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

RAM을 활용합니다. 그것이 바로 그 때문입니다. 각각의 레코드를 개별적으로'$ StreamWriter'에 쓰고 프로세스를 디스크 IO에 바인딩하는 대신'StringBuilder'를 버퍼로 써주세요. 그런 다음'StringBuilder'가 주어진 길이 인 50-100 MB에 도달하면'StringBuilder'를 문자열로 변환하고'$ StreamWriter'에 쓰고'StringBuilder'를 지운 다음 계속하십시오.마지막에'StringBuilder'를 마지막으로 플러시하십시오. –

+0

멋진 솔루션처럼 들립니다. 예가 대단 할 것입니다. –

+0

사실, 나는 그것을 되 찾습니다. 그것에 대해 더 생각하면, 나는 다른 상황을 생각하고 있습니다. 'StreamWriter' 자체는 이미 버퍼를 가지고 있기 때문에 두번째 성능을 향상시키지 않을 것입니다. –

답변

0

도움을 희망, 약간 작은 테이블 (~ 1GB)이긴하지만. 그것은 일

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

하지만 (GB 16에서 5 + GB)를 t 메모리의을 사용하고 실행하는 데 7-9 분에 나섭니다 : 처음에 그냥 사용했다. 이 모든 테스트는 노트북에 회전하는 금속 디스크를 사용하여 수행되었으므로 다음 사항도 염두에 두십시오.

내가 더 빨리 진행할 수 있을지 궁금해했습니다. 나는 처음에 절반 정도 시간이 걸렸하는이처럼 쓰고, RAM 약 100메가바이트 :

$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(); 

당신이 볼 수 있듯이, 그것은 기본적으로 당신과 같은 패턴입니다.

나는 그것을 더 향상시킬 수 있을지 궁금해했기 때문에 다른 프로젝트에서 성공했기 때문에 StringBuilder를 추가하려고 시도했습니다. 난 여전히 코드를 가지고,하지만 난 그게 더 빨리 작동하지 않는 것을 발견하고, RAM 200MB의 가지고 가지 않았다 : 아무리 내가, 내가 아래를 얻을 수가 없습니다 시도 무엇

$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(); 

을 ~ 4 : 약 1GB의 데이터는 30입니다.

쿼리를 4 개의 동등한 부분으로 나눠서 전체 데이터 세트를 얻을 수 있는지 또는 Runspace 풀을 사용하여 매우 어려운 프로세스 관리를 수행해야하는지 알 수 있으므로 결코 병렬 처리를 고려하지 않았습니다. 그렇다면 네 개의 서로 다른 파일에 쓰고 결국 다시 함께 결합해야합니다. 어쩌면 효과가 있을지 모르지만 그 시점에서 더 이상 나에게 흥미로운 문제는 아니 었습니다.

결국 가져 오기 내보내기 마법사를 사용하여 패키지를 만들고 패키지로 저장 한 후 DTExec.exe을 사용하여 실행했습니다. 1GB의 데이터에는 약 45-60 초 정도 걸립니다. 단점은 패키지를 빌드 할 때 테이블을 지정해야하며 동적으로 열을 결정하지 않으며 출력 파일을 UTF8로 가져 오는 것이 무리라는 점입니다.

나는 bcp.exesqlcmd.exe가 더 빠르다는 것을 발견했다. BCP는 매우 빠르며 20-30 초가 걸렸습니다. 그러나 출력 형식은 극히 제한되어 있으며 특히 BCP는 불필요하게 사용하기가 어렵습니다.

+0

철저한 답변을 보내 주셔서 감사합니다. 나는 동의한다, 나는 배치를 시도했다. 그리고 그것은 시간을 개선하지 않았다. 나는 다른 빠른 방법 중 하나를 사용할 수 있었으면 좋겠지 만 데이터에서 큰 따옴표를 벗어나기 위해 제대로 작동하지 못했고 사실상 모든 ASCII 문자가 어딘가에 데이터에 존재하므로 선택할 수 없습니다. 애매한 구획 문자. –