分页时出现奇怪的T-SQL / LINQ性能问题

时间:2013-11-29 12:53:11

标签: linq entity-framework tsql stored-procedures sql-server-2012

我为客户创建了一个应用程序(ASP.net MVC 4)。 此应用程序使用实体框架查询数据库(SQL Server 2012)。

客户要求我为该应用程序创建一个新报告。 此报告将显示其数据库中的所有产品,并显示已售出的数量(TotalOut)以及购买和库存的数量(TotalIn)。 因为此报告需要计算我选择在DB上创建功能的值。

为了创建这个功能,我使用了3个表。

  • 产品 - >表格包含公司销售的所有产品
  • 存储 - >表格包含产品的所有不同库存。 存储有1个产品。产品有0个或多个存储空间。
  • StorageHistory - >表与存储历史。销售或购买产品时,会在此表中添加记录。 StorageHistory有1个存储空间。存储有0个或多个StorageHistories。

所以我创建了我的函数并将其添加到我的EF DB上下文模型(edmx)中。 使用LINQ我向此函数添加额外的查询。

离。用户只能查询特定制造商的产品。

产品表包含几乎 15k记录。我添加了分页,因此用户一次只能看到15条记录。但分页导致了很多性能问题

默认情况下,我按照其ID订购产品。 Id是主键,类型为String。此 ID 是productCode,始终采用以下格式(ABC123456)

问题:

当我按Id(ASC或DESC)排序时,查询需要13秒才能执行。但是当我将Order By更改为任何其他列(例如Name)时,查询执行的时间不到一秒。

我已经检查了我的LINQ to EF代码,以确保在数据库级别查询所有内容。 这些是使用Skip()和Take()时生成的两个SQL查询。当直接在DB上对此进行排除时,我得到相同的结果。

订购ID时 - > 12秒执行时间

SELECT TOP (15) 
[Project1].[C1] AS [C1], 
[Project1].[Id] AS [Id], 
[Project1].[Name] AS [Name], 
[Project1].[SellingPrice] AS [SellingPrice], 
[Project1].[TotalIn] AS [TotalIn], 
[Project1].[TotalOut] AS [TotalOut]
FROM ( SELECT [Project1].[Id] AS [Id], [Project1].[Name] AS [Name], [Project1].[SellingPrice] AS [SellingPrice], [Project1].[TotalIn] AS [TotalIn], [Project1].[TotalOut] AS [TotalOut], [Project1].[C1] AS [C1], row_number() OVER (ORDER BY [Project1].[Id] ASC) AS [row_number]
    FROM ( SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[SellingPrice] AS [SellingPrice], 
        [Extent1].[TotalIn] AS [TotalIn], 
        [Extent1].[TotalOut] AS [TotalOut], 
        1 AS [C1]
        FROM [dbo].[GetProductStatistics](NULL, NULL) AS [Extent1]
    )  AS [Project1]
)  AS [Project1]
WHERE [Project1].[row_number] > 14200
ORDER BY [Project1].[Id] ASC

在名称上订购 - >执行时间不到1秒

SELECT TOP (15) 
[Project1].[C1] AS [C1], 
[Project1].[Id] AS [Id], 
[Project1].[Name] AS [Name], 
[Project1].[SellingPrice] AS [SellingPrice], 
[Project1].[TotalIn] AS [TotalIn], 
[Project1].[TotalOut] AS [TotalOut]
FROM ( SELECT [Project1].[Id] AS [Id], [Project1].[Name] AS [Name], [Project1].[SellingPrice] AS [SellingPrice], [Project1].[TotalIn] AS [TotalIn], [Project1].[TotalOut] AS [TotalOut], [Project1].[C1] AS [C1], row_number() OVER (ORDER BY [Project1].[Name] ASC) AS [row_number]
    FROM ( SELECT 
        [Extent1].[Id] AS [Id], 
        [Extent1].[Name] AS [Name], 
        [Extent1].[SellingPrice] AS [SellingPrice], 
        [Extent1].[TotalIn] AS [TotalIn], 
        [Extent1].[TotalOut] AS [TotalOut], 
        1 AS [C1]
        FROM [dbo].[GetProductStatistics](NULL, NULL) AS [Extent1]
    )  AS [Project1]
)  AS [Project1]
WHERE [Project1].[row_number] > 14200
ORDER BY [Project1].[Name] ASC

有人能想到为什么第一个查询速度慢得多的原因吗? 如何解决这种奇怪的行为?


修改

这是我创建的功能

CREATE FUNCTION GetProductStatistics 
(   
    @StartDate DateTime = null,
    @EndDate DateTime = null
)
RETURNS TABLE 
AS
RETURN 
(
    SELECT        
        p.Id, 
        p.Name,
        p.ExternalId,
        p.Manufacturer,
        p.SellingPrice,
        SUM(CASE WHEN h.Increase = 1 AND h.Type IN ('Create', 'Refill') AND (@StartDate IS NULL OR @EndDate IS NULL OR (h.UpdatedOn BETWEEN @StartDate AND @EndDate)) THEN h.Amount ELSE 0 END) * p.SellingPrice AS TotalIn, 
        SUM(CASE WHEN h.Increase = 0 AND h.Type IN ('Used') AND (@StartDate IS NULL OR @EndDate IS NULL OR (h.UpdatedOn BETWEEN @StartDate AND @EndDate)) THEN h.Amount ELSE 0 END) * p.SellingPrice AS TotalOut
    FROM
        dbo.Products AS p 
        LEFT OUTER JOIN dbo.Storage AS s ON p.Id = s.ProductId 
        LEFT OUTER JOIN dbo.StorageHistory AS h ON s.Id = h.StorageId
    WHERE p.IsArchived = 0
    GROUP BY 
        p.Id, 
        p.Name,
        p.ExternalId,
        p.Manufacturer,
        p.SellingPrice
)
GO

EDIT2:

这是彼此相邻的2个执行计划的图片。 @Yosi他们完全不同......

(点击查看大图)
Picture

当我更改LINQ To EF生成的查询的第8行的Order By时,查询运行时间不到1秒。

FROM ( SELECT [Project1].[Id] AS [Id], [Project1].[Name] AS [Name], [Project1].[SellingPrice] AS [SellingPrice], [Project1].[TotalIn] AS [TotalIn], [Project1].[TotalOut] AS [TotalOut], [Project1].[C1] AS [C1], row_number() OVER (ORDER BY [Project1].[Id] ASC) AS [row_number]

FROM ( SELECT [Project1].[Id] AS [Id], [Project1].[Name] AS [Name], [Project1].[SellingPrice] AS [SellingPrice], [Project1].[TotalIn] AS [TotalIn], [Project1].[TotalOut] AS [TotalOut], [Project1].[C1] AS [C1], row_number() OVER (ORDER BY [Project1].[NAME] ASC) AS [row_number]

EDIT3:

@GertArnold这里是索引(产品ID)的一般设置的屏幕截图。 在我的所有表中,我只有默认的聚簇主键索引... Product ID index

1 个答案:

答案 0 :(得分:1)

我的一位同事找到了解决方案。 因为该函数返回一个新表而不是原始产品表,所以没有定义索引。他告诉我先将结果插入表变量中,其中Id被定义为主键(索引)。 Id是特定模式的varchar,因此不易排序,因为它并不是真正独特的。

所以我将自定义功能更改为以下内容:

CREATE FUNCTION [dbo].[GetProductStatistics] 
(   
    @StartDate DateTime = null,
    @EndDate DateTime = null
)
RETURNS @temTable TABLE (
        Id       VARCHAR(50) NOT NULL, 
        Name        VARCHAR(50) NOT NULL, 
        ExternalId    VARCHAR(50) NOT NULL, 
        Manufacturer    VARCHAR(50) NOT NULL, 
        SellingPrice  numeric(18, 2) NOT NULL, 
        TotalIn  numeric(18, 2) NOT NULL, 
        TotalOut  numeric(18, 2) NOT NULL, 
        PRIMARY KEY (Id)
    )
AS
BEGIN 
    INSERT INTO @temTable
        SELECT        
            p.Id, 
            p.Name,
            p.ExternalId,
            p.Manufacturer,
            p.SellingPrice,
            SUM(CASE WHEN h.Increase = 1 AND h.Type IN ('Create', 'Refill') AND (@StartDate IS NULL OR @EndDate IS NULL OR (h.UpdatedOn BETWEEN @StartDate AND @EndDate)) THEN h.Amount ELSE 0 END) * p.SellingPrice AS TotalIn, 
            SUM(CASE WHEN h.Increase = 0 AND h.Type IN ('Used') AND (@StartDate IS NULL OR @EndDate IS NULL OR (h.UpdatedOn BETWEEN @StartDate AND @EndDate)) THEN h.Amount ELSE 0 END) * p.SellingPrice AS TotalOut
        FROM
            dbo.Products AS p 
            LEFT OUTER JOIN dbo.Storage AS s ON p.Id = s.ProductId 
            LEFT OUTER JOIN dbo.StorageHistory AS h ON s.Id = h.StorageId
        WHERE p.IsArchived = 0
        GROUP BY 
            p.Id, 
            p.Name,
            p.ExternalId,
            p.Manufacturer,
            p.SellingPrice;
    Return;
END

我必须说我对结果感到非常惊讶。无论我如何排序,查询都会在不到一秒的时间内执行。