如何异步和在线程中执行大量的SQL查询

时间:2019-07-12 14:14:14

标签: sql multithreading powershell asynchronous parallel-processing

问题::我有大量的SQL查询(大约10k-20k),我想在50个(或更多)线程中异步运行它们。

我为此工作编写了一个powershell脚本,但是它很慢(执行所有操作大约需要20个小时)。 所需结果最多为3-4小时

问题:如何优化此Powershell脚本?我应该重新考虑并使用pythonc#之类的其他技术吗?

我认为这是powershell的问题,因为当我使用whoisactive检查时,查询正在快速执行。创建,退出和卸载作业需要花费大量时间,因为为每个线程创建了单独的PS实例。

我的代码:

$NumberOfParallerThreads = 50;


$Arr_AllQueries = @('Exec [mystoredproc] @param1=1, @param2=2',
                    'Exec [mystoredproc] @param1=11, @param2=22',
                    'Exec [mystoredproc] @param1=111, @param2=222')

#Creating the batches
$counter = [pscustomobject] @{ Value = 0 };
$Batches_AllQueries = $Arr_AllQueries | Group-Object -Property { 
    [math]::Floor($counter.Value++ / $NumberOfParallerThreads) 
};

forEach ($item in $Batches_AllQueries) {
    $tmpBatch = $item.Group;

    $tmpBatch | % {

        $ScriptBlock = {
            # accept the loop variable across the job-context barrier
            param($query) 
            # Execute a command

            Try 
            {
                Write-Host "[processing '$query']"
                $objConnection = New-Object System.Data.SqlClient.SqlConnection;
                $objConnection.ConnectionString = 'Data Source=...';

                $ObjCmd = New-Object System.Data.SqlClient.SqlCommand;
                $ObjCmd.CommandText = $query;
                $ObjCmd.Connection = $objConnection;
                $ObjCmd.CommandTimeout = 0;

                $objAdapter = New-Object System.Data.SqlClient.SqlDataAdapter;
                $objAdapter.SelectCommand = $ObjCmd;
                $objDataTable = New-Object System.Data.DataTable;
                $objAdapter.Fill($objDataTable)  | Out-Null;

                $objConnection.Close();
                $objConnection = $null;
            } 
            Catch 
            { 
                $ErrorMessage = $_.Exception.Message
                $FailedItem = $_.Exception.ItemName
                Write-Host "[Error processing: $($query)]" -BackgroundColor Red;
                Write-Host $ErrorMessage 
            }

        }

        # pass the loop variable across the job-context barrier
        Start-Job $ScriptBlock -ArgumentList $_ | Out-Null
    }

    # Wait for all to complete
    While (Get-Job -State "Running") { Start-Sleep 2 }

    # Display output from all jobs
    Get-Job | Receive-Job | Out-Null

    # Cleanup
    Remove-Job *

}

更新

资源:数据库服务器位于具有以下位置的远程计算机上:

  • 24GB内存,
  • 8个核心,
  • 500GB存储空间,
  • SQL Server Management Studio(SSMS)18.0

我们要使用最大的cpu功率。

框架限制:唯一的限制是 不是 使用SSMS 来执行查询。请求应该来自外部资源,例如:Powershell,C#,Python等。

6 个答案:

答案 0 :(得分:4)

RunspacePool是前往此处的方法,请尝试以下操作:

$AllQueries = @( ... )
$MaxThreads = 5

# Each thread keeps its own connection but shares the query queue
$ScriptBlock = {
    Param($WorkQueue)

    $objConnection = New-Object System.Data.SqlClient.SqlConnection
    $objConnection.ConnectionString = 'Data Source=...'

    $objCmd = New-Object System.Data.SqlClient.SqlCommand
    $objCmd.Connection = $objConnection
    $objCmd.CommandTimeout = 0

    $query = ""

    while ($WorkQueue.TryDequeue([ref]$query)) {
        $objCmd.CommandText = $query
        $objAdapter = New-Object System.Data.SqlClient.SqlDataAdapter $objCmd
        $objDataTable = New-Object System.Data.DataTable
        $objAdapter.Fill($objDataTable) | Out-Null
    }

    $objConnection.Close()

}

# create a pool
$pool = [RunspaceFactory]::CreateRunspacePool(1, $MaxThreads)
$pool.ApartmentState  = 'STA'
$pool.Open()

# convert the query array into a concurrent queue
$workQueue = New-Object System.Collections.Concurrent.ConcurrentQueue[object]
$AllQueries | % { $workQueue.Enqueue($_) }

$threads = @()

# Create each powershell thread and add them to the pool
1..$MaxThreads | % {
    $ps = [powershell]::Create()
    $ps.RunspacePool = $pool
    $ps.AddScript($ScriptBlock) | Out-Null
    $ps.AddParameter('WorkQueue', $workQueue) | Out-Null
    $threads += [pscustomobject]@{
        Ps = $ps
        Handle = $null
    }
}

# Start all the threads
$threads | % { $_.Handle = $_.Ps.BeginInvoke() }

# Wait for all the threads to complete - errors will still set the IsCompleted flag
while ($threads | ? { !$_.Handle.IsCompleted }) {
    Start-Sleep -Seconds 1
}

# Get any results and display an errors
$threads | % {
    $_.Ps.EndInvoke($_.Handle) | Write-Output
    if ($_.Ps.HadErrors) {
        $_.Ps.Streams.Error.ReadAll() | Write-Error
    }
}

与powershell作业不同,RunspacePools可以共享资源。因此,所有查询都有一个并发队列,并且每个线程都保持自己与数据库的连接。

不过,正如其他人所说的那样-除非您正在对数据库进行压力测试,否则最好将查询重新组织为批量插入。

答案 1 :(得分:2)

可悲的是,我现在没有时间充分回答这个问题,但这应该有所帮助:

首先,您几乎不会保证要使用整个CPU插入那么多记录。但是!

自从出现以来,您正在使用SQL字符串命令:

  1. 将插入片段分成〜100-〜1000组并手动构建批量插入片段:

像这样的POC:

  $query = "INSERT INTO [dbo].[Attributes] ([Name],[PetName]) VALUES "

  for ($alot = 0; $alot -le 10; $alot++){
     for ($i = 65; $i -le 85; $i++) {
       $query += "('" + [char]$i + "', '" + [char]$i + "')"; 
       if ($i -ne 85 -or $alot -ne 10) {$query += ",";}
      }
   }

一旦构建了批次,就可以有效地使用现有代码将其传递给SQL以进行插入。

buld插入看起来像:

INSERT INTO [dbo].[Attributes] ([Name],[PetName]) VALUES ('A', 'A'),('B', 'B'),('C', 'C'),('D', 'D'),('E', 'E'),('F', 'F'),('G', 'G'),('H', 'H'),('I', 'I'),('J', 'J'),('K', 'K'),('L', 'L'),('M', 'M'),('N', 'N'),('O', 'O'),('P', 'P'),('Q', 'Q'),('R', 'R'),('S', 'S')

仅此一项就可以使您的刀片加速一吨!

  1. 如前所述,不要使用50个线程,除非您有25个以上的逻辑核心。您将花费大部分SQL插入时间在网络上等待,而硬盘驱动器而不是CPU。通过排队那么多线程,您可以将大部分CPU时间保留在等待堆栈中较慢的部分上。

仅凭这两件事,我就可以将您的插入内容缩短到大约几分钟(我使用这种方法在大约90秒内完成了80k次以上操作)。

最后一部分可能是重构的,以便每个内核都有自己的Sql连接,然后将其保持打开状态,直到准备好处置所有线程为止。

答案 2 :(得分:1)

我对powershell不太了解,但是我确实在工作中始终使用C#执行SQL。

C#的新async / await关键字使您轻松完成您正在谈论的事情。 C#还将为您创建一个线程池,并为您的计算机提供最佳线程数量。

async Task<DataTable> ExecuteQueryAsync(query)
{
    return await Task.Run(() => ExecuteQuerySync(query));
}

async Task ExecuteAllQueriesAsync()
{
    IList<Task<DataTable>> queryTasks = new List<Task<DataTable>>();

    foreach query
    {
         queryTasks.Add(ExecuteQueryAsync(query));
    }

    foreach task in queryTasks
    {
         await task;
    }
}

上面的代码会将所有查询添加到线程池的工作队列中。 然后等待它们全部完成。结果是您的SQL将达到最大并行度。

希望这会有所帮助!

答案 3 :(得分:0)

尝试使用SqlCmd

您可以使用Process.Start()运行多个进程,并使用sqlcmd在并行进程中运行查询。

当然,如果您必须在线程中执行此操作,那么此答案将不再是解决方案。

答案 4 :(得分:0)

您需要重新组织脚本,以使数据库连接在每个工作线程中保持打开状态,并将其用于该线程执行的所有查询。现在,您正在为每个查询打开一个新的数据库连接,这将增加大量的开销。消除开销可以使事情达到或超过目标速度。

答案 5 :(得分:0)

  1. 根据该表和对该表的操作对查询进行分组。 使用此功能,您可以确定可以对不同的表运行多少异步sql查询。
  2. 确保要运行的每个表的大小。 因为如果表包含数百万行,并且您也与其他表进行联接操作会增加时间,或者如果它是CUD操作,那么也可能会锁定表。
    1. 还要根据您的CPU内核而不是根据假设选择线程数。由于CPU内核一次只能运行一个进程,因此更好,您可以创建内核数量* 2 个线程是高效的。

因此,首先研究您的数据集,然后执行上面的两项,以便您可以轻松识别所有并行且有效运行的查询。

希望这会给您一些想法。更好的是,您可以为此使用任何python脚本,以便您可以轻松触发多个进程并监视其活动。