如何使两个 SQL 查询真正异步

Posted

技术标签:

【中文标题】如何使两个 SQL 查询真正异步【英文标题】:How to make two SQL queries really asynchronous 【发布时间】:2014-12-05 21:22:53 【问题描述】:

我的问题是基于一个真实的项目问题,但我从未使用过 System.Threading.Tasks 库或执行任何涉及线程的严肃编程,所以我的问题可能是缺乏对特定库的了解和对什么的更普遍的误解就编程而言,异步确实意味着。

所以我的真实情况是这样的 - 我需要获取有关用户的数据。在我目前的情况下,它是财务数据,所以假设我需要某个用户的所有Accounts、所有Deposits 和所有Consignations。在我的情况下,这意味着要为每个属性查询数百万条记录,并且每个查询本身相对较慢,但是获取Accounts 比获取Deposits 慢几倍。所以我为我要使用的三种银行产品定义了三个类,当我想获取某个用户的所有银行产品的数据时,我会这样做:

List<Account> accounts = GetAccountsForClient(int clientId);
List<Deposit> deposits = GetDepositsForClient(int clientId);
List<Consignation> consignations = GetConsignationsForClient(int clientId);

所以问题从这里开始,我需要同时获取所有这三个列表,因为我要将它们传递给显示所有用户数据的视图。但是现在执行是同步的(如果我在这里正确使用了该术语),因此收集所有三种产品的数据的总时间是:

Total_Time = Time_To_Get_Accounts + Time_To_Get_Deposits + Time_To_Get_Consignations

这不好,因为每个查询都比较慢,所以总时间相当长,而且,accounts 查询比其他两个查询花费更多的时间,所以今天进入我脑海的想法是 - “如果我可以同时执行这些查询会怎样”。也许这是我对这个话题最大的误解,但对我来说,最接近这个想法的是让它们异步所以也许Total_Time 不会是最慢查询的时间,但会比所有三个的总和快得多查询。

由于我的代码很复杂,我创建了一个简单的用例,我认为它可以很好地反映我正在努力做的事情。我有两种方法:

public static async Task<int> GetAccounts()

    int total1 = 0;
    using (SqlConnection connection = new SqlConnection(connString))
    
        string query1 = "SELECT COUNT(*) FROM [MyDb].[dbo].[Accounts]";
        SqlCommand command = new SqlCommand(query1, connection);
        connection.Open();
        for (int i = 0; i < 19000000; i++)
        
            string s = i.ToString();
        
        total1 = (int) await command.ExecuteScalarAsync();
        Console.WriteLine(total1.ToString());
    
    return total1;

第二种方法:

public static async Task<int> GetDeposits()

    int total2 = 0;
    using (SqlConnection connection = new SqlConnection(connString))
    
        string query2 = "SELECT COUNT(*) FROM [MyDb].[dbo].[Deposits]";
        SqlCommand command = new SqlCommand(query2, connection);
        connection.Open();
        total2 = (int) await command.ExecuteScalarAsync();
        Console.WriteLine(total2.ToString());
    
    return total2;

我这样称呼:

static void Main(string[] args)

    Console.WriteLine(GetAccounts().Result.ToString());

    Console.WriteLine(GetDeposits().Result.ToString());

如您所见,我首先调用GetAccounts(),并故意减慢执行速度,因此我给了执行机会以继续下一个方法。但是我在一段时间内没有得到任何结果,然后我同时在控制台上打印出来。

所以问题-如何使我不等待第一个方法完成,以便转到下一个方法。一般来说,代码结构并不那么重要,我真正想弄清楚的是是否有任何方法可以使两个查询同时执行。这里的示例是我的研究结果,也许可以扩展到我会得到想要的结果的地步。

附言 我使用ExecuteScalarAsync(); 只是因为我从使用它的方法开始。实际上我会使用ScalarReader

【问题讨论】:

您应该使用await connection.OpenAsync();,因此连接设置也是异步完成的。 【参考方案1】:

当您对尚未完成的任务使用Result 属性时,调用线程将阻塞,直到操作完成。这意味着在您的情况下,GetAccounts 操作需要在对 GetDeposits 的调用开始之前完成。

如果您想确保这些方法是并行的(包括同步的 CPU 密集型部分),您需要将该工作卸载到另一个线程。最简单的方法是使用Task.Run

static async Task Main()

    var accountTask = Task.Run(async () => Console.WriteLine(await GetAccounts()));
    var depositsTask = Task.Run(async () => Console.WriteLine(await GetDeposits()));

    await Task.WhenAll(accountTask, depositsTask);

【讨论】:

@I3arnon 我想我什么都不懂。我最初的想法是在不同的时间得到结果。根据您的描述,我认为可能像这样总时间等于最慢的时间,但是当我添加到第二种方法for (int i = 0; i &lt; 19000000; i++) string s = i.ToString(); 时,时间翻了一番。这很奇怪。如果它是同时发生的,并且在这种情况下,查询需要几毫秒,我期望的时间与只有一种方法变慢时的时间相同。 我不确定我是否清楚。我的想法是,如果GetAccounts() 需要 3 秒,GetDeposits() 少于 sec,我希望 3 秒才能得到结果。但是如果我强制GetDeposits() 执行 3 秒而不是再次获得 3 秒,因为这是最慢的时间并且任务同时执行,我会在 6 秒后得到结果。如果我按原样尝试这种方法,就会发生这种情况。我错过了什么吗? @Leron 异步与并行不同。您的方法既有异步部分也有同步部分(I/O 和 CPU)。您可以轻松地让异步部分同时执行,就像我在回答中所做的那样。要使同步部分并行执行,您需要使用 2 个线程和 Task.Run。这就是你想要的吗? 抱歉复活了一个老问题,但是标题“如何让两个SQL查询真正异步”。 IMO,如果查询是异步的,而其余代码不是,则满足原始问题。澄清一下,也可以这样写:var accountTask = GetAccounts();var depositsTask = GetDeposits();await Task.WhenAll(accountsTask, depositsTask);// Do something with the results. SQL 部分仍将并行执行,对吗? @BryanRayner 他们将同时执行。但是这些方法的同步部分会按顺序运行。【参考方案2】:

这是一种异步并行执行两个任务的方法:

Task<int> accountTask = GetAccounts();
Task<int> depositsTask = GetDeposits();

int[] results = await Task.WhenAll(accountTask, depositsTask);

int accounts = results[0];
int deposits = results[1];

【讨论】:

在连接字符串中必须指定“MultipleActiveResultSets=True”dapper-tutorial.net/knowledge-base/46163437/… @Pažout 您实际上不需要这样做,因为 OP 没有在两个调用之间重用单个连接。每个任务都有自己的数据库连接。【参考方案3】:

我通常更喜欢使用 Task.WaitAll。为了设置此代码段,我更改了 GetAccounts/GetDeposits 签名只是为了返回 int (public static int GetAccounts())

我将 Console.WriteLine 放在与分配返回相同的线程中,以验证 GetDeposits 在 GetAccounts 之前返回,但这是不必要的,最好将其移动到 Task.WaitAll 之后

     private static void Main(string[] args) 

        int getAccountsTask = 0;
        int getDepositsTask = 0;
        List<Task> tasks = new List<Task>() 
            Task.Factory.StartNew(() => 
                getAccountsTask = GetAccounts();
                Console.WriteLine(getAccountsTask);
            ),
            Task.Factory.StartNew(() => 
                getDepositsTask = GetDeposits();
                Console.WriteLine(getDepositsTask);

            )

        ;
        Task.WaitAll(tasks.ToArray());



    

【讨论】:

正是我想要的。您能否就如何获取有关您刚刚所做的事情的更多信息提供一些指导? 请注意,Task.WaitAll() 的返回类型为 void,因此如果任务返回某些内容,您将不会得到结果。要获得它,您需要使用await Task.WhenAll() 这根本不是异步的。这是并行执行的 2 个完全同步的操作。 @Leron, Task.Factory.StartNew 创建一个新任务并立即启动它,执行匿名方法() =&gt; getAccountsTask = GetAccounts(); Console.WriteLine(getAccountsTask); 。 Task.WaitAll() 暂停线程的执行,直到数组中的所有任务都完成。正如 L3arnon 指出的那样,这些是同步/并行运行的任务(假设这更接近于基于上下文的您想要的) @abatishchev 由于任务存储在一个数组中,您可以通过枚举tasks获得执行结果,但是直接返回想要的结果(来自ExecuteScalar的int值)到成员变量(getAccountsTaskgetDepositsTask)并且不依赖于来自任务的执行信息。【参考方案4】:

如果是 ASP.NET,则在呈现页面后使用 AJAX 获取并将数据放入存储中。每个 AJAX 获取都是异步的。如果您想在服务器上同时创建 SQL 查询?

用法:

 // Add some queries ie. ThreadedQuery.NamedQuery([Name], [SQL])
 var namedQueries= new ThreadedQuery.NamedQuery[] ... ;

 System.Data.DataSet ds = ThreadedQuery.RunThreadedQuery(
 "Server=foo;Database=bar;Trusted_Connection=True;", 
 namedQueries).Result;


 string msg = string.Empty;
 foreach (System.Data.DataTable tt in ds.Tables)
 msg += string.Format("0: 1\r\n", tt.TableName, tt.Rows.Count);

来源:

public class ThreadedQuery


    public class NamedQuery
    
        public NamedQuery(string TableName, string SQL)
        
            this.TableName = TableName;
            this.SQL = SQL;
        
        public string TableName  get; set; 
        public string SQL  get; set; 
    
    public static async System.Threading.Tasks.Task<System.Data.DataSet> RunThreadedQuery(string ConnectionString, params NamedQuery[] queries)
    

        System.Data.DataSet dss = new System.Data.DataSet();
        List<System.Threading.Tasks.Task<System.Data.DataTable>> asyncQryList = new List<System.Threading.Tasks.Task<System.Data.DataTable>>();

        foreach (var qq in queries)
            asyncQryList.Add(fetchDataTable(qq, ConnectionString));

        foreach (var tsk in asyncQryList)
        
            System.Data.DataTable tmp = await tsk.ConfigureAwait(false);
            dss.Tables.Add(tmp);
        

        return dss;

    

    private static async System.Threading.Tasks.Task<System.Data.DataTable> fetchDataTable(NamedQuery qry, string ConnectionString)
    
        // Create a connection, open it and create a command on the connection
        try
        

            System.Data.DataTable dt = new System.Data.DataTable(qry.TableName);
            using (SqlConnection connection = new SqlConnection(ConnectionString))
            
                await connection.OpenAsync().ConfigureAwait(false);
                System.Diagnostics.Debug.WriteLine("Connection Opened ... " + qry.TableName);
                using (SqlCommand command = new SqlCommand(qry.SQL, connection))
                
                    using (SqlDataReader reader = command.ExecuteReader())
                    
                        System.Diagnostics.Debug.WriteLine("Query Executed ... " + qry.TableName);

                        dt.Load(reader);

                        System.Diagnostics.Debug.WriteLine(string.Format("Record Count '0' ... 1", dt.Rows.Count, qry.TableName));

                        return dt;
                    
                
            
        
        catch(Exception ex)
        

            System.Diagnostics.Debug.WriteLine("Exception Raised ... " + qry.TableName);
            System.Diagnostics.Debug.WriteLine(ex.Message);

            return new System.Data.DataTable(qry.TableName);
        

    

【讨论】:

【参考方案5】:

如果该过程需要很长时间,则异步非常好。另一种选择是使用一个存储过程来返回所有三个记录集。

        adp = New SqlDataAdapter(cmd)
        dst = New DataSet
        adp.Fill(dst)

在页面后面的代码中,将它们称为 dst.Tables(0)、dst.Tables(1) 和 dst.Tables(2)。这些表将与存储过程中的 select 语句的顺序相同。

【讨论】:

你能用正确的语法更新你的答案吗?

以上是关于如何使两个 SQL 查询真正异步的主要内容,如果未能解决你的问题,请参考以下文章

如何异步查询多条SQL?

如何使用 SQL 显示两个不同区域中时间戳之间的实时差异?

sql中如何使一列中的多个重复数据只显示第一条

SQL查询性能调优--如何使查询更快

SQL severa 中l数据库如何进行查询

如何使这个 SQL 查询更高效?