【问题标题】:Joining GUID to String - Entity Framework Produces Slow Query将 GUID 加入字符串 - 实体框架产生慢查询
【发布时间】:2017-07-21 20:30:32
【问题描述】:

总结一下这个问题,我有一个实体框架根据我的 LINQ 查询生成的查询,该查询将两个表连接到一个值上,其中一个存储为 UNIQUEIDENTIFIER,另一个存储为(Nullable)VARCHAR。

由于两种不同的数据类型,我必须在 LINQ 查询中对 GUID 值调用 ToString()。

生成的 SQL 代码虽然令人钦佩,但最终速度很慢,因为它不只是比较两列(就像我在标准 SQL 查询中可能那样),它尝试转换一个表中的 UNIQUEIDENTIFIER 列并尝试赶上 Nulls。

如果我采用 Entity Framework 生成的查询并简单地删除它尝试进行转换的部分,则当有大约 300 条记录要返回时,查询将从 30 秒变为 0 秒。

所以,假设我坚持使用具有不同数据类型的表来存储相同的值,我想知道我是否可以告诉 Entity Framework 不要尝试在此列上进行转换而直接执行上比较。 (我们一直在查询和存储过程中这样做,它工作得很好而且速度很快。)

如果没有办法告诉实体框架,在这个特定的实例中,我是否只需要编写一个存储过程并调用它,而不是尝试在 LINQ 中编写查询?

这是为用户返回通知的方法。

var notifications = from un in _context.UserNotifications
                                join n in _context.Notifications on un.NotificationId equals n.Id
                                join r in _context.Referrals on n.txt_assess_seq_no equals r.seq_no.ToString()
                                where un.UserId == userId & !un.IsRead
                                orderby n.create_timestamp descending
                                select n;

这给了我想要的结果,但是由于 Notifications.txt_assess_seq_no(表中的 VARCHAR)和 Referrals.seq_no(UNIQUIDENTIFIER)的加入,Entity Framework 生成的 SQL 查询很慢。

这是生成的 SQL:

SELECT 
    [GroupBy1].[A1] AS [C1]
    FROM ( SELECT 
        COUNT(1) AS [A1]
        FROM   (SELECT [Extent1].[UserId] AS [UserId], [Extent2].[txt_assess_seq_no] AS [txt_assess_seq_no]
            FROM  [dbo].[UserNotifications] AS [Extent1]
            INNER JOIN [dbo].[ContactDetails] AS [Extent2] ON [Extent1].[NotificationId] = [Extent2].[seq_no]
            WHERE [Extent1].[IsRead] = 0 ) AS [Filter1]
        INNER JOIN [dbo].[Referrals] AS [Extent3] ON ([Filter1].[txt_assess_seq_no] = (LOWER( CAST( [Extent3].[seq_no] AS nvarchar(max))))) OR (([Filter1].[txt_assess_seq_no] IS NULL) AND (LOWER( CAST( [Extent3].[seq_no] AS nvarchar(max))) IS NULL))
        WHERE [Filter1].[UserId] = 'USER ID (GUID) AS STRING'
    )  AS [GroupBy1]

如果我只是改变这一行:

INNER JOIN [dbo].[Referrals] AS [Extent3] ON ([Filter1].[txt_assess_seq_no] = (LOWER( CAST( [Extent3].[seq_no] AS nvarchar(max))))) OR (([Filter1].[txt_assess_seq_no] IS NULL) AND (LOWER( CAST( [Extent3].[seq_no] AS nvarchar(max))) IS NULL))

到:

([Filter1].[txt_assess_seq_no] = [Extent3].[seq_no])

它超级快,正是我想要的,但我不太确定如何到达那里。我想保持用 LINQ 编写,但如果我只需要使用存储过程或一些老式 SQL 代码,我会这样做。

任何想法或建议将不胜感激!

【问题讨论】:

  • 你真正应该做的是修复表格。当您混合数据类型时,您正在为性能问题做好准备。
  • 我知道,但在这种情况下,由于您可能不喜欢听到的原因,这是不可能的。我确实提到假设这是不可能的,你有什么建议吗?不过,我非常感谢您的回答!
  • 嗯,发生的事情是 EF 意识到数据类型不同并进行了显式转换。您的代码只是在进行隐式转换。除了它创建的许多 sql 对性能来说很糟糕之外,我对 EF 知之甚少。
  • 你很幸运。隐式转换可能会导致一些严重的性能问题。但是 EF 生成的转换代码并没有那么糟糕。抱歉,我无法帮助您处理 EF 的问题。希望有人能过来帮忙。
  • 隐式转换与显式转换相反。 Uniqueidintifier 比字符串具有更高的优先级,因此如果没有明确的 guid 转换为字符串,您会得到相反的结果。

标签: sql-server asp.net-mvc entity-framework linq


【解决方案1】:

如前所述,SQL Server 将在连接中执行隐式转换,将 (N)VARCHAR 值转换为 UNIQUEIDENTIFIER 以进行比较。请注意,这与您的显式转换相反。在 EF 中无法使 LINQ 查询在此处生成具有隐式转换的查询。

您可以在 EF Core 项目中尝试,并建议一些方法来指定不同类型之间的隐式转换比较。

Anyhoo,您的性能问题可能与转换差异无关。尝试设置

db.Configuration.UseDatabaseNullSemantics = true;

这将简化生成查询中的 null 处理。

对于连接,您可以重写为过滤交叉连接(即非 ANSI 连接)来获得这种行为。例如

using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.Entity.SqlServer;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp6
{

    class Foo
    {
        public int Id   { get; set; }
        public string Name { get; set; }
        public Guid UID { get; set; }
    }

    class Bar
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string UID { get; set; }
    }
    class Db: DbContext
    {
        public DbSet<Foo> Foo { get; set; }
        public DbSet<Bar> Bar { get; set; }
    }
    class Program
    {

        static void Main(string[] args)
        {
            Database.SetInitializer(new DropCreateDatabaseAlways<Db>());

            using (var db = new Db())
            {
                db.Configuration.UseDatabaseNullSemantics = true;

                var q = from f in db.Foo
                        from b in db.Bar
                        where b.UID == f.UID.ToString()
                        select new { f, b };
                Console.WriteLine(q.ToString());


                Console.ReadKey();

            }
        }
    }
}

输出

SELECT
    [Extent1].[Id] AS [Id],
    [Extent1].[Name] AS [Name],
    [Extent1].[UID] AS [UID],
    [Extent2].[Id] AS [Id1],
    [Extent2].[Name] AS [Name1],
    [Extent2].[UID] AS [UID1]
    FROM  [dbo].[Foos] AS [Extent1]
    CROSS JOIN [dbo].[Bars] AS [Extent2]
    WHERE [Extent2].[UID] = (LOWER( CAST( [Extent1].[UID] AS nvarchar(max))))

【讨论】:

  • 这听起来很有希望。这会影响这个查询还是这个设置超出了这个查询的范围?
  • 您可以在 DbContext 构造函数中设置它以应用于所有查询,或者您可以在 DbContext 实例上设置它以仅应用于选定的查询。
  • @BillKron 请注意,该选项适用于比较运算符,但不适用于联接(David 的同事似乎忘记了它们:)。因此,除了将选项设置为 true 之外,请将 join r in _context.Referrals on n.txt_assess_seq_no equals r.seq_no.ToString() 替换为 from r in _context.Referrals where n.txt_assess_seq_no == r.seq_no.ToString()
  • 伊万,感谢您的额外帮助!我将在今晚晚些时候报告结果。
  • @DavidBrowne-Microsoft(还有 Ivan)- 非常感谢你们俩。这正是我想要的;一个 EF 配置设置,允许我在 LINQ 中保留现有查询,只需进行非常小的更改。伊万,我能够按照你的指示进行调整。大卫,因此,之前有问题的 WHERE 子句现在看起来像这样: WHERE ( [Filter1].[txt_assess_seq_no] = ( Lower(Cast([Extent3].[seq_no] AS NVARCHAR(max))) ) ) 完美!而且速度超级快。男人还能要求什么?谢谢你们,非常感谢你们的帮助!
【解决方案2】:

但是,您可以创建一个使用隐式转换的存储过程并从 EF 运行它。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-12-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多