为啥 LINQ 在我的查询中使用错误的数据类型,而它在 EF 架构中被正确声明?

Posted

技术标签:

【中文标题】为啥 LINQ 在我的查询中使用错误的数据类型,而它在 EF 架构中被正确声明?【英文标题】:why does LINQ use the wrong datatype in my query while it's declared correctly in the EF schema?为什么 LINQ 在我的查询中使用错误的数据类型,而它在 EF 架构中被正确声明? 【发布时间】:2020-06-05 12:18:28 【问题描述】:

我的数据库架构默认使用 varchar。使用 EF(6) 代码优先方法,我通过将字符串的 ColumnType 设置为 varchar 来确保我的模型是正确的:modelBuilder.Properties<string>().Configure(p => p.HasColumnType("varchar"));

我正在使用 PredicateBuilder 来构建我的 where 子句,并且一切都按预期工作; LINQ 使用 varchar 数据类型创建参数化查询。我也尝试过不使用 PredicateBuilder:出现完全相同的问题。

但是,一旦我添加了一个 Select 语句,LINQ 突然决定将数据类型更改为 nvarchar,而我想不出任何理由。这当然会对我的查询产生严重的负面影响,因为 sql server 现在必须进行大量隐式转换,从而使我的索引无用。它现在是扫描表而不是搜索。

var ciPredicate = PredicateBuilder.New<InfoEntity>(true);
ciPredicate = ciPredicate.And(x => x.InfoCode == ciCode);
ciPredicate = ciPredicate.And(x => x.Source == source);

//varchar - N'@p__linq__0 varchar(8000),@p__linq__1 varchar(8000)'
var ciQuery2 = this.Scope.Set<InfoEntity>().Where(ciPredicate).ToList();

//varchar - N'@p__linq__0 varchar(8000),@p__linq__1 varchar(8000)'
var ciQuery3 = this.Scope.Set<InfoEntity>().Where(ciPredicate).GroupBy(x => new  x.Source, x.InfoKey ).ToList();

//varchar - N'@p__linq__0 varchar(8000),@p__linq__1 varchar(8000)'
var ciQuery4 = this.Scope.Set<InfoEntity>().Where(ciPredicate).GroupBy(x => new  x.Source, x.InfoKey ).ToList().Select(group => group
                    .OrderByDescending(x => x.InfoSeqNr)
                    .FirstOrDefault()
                );

//nvarchar - N'@p__linq__0 nvarchar(4000),@p__linq__1 nvarchar(4000)'
var ciQueryNvarchar = this.Scope.Set<InfoEntity>().Where(ciPredicate).GroupBy(x => new  x.Source, x.InfoKey )
                .Select(group => group
                    .OrderByDescending(x => x.InfoSeqNr)
                    .FirstOrDefault()
                ).ToList();

表定义:

CREATE TABLE Info(
  Id int NOT NULL,
  InfoKey int NOT NULL,
  Source varchar(50) NOT NULL,
  InfoCode varchar(50) NOT NULL,
  InfoDesc varchar(4000) NOT NULL,
  InfoSeqNr int NOT NULL
)

由于这只是一个查询的开始,我们不能在中间使用 ciQuery4 和 ToList()。

我一生都无法弄清楚为什么会发生这种情况,我们将不胜感激。

【问题讨论】:

【参考方案1】:

这似乎是 EF 中的一个错误......或者真的是这样吗?

我能够在 EF6.4.4 中通过 GroupBy 子句中的 select 语句重现这一点。单列GroupBy 和复合列GroupBy 似乎没有区别。

但是当我把Select-clause 改成这样的时候

var ciQueryNvarchar = this.Scope.Set<InfoEntity>().Where(ciPredicate).GroupBy(x => new  x.Source, x.InfoKey )
                .Select(group => new 
                    Group = group.Key,
                    MostRecentItem = group.OrderByDescending(x => x.InfoSeqNr).FirstOrDefault()
                ).ToList();

EF 在生成的 SQL 语句中不再使用 nvarchar(4000)。所以这可能是一种解决方法,但它并不漂亮......

更好的选择是完全重写查询,因为 LINQ GroupBy 和 SQL GROUP BY are different things。 LINQ 的 GroupBy 是某种排序 + 分桶操作,而 SQL 的 GROUP BY 还结合了聚合函数,我们在这种情况下没有。

因此,上面的查询可以重写为

var ciQueryNvarchar = this.Scope.Set<InfoEntity>()
    .Where(ciPredicate)
    .Select(x => new  x.Source, x.InfoKey )
    .Distinct()
    .Select(x => this.Scope.Set<InfoEntity>()
        .Where(ciPredicate)
        .Where(y => y.Source == x.Source && y.InfoKey == x.InfoKey)
        .OrderByDescending(y => y.InfoSeqNr)
        .FirstOrDefault()
    )
    .ToList();

比较查询计划时,初始查询和重写查询生成的 SQL 几乎相同,但有趣的部分在于可以解释类型更改为 nvarchar 的差异。

在 .NET 中,所有字符串都是 Unicode (nvarchar)。因此,如果生成的查询包含 SELECT @p__linq__0 AS [p__linq__0], @p__linq__1 AS [p__linq__1],EF 的最佳猜测是使用 nvarchar 来匹配该列以匹配 .NET 的 unicode 字符串。

所以它可能毕竟不是一个错误?

另一种选择可能是使用纯 SQL(这也将使查询更具可读性),但这也可能不理想(鉴于重命名重构和数据库架构更改)。

【讨论】:

以上是关于为啥 LINQ 在我的查询中使用错误的数据类型,而它在 EF 架构中被正确声明?的主要内容,如果未能解决你的问题,请参考以下文章

在 C# 中使用 LINQ 方法语法 (LINQ to SQL) 时发生异常

使用具有不同层的 LINQ 意味着我无法访问特定类型

具有匿名类型和用户定义类型的 LINQ 选择查询

LINQ 查询到 LINQ 方法;为啥我需要 CBool​​?

在 linq 查询中使用拆分

Linq - 从 SQL Server 中的 nvarchar 类型列中查找最大值