Powershell的StringBuilder性能不好?

问题描述

我正在尝试建立查询,以便可以将大量数据插入sqlite3表中。我尝试了几种方法来进行此操作,包括PSSQLite,该方法应该能够获取DataTable并轻松地将其插入。即使只有10,000条记录,也要花费近40分钟才能运行,我也不知道为什么。

我的另一个选择是构建查询并使用其他方法(例如Invoke-sqlcmd)执行查询。我尝试使用StringBuilder进行此操作,除了需要花费2分钟以上的时间来构建字符串。就像我说的那样,它只有10,000条记录,因此从我已经准备好的记录来看,它只需要10-15秒的TOPS。考虑到我至少要导入几百万条记录,我真的需要这样做才能更快。

这是我正在使用的代码。我只是在这里想念东西吗?

$sb = [System.Text.StringBuilder]::new()
$sb.AppendLine("BEGIN TRANSACTION")
foreach ($document in $documents) {
   
  $null = $sb.AppendLine("INSERT or IGnorE into documents(DocId,Submid,docno,Tray,Pieceno,CreateDate,Account,AccName,AccAddr1,AccAddr2,AccAddr3,AccAddr4,AccCity,AccState,AccZip,BCDP,BarcodeID,ServTypeID,Mailerid,SerialNo,Sys_Name,Sys_Addr1,Sys_Addr2,Sys_Addr3,Sys_Addr4,Sys_City,Sys_State,Sys_Zip)");
  $null = $sb.AppendLine("VALUES('$($document.DocId)','$($document.Submid)','$($document.docno)','$($document.Tray)','$($document.Pieceno)','$($document.CreateDate)','$($document.Account)','$($document.AccName)','$($document.AccAddr1)','$($document.AccAddr2)','$($document.AccAddr3)','$($document.AccAddr4)','$($document.AccCity)','$($document.AccState)','$($document.AccZip)','$($document.BCDP)','$($document.BarcodeID)','$($document.ServTypeID)','$($document.Mailerid)','$($document.SerialNo)','$($document.Sys_Name)','$($document.Sys_Addr1)','$($document.Sys_Addr2)','$($document.Sys_Addr3)','$($document.Sys_Addr4)','$($document.Sys_City)','$($document.Sys_State)','$($document.Sys_Zip)')")
    
}
    
$sb.AppendLine("COMMIT")
$query = $sb.ToString();
#Invoke-sqliteQuery $ref_db $query #commenting this out,because I haven't even attempted the insert because StringBuilder is not optimized enough yet.

在这种情况下,$ documents是一个通用对象,包含INSERT语句中的每个字段。大多数字段都填充有字符串,其中一些为空白。

#EDIT:我在设置了断点的Powershell ISE中运行它,这会导致性能问题吗?

解决方法

在进入问题的StringBuilder之前,让我们看一下SQLite:

如果您更改SQL以提交多行INSERT语句而不是10000个单独的INSERT语句,我希望您会发现处理时间有所不同就像您现在正在做的-换句话说:

$null = $sb.AppendLine("INSERT or IGNORE into documents(DocId,Submid,docno,Tray,Pieceno,CreateDate,Account,AccName,AccAddr1,AccAddr2,AccAddr3,AccAddr4,AccCity,AccState,AccZip,BCDP,BarcodeID,ServTypeID,Mailerid,SerialNo,Sys_Name,Sys_Addr1,Sys_Addr2,Sys_Addr3,Sys_Addr4,Sys_City,Sys_State,Sys_Zip)")
$null = $sb.AppendLine("VALUES")

foreach ($document in $documents) {
  # Add separate value tuple for each document,add trailing `,`
  $null = $sb.AppendLine("('$($document.DocId)','$($document.Submid)','$($document.docno)','$($document.Tray)','$($document.Pieceno)','$($document.CreateDate)','$($document.Account)','$($document.AccName)','$($document.AccAddr1)','$($document.AccAddr2)','$($document.AccAddr3)','$($document.AccAddr4)','$($document.AccCity)','$($document.AccState)','$($document.AccZip)','$($document.BCDP)','$($document.BarcodeID)','$($document.ServTypeID)','$($document.Mailerid)','$($document.SerialNo)','$($document.Sys_Name)','$($document.Sys_Addr1)','$($document.Sys_Addr2)','$($document.Sys_Addr3)','$($document.Sys_Addr4)','$($document.Sys_City)','$($document.Sys_State)','$($document.Sys_Zip)'),")
}
    
# trim trailing newline + comma on last insert value before adding COMMIT statement
$query = $sb.ToString().TrimEnd("`r`n,") + "`r`nCOMMIT"
Invoke-SqliteQuery $ref_db $query

通过避免可扩展的字符串,而是使用$sb.AppendFormat(),即:

$sb = [System.Text.StringBuilder]::new()
$sb.AppendLine("BEGIN TRANSACTION")
$null = $sb.AppendFormat('VALUES ({0},{2},...)',$doc.DocId,$doc.SubmId,...).AppendLine()

...但这可能不是问题。

Windows PowerShell中,一旦大小超过大对象堆缓存(85Kb)的阈值,则字符串操作(无论是通过直接串联还是通过字符串构建)都会有一些very funky performance characteristics )。

.net Core中似乎没有发生这种情况,因此升级到PowerShell的较新版本(例如PowerShell 7)可能会一起消除此问题。

如果您需要定位Windows PowerShell,则可能只想直接将SQL脚本直接写到磁盘上,然后用Invoke-SqliteQuery -InputFile读回即可:

$scriptFile = New-Item import.sql

try{
  $fileWriter = $scriptFile.CreateText()
  $fileWriter.WriteLine("INSERT or IGNORE into documents(DocId,Sys_Zip)")

  foreach ($document in $documents) {
    $fileWriter.WriteLine("VALUES")
    $fileWriter.WriteLine("('$($document.DocId)','$($document.Sys_Zip)')")
    $fileWriter.WriteLine("")
  }
  $fileWriter.WriteLine("COMMIT")
}
finally{
  $fileWriter.Close()
}    

Invoke-SqliteQuery $ref_db -InputFile import.sql
,

您是否只是尝试通过管道和-Join运算符使用PowerShell方式?

$sb = ($Documents | Foreach { "BEGIN TRANSACTION" }  {
          "INSERT or IGNORE into documents(DocId,Sys_Zip)"
          "VALUES('$($_.DocId)','$($_.Submid)','$($_.docno)','$($_.Tray)','$($_.Pieceno)','$($_.CreateDate)','$($_.Account)','$($_.AccName)','$($_.AccAddr1)','$($_.AccAddr2)','$($_.AccAddr3)','$($_.AccAddr4)','$($_.AccCity)','$($_.AccState)','$($_.AccZip)','$($_.BCDP)','$($_.BarcodeID)','$($_.ServTypeID)','$($_.Mailerid)','$($_.SerialNo)','$($_.Sys_Name)','$($_.Sys_Addr1)','$($_.Sys_Addr2)','$($_.Sys_Addr3)','$($_.Sys_Addr4)','$($_.Sys_City)','$($_.Sys_State)','$($_.Sys_Zip)')"
    } { "COMMIT" }) -Join [Environment]::NewLine

或者(因为Foreach语句通常比Foreach-Object cmdlet快一点):

$sb = @(
    "BEGIN TRANSACTION"
    foreach ($document in $documents) {
      "INSERT or IGNORE into documents(DocId,Sys_Zip)"
      "VALUES('$($_.DocId)','$($_.Sys_Zip)')"
    }
    "COMMIT"
) -Join [Environment]::NewLine