【问题标题】:Powershell poor StringBuilder performance?Powershell 的 StringBuilder 性能不佳?
【发布时间】:2020-11-05 15:05:00
【问题描述】:

我正在尝试构建查询,以便将大量数据插入到 sqlite3 表中。我已经尝试了几种方法来做到这一点,包括PSSQLite,它应该能够获取一个 DataTable 并轻松插入它。即使只有 10,000 条记录,它也需要将近 40 分钟才能运行,我不知道为什么。

我的另一个选择是构建一个查询并使用另一种方法(例如 Invoke-Sqlcmd)执行它。我试过用 StringBuilder 来做这件事,只是构建字符串需要 2 多分钟。就像我说的,它只有 10,000 条记录,所以根据我准备的内容,TOPS 应该只需要 10-15 秒。考虑到我至少有几百万条记录要导入,我真的需要它来加快速度。

这是我正在使用的代码。我只是在这里遗漏了什么吗?

$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 中运行它,这会导致性能问题吗?

【问题讨论】:

  • 这实际上是字符串生成器的性能吗?或者更确切地说是 PSqlite 的性能?
  • 可能是因为 pssqlite 尝试填充结果集,我没有看到非查询选项(?)
  • 我建议构建一个一次插入多条记录的 SQL 语句。而且我总是使用存储过程而不是用户提供的数据。
  • 我编辑了我的问题,因为它具有误导性。我什至没有运行我使用 StringBuilder 创建的查询,因为它太慢了,无法满足我的需要。构建查询本身花费的时间太长。

标签: sql string powershell sqlite stringbuilder


【解决方案1】:

在谈到您问题的StringBuilder 方面之前,让我们看一下SQLite:

如果您将 SQL 更改为提交 single 多行 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 框架。

没有似乎发生在 .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,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)")

  foreach ($document in $documents) {
    $fileWriter.WriteLine("VALUES")
    $fileWriter.WriteLine("('$($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)')")
    $fileWriter.WriteLine("")
  }
  $fileWriter.WriteLine("COMMIT")
}
finally{
  $fileWriter.Close()
}    

Invoke-SqliteQuery $ref_db -InputFile import.sql

【讨论】:

  • 我可能需要改写我的问题。我有两个性能问题:1. 使用 PSSQLite 直接插入我的查询结果(已经是 DataTable 格式) 2. 通过 StringBuilder 手动构建查询时。我什至还没有运行这个查询,因为我无法将 stringbuilder 优化到不会永远花费的地方
  • @fudge 您在问题中声称需要 2 分钟 - 如果您想要更快的测试,请尝试使用 1000 个文档。让我们知道$documents 的来源可能也很有用?
  • 我只是看到与这里应该可能的性能相比表现不佳:powershell.one/tricks/performance/strings 我认为这可能只是我正在尝试做的事情的现实。这可能只是一个缓慢的操作
  • @fudge 如果您向下滚动到该页面的底部,您会发现它实际上引用了我指出这仅在字符串长度超过一定长度时发生(似乎是大对象堆缓存阈值为 85KB)。 Measure-Script 可能会帮助您确定这是否也是您所看到的
  • 这就是我浏览文章的结果!!是的,这正是问题所在。我可以通过简单地升级到 PS v7 来解决这个问题。它从 20 分钟缩短到 2 秒。我怎么能相信你呢?也许我会标记您的答案并对其进行编辑以包含这些详细信息。
【解决方案2】:

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

$sb = ($Documents | Foreach { "BEGIN TRANSACTION" }  {
          "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)"
          "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,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)"
      "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

【讨论】:

    猜你喜欢
    • 2022-10-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-02-27
    • 1970-01-01
    • 2015-04-29
    相关资源
    最近更新 更多