10

我们如何提高这个查询的速度?

在执行以下查询的范围内,我们大约有100 个消费者。1-2 minutes这些运行中的每一个都代表消耗函数的 1 次运行。

        TableQuery<T> treanslationsQuery = new TableQuery<T>()
         .Where(
          TableQuery.CombineFilters(
            TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, sourceDestinationPartitionKey)
           , TableOperators.Or,
            TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, anySourceDestinationPartitionKey)
          )
         );

此查询将产生大约5000 个结果。

完整代码:

    public static async Task<IEnumerable<T>> ExecuteQueryAsync<T>(this CloudTable table, TableQuery<T> query) where T : ITableEntity, new()
    {
        var items = new List<T>();
        TableContinuationToken token = null;

        do
        {
            TableQuerySegment<T> seg = await table.ExecuteQuerySegmentedAsync(query, token);
            token = seg.ContinuationToken;
            items.AddRange(seg);
        } while (token != null);

        return items;
    }

    public static IEnumerable<Translation> Get<T>(string sourceParty, string destinationParty, string wildcardSourceParty, string tableName) where T : ITableEntity, new()
    {
        var acc = CloudStorageAccount.Parse(Environment.GetEnvironmentVariable("conn"));
        var tableClient = acc.CreateCloudTableClient();
        var table = tableClient.GetTableReference(Environment.GetEnvironmentVariable("TableCache"));
        var sourceDestinationPartitionKey = $"{sourceParty.ToLowerTrim()}-{destinationParty.ToLowerTrim()}";
        var anySourceDestinationPartitionKey = $"{wildcardSourceParty}-{destinationParty.ToLowerTrim()}";

        TableQuery<T> treanslationsQuery = new TableQuery<T>()
         .Where(
          TableQuery.CombineFilters(
            TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, sourceDestinationPartitionKey)
           , TableOperators.Or,
            TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, anySourceDestinationPartitionKey)
          )
         );

        var over1000Results = table.ExecuteQueryAsync(treanslationsQuery).Result.Cast<Translation>();
        return over1000Results.Where(x => x.expireAt > DateTime.Now)
                           .Where(x => x.effectiveAt < DateTime.Now);
    }

在这些执行期间,当有 100 个消费者时,如您所见,请求将聚集并形成峰值:

在此处输入图像描述

在这些高峰期间,请求通常需要 1 分钟以上:

在此处输入图像描述

我们如何提高这个查询的速度?

4

5 回答 5

5

您可以考虑 3 件事:

1 . 首先,摆脱对Where查询结果执行的子句。最好在查询中尽可能多地包含子句(如果您的表上有任何索引也包含它们,那就更好了)。现在,您可以按如下方式更改您的查询:

var translationsQuery = new TableQuery<T>()
.Where(TableQuery.CombineFilters(
TableQuery.CombineFilters(
    TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, sourceDestinationPartitionKey),
    TableOperators.Or,
    TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, anySourceDestinationPartitionKey)
    ),
TableOperators.And,
TableQuery.CombineFilters(
    TableQuery.GenerateFilterConditionForDate("affectiveAt", QueryComparisons.LessThan, DateTime.Now),
    TableOperators.And,
    TableQuery.GenerateFilterConditionForDate("expireAt", QueryComparisons.GreaterThan, DateTime.Now))
));

因为您有大量数据要检索,所以最好并行运行查询。所以,你应该用我写的基于Stephen Toub Parallel.Whiledo while替换循环内部ExecuteQueryAsync方法;这样,它将减少查询执行时间。这是一个不错的选择,因为你可以在调用这个方法的时候移除,但是它有一点限制,我会在这部分代码之后再讲:Parallel.ForEachResult

public static IEnumerable<T> ExecuteQueryAsync<T>(this CloudTable table, TableQuery<T> query) where T : ITableEntity, new()
{
    var items = new List<T>();
    TableContinuationToken token = null;

    Parallel.ForEach(new InfinitePartitioner(), (ignored, loopState) =>
    {
        TableQuerySegment<T> seg = table.ExecuteQuerySegmented(query, token);
        token = seg.ContinuationToken;
        items.AddRange(seg);

        if (token == null) // It's better to change this constraint by looking at https://www.vivien-chevallier.com/Articles/executing-an-async-query-with-azure-table-storage-and-retrieve-all-the-results-in-a-single-operation
            loopState.Stop();
    });

    return items;
}

然后你可以在你的Get方法中调用它:

return table.ExecuteQueryAsync(translationsQuery).Cast<Translation>();

如您所见,方法本身不是异步的(您应该更改它的名称)并且Parallel.ForEach与传入异步方法不兼容。这就是我ExecuteQuerySegmented改用的原因。但是,为了提高性能并利用异步方法的所有优点,您可以将上述ForEach循环替换为DataflowActionBlock中的方法或AsyncEnumerator Nuget 包中的扩展方法。ParallelForEachAsync

2 .执行独立的并行查询然后合并结果是一个不错的选择,即使它的性能提升最多10%。这使您有时间能够找到最佳性能友好的查询。但是,永远不要忘记在其中包含所有约束,并测试两种方法以了解哪一种更适合您的问题。

3 . 我不确定这是否是一个好建议,但是请执行此操作并查看结果。如MSDN中所述:

表服务强制执行服务器超时,如下所示:

  • 查询操作:在超时时间间隔内,查询最多可以执行 5 秒。如果查询未在五秒间隔内完成,则响应包括用于在后续请求中检索剩余项目的继续令牌。有关更多信息,请参阅查询超时和分页。

  • 插入、更新和删除操作:最大超时间隔为 30 秒。三十秒也是所有插入、更新和删除操作的默认间隔。

如果您指定的超时时间小于服务的默认超时时间,则将使用您的超时时间间隔。

所以你可以玩超时并检查是否有任何性能改进。

2021 年 6 月 30 日更新

感谢@WouterVanRanst 仔细查看上面的代码片段,我决定更新它并使用另一个重载Parallel.ForEach方法,使循环成为单线程并防止在TableContinuationToken. 您可以在 MSDN 上找到有关分区局部变量的描述以及此处的示例。这是方法的新外观ExecuteQueryAsync<T>

public static IEnumerable<T> ExecuteQueryAsync<T>(this CloudTable table, TableQuery<T> query) where T : ITableEntity, new()
{
    TableContinuationToken token = null;
    var items = new List<T>();

    Parallel.ForEach(new InfinitePartitioner(), () =>
    {
        return null as TableQuerySegment<T>;
    }, (ignored, loopState, segment) =>
    {
        segment = table.ExecuteQuerySegmented(query, token) as TableQuerySegment<T>;
        
        token = segment.ContinuationToken;

        if (token == null)
            loopState.Stop();

        return segment;
    },
    (seg) => items.AddRange(seg)
    );

    return items;
}

注意: 当然,您可以完善上面的代码或找到更好的方法来防止竞争条件,但它很快就会很简单。我很高兴听到您对此的看法。

于 2019-11-12T01:29:29.223 回答
3
  var over1000Results = table.ExecuteQueryAsync(treanslationsQuery).Result.Cast<Translation>();
        return over1000Results.Where(x => x.expireAt > DateTime.Now)
                           .Where(x => x.effectiveAt < DateTime.Now);

这是问题之一,您正在运行查询,然后使用这些“wheres”从内存中过滤它。将过滤器移动到查询运行之前,这应该会有很大帮助。

其次,您必须提供一些从数据库中检索的行数限制

于 2019-11-04T22:56:12.913 回答
2

因此,秘诀不仅在于代码,还在于设置 Azure 存储表。

a) 在 Azure 中优化查询的主要选项之一是引入缓存。这将大大减少您的整体响应时间,从而避免您提到的高峰时段的瓶颈。

b) 此外,当从 Azure 中查询实体时,最快的方法是同时使用 PartitionKey 和 RowKey。这些是表存储中唯一的索引字段,任何使用这两者的查询都将在几毫秒内返回。因此,请确保您同时使用 PartitionKey 和 RowKey。

在此处查看更多详细信息: https ://docs.microsoft.com/en-us/azure/storage/tables/table-storage-design-for-query

希望这可以帮助。

于 2019-11-13T17:31:18.007 回答
2

不幸的是,下面的查询引入了全表扫描

    TableQuery<T> treanslationsQuery = new TableQuery<T>()
     .Where(
      TableQuery.CombineFilters(
        TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, sourceDestinationPartitionKey)
       , TableOperators.Or,
        TableQuery.GenerateFilterCondition("PartitionKey", QueryComparisons.Equal, anySourceDestinationPartitionKey)
      )
     );

您应该将其拆分为两个 Partition Key 过滤器并分别查询它们,这将成为两个分区扫描并且执行效率更高。

于 2019-11-05T00:29:35.353 回答
-1

注意:这是一般的数据库查询优化建议。

ORM 可能正在做一些愚蠢的事情。在进行优化时,可以降低抽象层。所以我建议用查询语言(SQL?)重写查询,以便更容易看到发生了什么,也更容易优化。

优化查找的关键是排序!与每次查询时扫描整个表相比,保持表排序通常要便宜得多!因此,如果可能,请按照查询中使用的键对表进行排序。在大多数数据库解决方案中,这是通过创建索引键来实现的。

如果组合很少,另一种效果很好的策略是将每个查询作为一个单独的(内存中的临时)表,该表始终是最新的。因此,当插入某些内容时,它也会“插入”到“视图”表中。一些数据库解决方案将此称为“视图”。

更粗暴的策略是创建只读副本来分配负载。

于 2019-11-13T22:45:54.493 回答