填充非常大的哈希表-如何最有效地做到这一点?

时间:2018-08-16 09:38:54

标签: performance powershell hashtable

背景/上下文:

为了识别“来自数据集A的项目X与来自数据集B的项目Y或Z匹配”,我必须交叉检查/比较多个数据集(这些数据集倾向于相互不一致)。

涉及的那些数据集有些大(10万条记录),并且涉及到我戳SQL数据库的情况。

经过一些初步的研究和性能测试,我已经从解析通过“大规模数组”转变为对关键属性点有效地使用“索引哈希表”。

挑战:

使用哈希表 very 很快,但是我的问题在于有效地构建它们。感觉就像我“快到了”,但是不得不诉诸(相对)缓慢的方法(50,000条记录大约需要300-400秒)。

这是我现在要编制索引的基本数据(我从SQL中获得了不同的设备名称列表以及该设备具有多少记录的计数):

DEVICENAME      COUNTOF
==========      ========
DEVICE_1        1
DEVICE_2        1
DEVICE_3        2
....            ...
DEVICE_49999    3
DEVICE_50000    1

当前解决方案:

我目前正在通过遍历结果集(作为结果集,我从SQL中拉出的数组)并为每个订单项使用“ .add”来构造哈希表。

所以只是一个简单的...

for ($i=0; $i -lt @($SQL_Results).CountOf; $i++) {
    $MyIndexHash.Add( @($SQL_Results[$i]).DeviceName,  @($SQL_Results[$i]).CountOf)
}

相对而言,这有点“慢”(前面提到的300-400秒用于构建50,000个订单项)。我可以等待,但由于(出于直觉)我尝试了以下“接近即时”的操作,因此嘲笑可能有更好的方法(大约花了3秒钟)

$MyIndexHash.Keys = $SQL_Results.DEVICENAME

但是-仅 填充哈希表的KEYS,而不是相关值。而且我还没有找到一种有效实现以下目标的方法(将值从数组中直接分配到哈希表中):

$MyIndexHash.Keys = ($SQL_Results.DEVICENAME, $SQL_Results.COUNTOF)

这是一个“纯粹的性能”问题-因为我需要对其他80,000个和150,000个订单项进行比较。如果我必须“只是等待”通过遍历我的SQL结果数组的每一行来构造哈希表,就可以了。

注意-我查看了-Powershell 2 and .NET: Optimize for extremely large hash tables?-但是由于我有可变数据集(很好-“未知但可能很大”)可以处理,所以我不确定是否可以/开始分解哈希表。

此外,哈希表中的LOOKUP(一旦填充)毕竟是超级快的……只是希望以某种更有效的方式完成哈希表的构建?

欢迎提出任何建议,以提高我如何更有效地构建哈希表。

谢谢!

更新/调查

基于对@Pawel_Dyl应该多快进行哈希表分配的评论,我让我对代码的变化和更大的数据值集(20万行)进行了一些调查。

以下是测试结果以及持续时间:

#Create the Demo Data... 200k lines
$Src = 1..200000 | % { [pscustomobject]@{Name="Item_$_"; CountOf=$_} }

# Test # 1 - Checking (... -lt $Src.Count) option vs (... -lt @($Src)Count ) ...
# Test 1A - using $Src.CountOf
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash1A = @{}
foreach ($i in $Src) { $hash1A[$i.Name] = $i.CountOf }
$Timer.Stop()
$Timer.ElapsedMilliseconds
# Duration = 736 ms

# Now with @()
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash1B = @{}
foreach ($i in @($Src)) { $hash1B[$i.Name] = $i.CountOf }
$Timer.Stop()
$Timer.ElapsedMilliseconds
# Duration = 728 ms

##################

# Test # 2 - Checking (... -lt $Src.Count) option vs (... -lt @($Src).Count ) ...

$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash2A = @{}
for ($i=0; $i -lt @($Src).Count; $i++) {
    $hash2A.Add(@($Src[$i]).Name, @($Src[$i]).CountOf)
}
$Timer.Stop()
$Timer.ElapsedMilliseconds
# Duration == 4,625,755 (!) (commas added for easier readability!

$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash2B = @{}
for ($i=0; $i -lt $Src.Count; $i++) {
    $hash2B.Add( $Src[$i].Name, $Src[$i].CountOf )
}
$Timer.Stop()
$Timer.ElapsedMilliseconds
# Duration == 1788 ms

因此问题出在使用@()-s在循环中引用数组。旨在防止SQL中的1行数组/结果(出于某种奇怪的原因,Powershell并没有将其作为一个概念,而是将其完全视为DATAOBJECT而不是数组(因此,.Count之类的东西不可用)而不强制POSH通过@()将其作为数组处理。

因此,“暂时”的解决方案是添加一个简单的...     如果(@($ MyArray).Count -eq 1){用@()做事}     ElseIf(@($ MyArray).Count -gt 1){不要使用@()-s做东西

我们有罪魁祸首-在循环中使用@()-s花费了将近1.25个小时,而同一操作大约需要1秒钟。

这种改变极大地加快了速度(即使只有90,000个对象“生气”,构建每个哈希表也只需要0.1秒的时间。对于代码来说,它的方便性稍差一些,但是,哦,我仍然不愿意)无法理解为什么Powershell在“一线数组”的概念上存在问题,并决定以不同的方式/作为单独的数据类型来处理它们,但是您就可以了。

我仍将查看DataReader的建议,以了解在哪里/如何在代码中最好地利用它们,以作为将来的改进。非常感谢您的所有建议和能使一切变得有意义的出色解释!

2 个答案:

答案 0 :(得分:3)

注意:我强烈建议强烈不要使用Count作为输出列的名称,因为它与PowerShell中的默认属性冲突。示例:@().Count返回0。您的代码可能有效,但是非常含糊。强烈建议将查询更改为使用DeviceCount或类似名称。


在PowerShell中使用绝对最快的方法是使用SqlDataReader进行所有操作,然后直接循环输出。假设您的数据源是SQL Server:

$ConnectionString = 'Data Source={0};Initial Catalog={1};Integrated Security=True' -f $SqlServer, $Database
$SqlConnection = [System.Data.SqlClient.SqlConnection]::new($ConnectionString)
$SqlCommand = [System.Data.SqlClient.SqlCommand]::new($SqlQuery, $SqlConnection)

$Data = @{}
$SqlConnection.Open()
try {
    $DataReader = $SqlCommand.ExecuteReader()
    while ($DataReader.Read()) {
        $Data[$DataReader.GetString(0)] = $DataReader.GetInt32(1)
    }
}
finally {
    $SqlConnection.Close()
    $SqlConnection.Dispose()
}

在我的系统上,我可以在大约700毫秒内获取和处理160,000条记录(请记住,我没有使用聚合函数)。

对我来说,使用$Data.Add($DataReader.GetString(0), $DataReader.GetInt32(1))语法而不是$Data[$DataReader.GetString(0)] = $DataReader.GetInt32(1)的速度要慢20%。但是,此方法确实有一个重要警告。 $HashTable.Add($Key, $Value)将在重复键上引发错误。 $HashTable[$Key] = $Value只会默默地替换该值。确保您的SQL查询正确无误,不会返回重复的值

您也可以使用$DataReader['DeviceName']而不是$DataReader.GetString(0),但这意味着SqlDataReader必须进行查找,因此它速度稍慢(大约10%)。使用GetX()方法的缺点是:a)参数01引用列顺序,因此您必须知道输出的列顺序(通常没什么大不了的)和b)您必须知道输出的数据类型(通常也没什么大不了的。)

在第一次运行时,我没有看到使用Dictionary而不是HashTable产生明显的性能差异,但是第一次运行后,使用Dictionary的速度提高了约20%。就是说,冷冷我没什么区别。运行热,我看到字典运行得更快。您可能希望进行测试。如果是这样,请使用以下方法代替使用$Data = @{}

$InitialSize = 51000 # The more accurate this guess is without going under, the better
$Data = [System.Collections.Generic.Dictionary[String,Int32]]::new($InitialSize)

供进一步参考,如果您需要对查询 do 具有重复查询值的SQL结果集进行更快的查询,则为usually fastest to use a DataView,对它进行排序时使用索引进行搜索:

$ConnectionString = 'Data Source={0};Initial Catalog={1};Integrated Security=True' -f $SqlServer, $Database
$SqlConnection = [System.Data.SqlClient.SqlConnection]::new($ConnectionString)
$SqlCommand = [System.Data.SqlClient.SqlCommand]::new($SqlQuery, $SqlConnection)

$DataTable = [System.Data.DataTable]::new()
$SqlConnection.Open()
try {
    $DataReader = $SqlCommand.ExecuteReader()
    $DataTable.Load($DataReader)
}
finally {
    $SqlConnection.Close()
    $SqlConnection.Dispose()
}

$DataView = [System.Data.DataView]::new($DataTable)
$DataView.Sort = 'DeviceName' # Create an index used for Find() and FindRows()
$DataView.Find('DEVICE_1') # -1 means not found, otherwise it's the index of the row
$DataView.FindRows('DEVICE_1')

您可以使用DataAdapter或DataSet;我已经选择在这里只使用一个DataTable,因为我已经有执行它的代码。

答案 1 :(得分:2)

我希望性能瓶颈不在哈希表之内。我测量了我看到的最常用的方法,结果如下:

#demo data
#$src = 1..200000 | % { [pscustomobject]@{Name="Item_$_";Count=$_} }

#1
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash1 = @{}
$src | % {$hash1[$_.Name]=$_.Count}
$timer.Stop()
$timer.ElapsedMilliseconds

#2
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash2 = @{}
for ($i=0; $i -lt $src.Count; $i++) {
    $hash2.Add($src[$i].Name,$src[$i].Count)
}
$timer.Stop()
$timer.ElapsedMilliseconds

#3
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash3 = @{}
foreach ($i in $src.GetEnumerator()) { $hash3[$i.Name] = $i.Count }
$timer.Stop()
$timer.ElapsedMilliseconds

#4
$timer = [System.Diagnostics.Stopwatch]::StartNew()
$hash3 = @{}
foreach ($i in $src) { $hash3[$i.Name] = $i.Count }
$timer.Stop()
$timer.ElapsedMilliseconds

在我的计算机上分别花费了大约5s,〜1.7s,〜0.7s,〜0.7s来完成第1-4节(200000条记录)。如果需要进一步优化,我将评估一些用于构建字典的本机.NET方法。

尝试优化其余代码。提示:

  • 您确定所有记录都在hastable循环之前已在内存中 开始吗?
  • 确定属性是简单类型(int,字符串-提防) 代理,带有“隐藏”代码的属性)?