使用EF Linq Select语句选择常量或函数。

3

我有一个Select语句,目前的格式如下:

dbEntity
.GroupBy(x => x.date)
.Select(groupedDate => new {
                             Calculation1 = doCalculation1 ? x.Sum(groupedDate.Column1) : 0),
                             Calculation2 = doCalculation2 ? x.Count(groupedDate) : 0)

在查询中,doCalculation1 和 doCalculation2 是之前设置的布尔值。这将在生成的 SQL 中创建一个 case 语句,类似于

DECLARE @p1 int = 1
DECLARE @p2 int = 0
DECLARE @p3 int = 1
DECLARE @p4 int = 0
SELECT (Case When @p1 = 1 THEN Sum(dbEntity.Column1)
     Else @p2
     End) as Calculation1,
     (Case When @p3 = 1 THEN Count(*)
     Else @p4
     End) as Calculation2

当doCalculation1为true时,我希望生成的SQL语句如下所示。
SELECT SUM(Column1) as Calculation1, Count(*)  as Calculation2

当 doCalculation2 为 false 时,就像这样。
SELECT 0 as Calculation1, Count(*) as Calculation2

有没有办法让EF强制执行查询并像这样工作?
编辑:
bool doCalculation = true;
bool doCalculation2 = false;
            dbEntity
            .Where(x => x.FundType == "E")
            .GroupBy(x => x.ReportDate)
              .Select(dateGroup => new 
              {
                  ReportDate = dateGroup.Key,
                  CountInFlows = doCalculation2 ? dateGroup.Count(x => x.Flow > 0) : 0,
                  NetAssetEnd = doCalculation ? dateGroup.Sum(x => x.AssetsEnd) : 0
              })
              .ToList();

生成这个SQL。
-- Region Parameters
DECLARE @p0 VarChar(1000) = 'E'
DECLARE @p1 Int = 0
DECLARE @p2 Decimal(5,4) = 0
DECLARE @p3 Int = 0
DECLARE @p4 Int = 1
DECLARE @p5 Decimal(1,0) = 0
-- EndRegion
SELECT [t1].[ReportDate], 
    (CASE 
        WHEN @p1 = 1 THEN (
            SELECT COUNT(*)
            FROM [dbEntity] AS [t2]
            WHERE ([t2].[Flow] > @p2) AND ([t1].[ReportDate] = [t2].[ReportDate]) AND ([t2].[FundType] = @p0)
        )
        ELSE @p3
     END) AS [CountInFlows], 
    (CASE 
        WHEN @p4 = 1 THEN CONVERT(Decimal(33,4),[t1].[value])
        ELSE CONVERT(Decimal(33,4),@p5)
     END) AS [NetAssetEnd]
FROM (
    SELECT SUM([t0].[AssetsEnd]) AS [value], [t0].[ReportDate]
    FROM [dbEntity] AS [t0]
    WHERE [t0].[FundType] = @p0
    GROUP BY [t0].[ReportDate]
    ) AS [t1]

执行计划中包含许多索引扫描、暂存器和连接操作。在测试集上平均运行时间约为20秒,而生产集将要更大。

我希望它能像SQL一样运行速度快。

select reportdate, 1, sum(AssetsEnd)
from vwDailyFundFlowDetail
where fundtype = 'E'
group by reportdate

平均运行时间约为12秒,并且大部分查询都在执行计划中的单个索引查找中绑定。实际的sql输出无关紧要,但是使用case语句性能似乎要差得多。
至于我为什么要这样做,我需要生成动态的select语句,就像我在Dynamically generate Linq Select中询问的那样。用户可以选择执行一项或多项计算,直到请求到达之前我不知道选择了哪些选项。这些请求很耗费资源,因此我们不想在没有必要的情况下运行它们。我根据用户请求设置doCalculation bools。
该查询旨在替换将从硬编码的SQL查询字符串插入或删除字符的某些代码,然后执行该字符串。那种方式运行得相当快,但维护起来非常麻烦。

1
你为什么想要改变SQL语句?你把它看作是一个性能问题吗?执行计划是否看起来不好? - Mant101
1
带有case语句的版本比纯选择语句的版本运行速度慢了约40%。带有case语句的执行计划具有大量索引扫描,而与Select 0版本获得的索引查找不同。我对执行计划的了解不足以确定其他发生的情况,但我将在一分钟内编辑我的问题,并附上它生成的精确sql。 - Alexander Burke
1个回答

2
技术上说,通过 表达式树访问器 可以将您的 Select 查询中的 Expression 传递,该访问器检查三元运算符左侧的常量值,并用相应的子表达式替换三元表达式。

例如:
public class Simplifier : ExpressionVisitor
{
    public static Expression<T> Simplify<T>(Expression<T> expr)
    {
        return (Expression<T>) new Simplifier().Visit(expr);
    }

    protected override Expression VisitConditional(ConditionalExpression node)
    {
        var test = Visit(node.Test);
        var ifTrue = Visit(node.IfTrue);
        var ifFalse = Visit(node.IfFalse);

        var testConst = test as ConstantExpression;
        if(testConst != null)
        {
            var value = (bool) testConst.Value;
            return value ? ifTrue : ifFalse;
        }

        return Expression.Condition(test, ifTrue, ifFalse);
    }

    protected override Expression VisitMember(MemberExpression node)
    {
        // Closed-over variables are represented as field accesses to fields on a constant object.
        var field = (node.Member as FieldInfo);
        var closure = (node.Expression as ConstantExpression);
        if(closure != null)
        {
            var value = field.GetValue(closure.Value);
            return VisitConstant(Expression.Constant(value));
        }
        return base.VisitMember(node);
    }
}

用法示例:
void Main()
{
    var b = true;
    Expression<Func<int, object>> expr = i => b ? i.ToString() : "N/A";
    Console.WriteLine(expr.ToString()); // i => IIF(value(UserQuery+<>c__DisplayClass0).b, i.ToString(), "N/A")
    Console.WriteLine(Simplifier.Simplify(expr).ToString()); // i => i.ToString()
    b = false;
    Console.WriteLine(Simplifier.Simplify(expr).ToString()); // i => "N/A"
}

所以,你可以在你的代码中像这样使用它:
Expression<Func<IGrouping<DateTime, MyEntity>>, ClassYouWantToReturn> select = 
    groupedDate => new {
        Calculation1 = doCalculation1 ? x.Sum(groupedDate.Column1) : 0),
        Calculation2 = doCalculation2 ? x.Count(groupedDate) : 0
    };
var q = dbEntity
    .GroupBy(x => x.date)
    .Select(Simplifier.Simplify(select))

然而,这可能比它的价值更麻烦。SQL Server几乎肯定会优化掉"1 == 1"的情况,允许Entity Framework生成不太美观的查询不应该成为性能问题。
更新
查看更新后的问题,这似乎是为数不多的几个实例之一,其中产生正确的查询确实很重要,从性能方面考虑。
除了我的建议解决方案外,还有其他几个选择:您可以使用原始SQL映射到返回类型,或者您可以使用LinqKit根据所需选择不同的表达式,然后在Select查询中"Invoke"该表达式。

我不确定我理解了。如果这比它值得的麻烦更多,你建议生成的 Sql 最终应该像第二种情况一样,而不是变成一个 case 语句? - Alexander Burke
@AlexanderBurke:我更新了答案,并提供了一个示例,展示如何编写表达式访问器来简化您的选择表达式,以便Entity Framework生成您要求的查询。正如您所看到的,这相当复杂,并且没有任何实际好处,因为从SQL Server的角度来看并没有真正的区别,而这些查询仅用于SQL Server消耗--它们不是为人类可读而设计的。因此,我建议您保留代码不变,不要费心尝试更改SQL输出。 - StriplingWarrior
谢谢您提供的示例。我不确定是否有做错什么,但是生成的 SQL Linq 语句似乎比只有 Select 1 的 SQL 运行速度慢了40-60%。我也查看了执行计划,发现使用 Case 语句的版本有很多小表扫描和各种连接占据了大部分查询。而只有 Select 1 的 SQL 版本则由一个表搜索占据了72%,哈希匹配和嵌套循环占据了27%。我将编辑我的问题,包括精确的 Linq 查询和它生成的 SQL,这样您就可以看到。 - Alexander Burke
我的dbentity是一个名为dailyFlow的表,我可以看到Select语句的类型。Resharper和Visual Studio说,变量select中赋值给groupedDate的类型是(paramater) ? groupedDate,这不是我见过的格式。然后它无法确定groupedDate应该具有哪些列。这就是我迷失的地方。 - Alexander Burke
我找到了打字问题的解决方法,只是我不太明白Func需要哪些类型。我已经得到了这个解决方案,基本上可以实现我的要求。我编写的纯SQL和EF生成的执行计划仍然略有不同,但大部分查询时间都花在了相同的部分。而且无论测试大小如何,它都只会在EF上多出10秒左右的开销,而不是慢40-60%。感谢您的帮助。 - Alexander Burke
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接