【问题标题】:Linq to SQL count grouped elements generating a timeoutLinq to SQL 计数生成超时的分组元素
【发布时间】:2015-08-11 16:30:59
【问题描述】:

我有一个如下所示的表格:

FruitID | FruitType
  23    |    2
  215   |    2
  256   |    1
  643   |    3

我想通过FruitType 获得计数,给定一个名为TheFruitIDs 的FruitIDs 列表。这就是我所拥有的:

var TheCounter = (from f in MyDC.Fruits    
                  where TheFruitIDs.Contains(f.FruitID) 
                  group f by 0 into TheFruits
                  select new MyCounterMode()
                  {
                     CountType1 = (int?) TheFruits.Where(f => f.FruitType == 1).Count() ?? 0,
                     CountType2 = (int?) TheFruits.Where(f => f.FruitType == 2).Count() ?? 0,
                     .... all the way to CountType6      
                  }).Single();

此代码有效,但问题是有时我会收到超时错误,因为查询运行时间过长。如何更改此代码以避免超时问题?

【问题讨论】:

  • 您确定这会转化为单个查询吗?似乎该投影可能每行产生 6 个新查询。是这样吗?
  • 这并不能回答你的问题,但Count() 返回一个int,而不是int?,即使转换为SQL;因此空合并运算符是不必要的(甚至可能不起作用)。这是因为当转换为int? 时,它永远不会有空值,因为 0 的值不为空。我很惊讶编译时没有将演员表包装在一组额外的括号中,但这可能是复制粘贴的事情。
  • 您将不得不查看生成的 SQL 并在查询分析器中运行它。
  • group f by 0 into TheFruits 这有什么意义?
  • TheFruitIDs 通常包含多少个元素?

标签: c# .net linq linq-to-sql


【解决方案1】:

查询的最简单方法是按FruitType 分组,然后计算行数:

var countsDictionary = MyDC
  .Fruits
  .Where(f => TheFruitIDs.Contains(f.FruitID))
  .GroupBy(
    f => f.FruitType,
    (fruitType, fruits) => new { FruitType = fruitType, Count = fruits.Count() }
  )
  .ToDictionary(c => c.FruitType, c => c.Count);

这将有效地创建以下字典(假设 where 部分未排除任何数据):

水果类型 |数数 ----------+------ 1 | 1 2 | 2 3 | 1

如果您真的想将其折叠成一个具有特定水果类型计数的对象,则必须创建此对象:

var TheCounter = new {
  CountType1 = countsDictionary.ContainsKey(1) ? countsDictionary[1] : 0,
  CountType2 = countsDictionary.ContainsKey(2) ? countsDictionary[2] : 0,
  CountType3 = countsDictionary.ContainsKey(3) ? countsDictionary[3] : 0
};

您的查询中还有另一件事可能会导致性能问题,从而可能导致超时:where 部分中的水果 ID 列表包含在查询中,如果该列表非常大,可能会减慢您的查询速度.除非您根据对数据库的先前查询创建此列表,否则您无能为力。在这种情况下,您应该尽量避免将水果 ID 列表拉到客户端。相反,您应该将选择 ID 的查询与计算类型的查询结合起来。这将确保整个查询在服务器端执行。

您似乎关心代码的结构变化。只要您正在创建匿名对象,就很难编写可重用的代码。您可以考虑仅使用带有计数或类似内容的字典。另一种选择是使用计数创建一个动态对象。就个人而言,我不喜欢这种解决方案,但您可能会发现它很有用。

为了简化代码,需要一个类来存储计数:

class TypeCount {

  public TypeCount(Int32 type, Int32 count) {
    Type = type;
    Count = count;
  }

  public Int32 Type { get; private set; }

  public Int32 Count { get; private set; }

}

具有基于元组序列的属性CountType0、CountType1、CountType2 等的动态对象:

class CountsDictionary : DynamicObject {

  readonly IDictionary<Int32, Int32> counts;

  public CountsDictionary(IEnumerable<TypeCount> typeCounts) {
    if (typeCounts== null)
      throw new ArgumentNullException("typeCounts");
    this.counts = typeCounts.ToDictionary(c => c.Type, c => c.Count);
  }

  public override Boolean TryGetMember(GetMemberBinder binder, out Object result) {
    Int32 value;
    if (binder.Name.StartsWith("CountType") && Int32.TryParse(binder.Name.Substring(9), NumberStyles.None, CultureInfo.InvariantCulture, out value) && value >= 0) {
      result = this.counts.ContainsKey(value) ? this.counts[value] : 0;
      return true;
    }
    result = 0;
    return false;
  }

}

创建动态对象的扩展方法:

static class CountExtensions {

  public static dynamic ToCounts(this IEnumerable<TypeCount> typeCounts) {
    return new CountsDictionary(typeCounts);
  }

}

把它们放在一起:

var counts = MyDC
  .Fruits
  .Where(f => TheFruitIDs.Contains(f.FruitID))
  .GroupBy(
    f => f.FruitType,
    (fruitType, fruits) => new TypeCount(fruitType, fruits.Count())
  )
  .ToCounts();

然后您可以检索属性counts.CountType1、counts.CountType2 和counts.CountType3。其他 count.CountType# 属性将返回 0。但是,由于 counts 是动态的,因此您不会获得任何智能感知。

【讨论】:

  • 我需要将分组和计数器对象填充到数据库中而不是内存中。
  • @frenchie:分组和计数都将在服务器端进行。执行ToList 或在本例中为ToDictionary 会将包含水果类型和计数的行拉到客户端。我决定使用ToDictionary 来提高每个计数的查找效率,但如果只有几行,您不妨使用ToList,然后在列表中对每个计​​数执行线性搜索。
  • 是的,但问题是数据库中没有填充 MyCounterModel。如何像在问题查询中一样填充它?
  • @frenchie:您有超时问题,因为您问题中的查询效率低下。我的答案(以及至少一个其他答案)提供了我认为对记录进行分组和计数的最有效方法,并且它不应该受到超时的影响。我不明白您关于 MyCounterModel 未在数据库中填充 的说法。任何 C# 对象都存在,因此从数据库返回的表格数据在客户端上“填充”。您的查询返回一行六列。我的查询返回六行两列。此外,您的查询会导致服务器超时。
  • 是的,我知道您的查询效率更高,但我想知道如何在我的代码中实现您的查询,以便在一次操作中将输出映射到我拥有的对象模型?该查询只是我用于此问题的一个模式示例。实际上,我有几个使用计数器的查询,这些计数器实际上是更大查询的子查询。我将超时问题与子查询中的计数器隔离开来,这就是我询问如何填充对象模型的原因。但是,是的,您的问题是我要去的方向;我已经测试过了,性能很棒!
【解决方案2】:

这是您的查询翻译成的内容:

SELECT
[Limit1].[C1] AS [C1],
[Limit1].[C2] AS [C2],
[Limit1].[C3] AS [C3],
[Limit1].[C4] AS [C4],
[Limit1].[C5] AS [C5],
[Limit1].[C6] AS [C6],
[Limit1].[C7] AS [C7]
FROM ( SELECT TOP (2)
    [Project13].[C1] AS [C1],
    CASE WHEN ([Project13].[C2] IS NULL) THEN 0 ELSE [Project13].[C3] END AS [C2],
    CASE WHEN ([Project13].[C4] IS NULL) THEN 0 ELSE [Project13].[C5] END AS [C3],
    CASE WHEN ([Project13].[C6] IS NULL) THEN 0 ELSE [Project13].[C7] END AS [C4],
    CASE WHEN ([Project13].[C8] IS NULL) THEN 0 ELSE [Project13].[C9] END AS [C5],
    CASE WHEN ([Project13].[C10] IS NULL) THEN 0 ELSE [Project13].[C11] END AS [C6],
    CASE WHEN ([Project13].[C12] IS NULL) THEN 0 ELSE [Project13].[C13] END AS [C7]
    FROM ( SELECT
        [Project12].[C1] AS [C1],
        [Project12].[C2] AS [C2],
        [Project12].[C3] AS [C3],
        [Project12].[C4] AS [C4],
        [Project12].[C5] AS [C5],
        [Project12].[C6] AS [C6],
        [Project12].[C7] AS [C7],
        [Project12].[C8] AS [C8],
        [Project12].[C9] AS [C9],
        [Project12].[C10] AS [C10],
        [Project12].[C11] AS [C11],
        [Project12].[C12] AS [C12],
        (SELECT
            COUNT(1) AS [A1]
            FROM [dbo].[Fruits] AS [Extent13]
            WHERE ([Extent13].[FruitID] IN (23, 215, 256, 643)) AND ([Project12].[C1] = 0) 
            AND (6 = [Extent13].[FruitType])) AS [C13]
        FROM ( SELECT
            [Project11].[C1] AS [C1],
            [Project11].[C2] AS [C2],
            [Project11].[C3] AS [C3],
            [Project11].[C4] AS [C4],
            [Project11].[C5] AS [C5],
            [Project11].[C6] AS [C6],
            [Project11].[C7] AS [C7],
            [Project11].[C8] AS [C8],
            [Project11].[C9] AS [C9],
            [Project11].[C10] AS [C10],
            [Project11].[C11] AS [C11],
            (SELECT
                COUNT(1) AS [A1]
                FROM [dbo].[Fruits] AS [Extent12]
                WHERE ([Extent12].[FruitID] IN (23, 215, 256, 643)) 
                AND ([Project11].[C1] = 0) 
                AND (6 = [Extent12].[FruitType])) AS [C12]
            FROM ( SELECT
                [Project10].[C1] AS [C1],
                [Project10].[C2] AS [C2],
                [Project10].[C3] AS [C3],
                [Project10].[C4] AS [C4],
                [Project10].[C5] AS [C5],
                [Project10].[C6] AS [C6],
                [Project10].[C7] AS [C7],
                [Project10].[C8] AS [C8],
                [Project10].[C9] AS [C9],
                [Project10].[C10] AS [C10],
                (SELECT
                    COUNT(1) AS [A1]
                    FROM [dbo].[Fruits] AS [Extent11]
                    WHERE ([Extent11].[FruitID] IN (23, 215, 256, 643)) 
                    AND([Project10].[C1] = 0) 
                    AND (5 = [Extent11].[FruitType])) AS [C11]
                FROM ( SELECT
                    [Project9].[C1] AS [C1],
                    [Project9].[C2] AS [C2],
                    [Project9].[C3] AS [C3],
                    [Project9].[C4] AS [C4],
                    [Project9].[C5] AS [C5],
                    [Project9].[C6] AS [C6],
                    [Project9].[C7] AS [C7],
                    [Project9].[C8] AS [C8],
                    [Project9].[C9] AS [C9],
                    (SELECT
                        COUNT(1) AS [A1]
                        FROM [dbo].[Fruits] AS [Extent10]
                        WHERE ([Extent10].[FruitID] IN (23, 215, 256, 643))
                        AND ([Project9].[C1] = 0) 
                        AND (5 = [Extent10].[FruitType])) AS [C10]
                    FROM ( SELECT
                        [Project8].[C1] AS [C1],
                        [Project8].[C2] AS [C2],
                        [Project8].[C3] AS [C3],
                        [Project8].[C4] AS [C4],
                        [Project8].[C5] AS [C5],
                        [Project8].[C6] AS [C6],
                        [Project8].[C7] AS [C7],
                        [Project8].[C8] AS [C8],
                        (SELECT
                            COUNT(1) AS [A1]
                            FROM [dbo].[Fruits] AS [Extent9]
                            WHERE ([Extent9].[FruitID] IN (23, 215, 256, 643)) 
                            AND ([Project8].[C1] = 0) 
                            AND (4 = [Extent9].[FruitType])) AS [C9]
                        FROM ( SELECT
                            [Project7].[C1] AS [C1],
                            [Project7].[C2] AS [C2],
                            [Project7].[C3] AS [C3],
                            [Project7].[C4] AS [C4],
                            [Project7].[C5] AS [C5],
                            [Project7].[C6] AS [C6],
                            [Project7].[C7] AS [C7],
                            (SELECT
                                COUNT(1) AS [A1]
                                FROM [dbo].[Fruits] AS [Extent8]
                                WHERE ([Extent8].[FruitID] IN (23, 215, 256, 643)) 
                                AND ([Project7].[C1] = 0) 
                                AND (4 = [Extent8].[FruitType])) AS [C8]
                            FROM ( SELECT
                                [Project6].[C1] AS [C1],
                                [Project6].[C2] AS [C2],
                                [Project6].[C3] AS [C3],
                                [Project6].[C4] AS [C4],
                                [Project6].[C5] AS [C5],
                                [Project6].[C6] AS [C6],
                                (SELECT
                                    COUNT(1) AS [A1]
                                    FROM [dbo].[Fruits] AS [Extent7]
                                    WHERE ([Extent7].[FruitID] IN (23, 215, 256, 643)) 
                                    AND ([Project6].[C1] = 0) 
                                    AND (3 = [Extent7].[FruitType])) AS [C7]
                                FROM ( SELECT
                                    [Project5].[C1] AS [C1],
                                    [Project5].[C2] AS [C2],
                                    [Project5].[C3] AS [C3],
                                    [Project5].[C4] AS [C4],
                                    [Project5].[C5] AS [C5],
                                    (SELECT
                                        COUNT(1) AS [A1]
                                        FROM [dbo].[Fruits] AS [Extent6]
                                        WHERE ([Extent6].[FruitID] IN (23, 215, 256, 643)) 
                                        AND ([Project5].[C1] = 0) 
                                        AND (3 = [Extent6].[FruitType])) AS [C6]
                                    FROM ( SELECT
                                        [Project4].[C1] AS [C1],
                                        [Project4].[C2] AS [C2],
                                        [Project4].[C3] AS [C3],
                                        [Project4].[C4] AS [C4],
                                        (SELECT
                                            COUNT(1) AS [A1]
                                            FROM [dbo].[Fruits] AS [Extent5]
                                            WHERE ([Extent5].[FruitID] IN (23, 215, 256, 643)) 
                                            AND ([Project4].[C1] = 0) 
                                            AND (2 = [Extent5].[FruitType])) AS [C5]
                                        FROM ( SELECT
                                            [Project3].[C1] AS [C1],
                                            [Project3].[C2] AS [C2],
                                            [Project3].[C3] AS [C3],
                                            (SELECT
                                                COUNT(1) AS [A1]
                                                FROM [dbo].[Fruits] AS [Extent4]
                                                WHERE ([Extent4].[FruitID] IN (23, 215, 256, 643)) 
                                                AND ([Project3].[C1] = 0) 
                                                AND (2 = [Extent4].[FruitType])) AS [C4]
                                            FROM ( SELECT
                                                [Project2].[C1] AS [C1],
                                                [Project2].[C2] AS [C2],
                                                (SELECT
                                                    COUNT(1) AS [A1]
                                                    FROM [dbo].[Fruits] AS [Extent3]
                                                    WHERE ([Extent3].[FruitID] IN (23, 215, 256, 643)) 
                                                    AND ([Project2].[C1] = 0) 
                                                    AND (1 = [Extent3].[FruitType])) AS [C3]
                                                FROM ( SELECT
                                                    [Distinct1].[C1] AS [C1],
                                                    (SELECT
                                                        COUNT(1) AS [A1]
                                                        FROM [dbo].[Fruits]AS [Extent2]
                                                        WHERE ([Extent2].[FruitID] IN (23, 215, 256, 643)) 
                                                        AND ([Distinct1].[C1] = 0) 
                                                        AND (1 = [Extent2].[FruitType])) AS [C2]
                                                    FROM ( SELECT DISTINCT
                                                        0 AS [C1]
                                                        FROM [dbo].[Fruits]AS [Extent1]
                                                        WHERE [Extent1].[FruitID] IN (23, 215, 256, 643)
                                                    )  AS [Distinct1]
                                                )  AS [Project2]
                                            )  AS [Project3]
                                        )  AS [Project4]
                                    )  AS [Project5]
                                )  AS [Project6]
                            )  AS [Project7]
                        )  AS [Project8]
                    )  AS [Project9]
                )  AS [Project10]
            )  AS [Project11]
        )  AS [Project12]
    )  AS [Project13]
)  AS [Limit1]

请注意,对于每个分组,IN 都会再次评估,从而为大量 ID 列表生成非常大的工作量。

你必须把工作分成两步。

List<int> theFruitIDs = new List<int> { 23, 215, 256, 643 };

var theCounter = (from f in MyDC.Fruits
                  where theFruitIDs.Contains(f.FruitID)
                  group f by f.FruitType into theFruits
                  select new { fruitType = theFruits.Key, fruitCount = theFruits.Count() })
                  .ToList();

这转化为更快的 SQL。注意最后的 ToList() 强制执行单个查询。

SELECT
[GroupBy1].[K1] AS [FruitType],
[GroupBy1].[A1] AS [C1]
FROM ( SELECT
    [Extent1].[FruitType] AS [K1],
    COUNT(1) AS [A1]
    FROM [dbo].[Fruits] AS [Extent1]
    WHERE [Extent1].[FruitID] IN (23, 215, 256, 643)
    GROUP BY [Extent1].[FruitType]
)  AS [GroupBy1]

现在您可以获取生成的列表并在内存中旋转它以获得您的 MyCounterMode。

var thePivot = new MyCounterMode
                {
                    CountType1 = theCounter.Where(x => x.fruitType == 1).Select(x => x.fruitCount).SingleOrDefault(),
                    CountType2 = theCounter.Where(x => x.fruitType == 2).Select(x => x.fruitCount).SingleOrDefault(),
                    CountType3 = theCounter.Where(x => x.fruitType == 3).Select(x => x.fruitCount).SingleOrDefault(),
                };

【讨论】:

  • 不,我需要将分组和计数器对象填充到数据库中而不是内存中
  • 我提出和解释的都是在db上完成的。列表“theCounter”只是一个包含六个元素的列表,第二组“thePivot”只是将这六行转换为六列。
  • 问题在于,在某些查询中,我使用相同的模式来填充计数器作为更大查询的子查询,因此我需要将计数器查询编写为一个独立的查询。
  • 我认为如果不使用“PIVOT”等不受支持的关键字,您想要在单个查询中执行是不可能的。我认为两次访问数据库比原来的实现要好得多(真的很多)。其他解决方案可以实现,但不能在 linq 中实现。
  • 您显示的SQL似乎是由Entity Framework生成的。这个问题被标记为 [linq-to-sql],它会生成一些不同的 SQL。但是,我相信您关于生成的 SQL 的整体结构的结论是正确的。
【解决方案3】:

您的 LINQ 会为每个计数生成单独的 sql,因此您需要使用 TheFruits 来计算您的项目

试试这个

var TheCounter = (from f in MyDC.Fruits    
                  where TheFruitIDs.Contains(f.FruitID) 
                  group new {f.FruitType} by f.FruitType into TheFruits
                  select new MyCounterMode()
                  {
                     CountType1 = TheFruits.Count(f => f.FruitType == 1),
                     CountType2 = TheFruits.Count(f => f.FruitType == 2),
                     .... all the way to CountType6      
                  }).Single();

【讨论】:

  • 这看起来不错,因为分组和计数是在数据库中完成的,而不是在内存中。
  • 此查询将生成 6 行,然后 Single 运算符将引发异常。
【解决方案4】:

您可以在内存中执行group by。将 group by 与多个计数相结合会产生大量子查询,这可能会表现得很糟糕。

var tempResult = (from f in MyDC.Fruits where TheFruitIDs.Contains(f.FruitID)).ToList();

var TheCounter = (from f in tempResult
                  group f by f.FruitType into TheFruits
                  select new MyCounterMode()
                  {
                     CountType1 = (int?) TheFruits.Count(f => f.FruitType == 1),
                     CountType2 = (int?) TheFruits.Count(f => f.FruitType == 2),
                     .... all the way to CountType6      
                  }).Single();

【讨论】:

  • 虽然这对于 LINQ-to-SQL 来说通常是正确的,但这里 group by 0 只进行 1 个组和一个查询。尽管如此,我也认为在内存中进行分组(透视)更好,因此数据库可以简单地专注于生成数据。
  • 不,我需要将分组和计数器对象填充到数据库中而不是内存中。
【解决方案5】:

您需要记住,每次迭代都会执行select!!

所以尝试类似:

'var TheCounter = (from f in MyDC.Fruits     
                  group f by f.FruitID into TheFruits
                  select new KeyValuePair<int, int>(TheFruits.Key,TheFruits.Count())).ToDictionary(r=>r.Key,r=>r.Value);'

这会给你一个字典:Key- FruitId, Value- Count

【讨论】:

    【解决方案6】:

    以下是我一直以来的实现方式(我构建了一个简单的控制台程序来演示):

    Fruit.cs

    public class Fruit
    {
        public Fruit(int fruitId, int fruitType)
        {
            FruitId = fruitId;
            FruitType = fruitType;
        }
    
        public int FruitId { get; set; }
        public int FruitType { get; set; }
    }
    

    Program.cs

    class Program
    {
        static void Main(string[] args)
        {
            // Data
            var fruits = new List<Fruit>
            {
                new Fruit(23, 2),
                new Fruit(215, 2),
                new Fruit(256, 1),
                new Fruit(643, 3)
            };
    
            // Query
            var query = fruits
                .GroupBy(x => x.FruitType)
                .Select(x => new {Name = x.Key, Total = x.Count()});
    
            // Output
            foreach (var item in query)
            {
                Console.WriteLine(item.Name + ": " + item.Total);
            }
            Console.ReadLine();
        }
    }
    

    你需要关注的是query。使用GroupBy 后,您将拥有一个群组列表。对于每个组,Key 是分组条件(这里是FruitType)。然后,我们调用Count() 来获取该组中元素的数量。

    【讨论】:

      【解决方案7】:

      这是一种不受 CountType # 限制的动态方法:

      int typesOfCounts = 6;
      
      IEnumerable<Fruit> theCounter = fruitList.Where(x => theFruitIDs.Contains(x.FruitID));
      
      Dictionary<string, int> myCounterMode = new Dictionary<string, int>();
      
      for (var i = 1; i < typesOfCounts + 1; i++)
      {
          string counterType = "CountTypeX";
          counterType = counterType.Replace("X", i.ToString());
      
          myCounterMode.Add(counterType, theCounter.Count(x => x.FruitType == i));
      }
      
      return myCounterMode;
      

      【讨论】:

        猜你喜欢
        • 2013-07-19
        • 2015-05-29
        • 1970-01-01
        • 2012-02-16
        • 1970-01-01
        • 2019-11-08
        • 2011-05-12
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多