SQL Server 性能和索引视图

Posted

技术标签:

【中文标题】SQL Server 性能和索引视图【英文标题】:SQL Server Performance and Indexed View 【发布时间】:2013-04-25 19:36:48 【问题描述】:

使用 SQL Server 2008。

(对不起,如果这是一篇文章,但我正在尝试提供尽可能多的信息。)

我有多个位置,每个位置都包含多个部门,每个部门都包含多个可以进行零到多次扫描的项目。每次扫描都与可能有也可能没有截止时间的特定操作相关。每个项目也属于一个特定的包,属于一个特定的工作,属于一个特定的项目,属于一个特定的客户。每个作业都包含一个或多个包,其中包含一个或多个项目。

                                        +=============+     +=============+
                                        |   Projects  | --> |   Clients   |
                                        +=============+     +=============+
                                              ^
                                              |
+=============+                         +=============+
|  Locations  |                         |     Jobs    |
+=============+                         +=============+
      ^                                       ^
      |                                       |
+=============+     +=============+     +=============+
| Departments | <-- |    Items    | --> |   Packages  |
+=============+     +=============+     +=============+
                          ^
                          |
                    +=============+     +=============+
                    |    Scans    | --> | Operations  |
                    +=============+     +=============+

items 表中大约有 24,000,000 条记录,scans 表中大约有 48,000,000 条记录。新项目全天偶尔批量插入到数据库中,通常一次插入数以万计。每小时都会批量插入新的扫描,从几百到几十万不等。

这些表被大量查询、切片和切块。我正在编写非常具体的存储过程,但它变成了维护的噩梦,因为我正处于一百个存储过程的边缘,并且在站点中没有尽头(例如,类似于 ScansGetDistinctCountByProjectIDByDepartmentIDGroupedByLocationID、ScansGetDistinctCountByPackageIDByDepartmentIDGroupedByLocationID 等) 幸运的是,要求几乎每天都更改(感觉如何),每次我必须更改/添加/删除一列时,嗯...我最终在酒吧。

所以我创建了一个索引视图和一些带有参数的通用存储过程来确定过滤和分组。不幸的是,性能下降了马桶。 我想第一个问题是,既然选择性能是最重要的,我是否应该坚持使用特定方法并努力应对基础表的更改?或者,可以做些什么来加快索引视图/通用查询方法? 除了减轻维护噩梦之外,我实际上希望索引视图也能提高性能。

这是生成视图的代码:

CREATE VIEW [ItemScans] WITH SCHEMABINDING AS

SELECT
    p.ClientID          
    , p.ID        AS [ProjectID]            
    , j.ID        AS [JobID]
    , pkg.ID      AS [PackageID]
    , i.ID        AS [ItemID]       
    , s.ID        AS [ScanID]
    , s.DateTime
    , o.Code
    , o.Cutoff
    , d.ID        AS [DepartmentID]
    , d.LocationID
    -- other columns
FROM
    [Projects] AS p
    INNER JOIN [Jobs] AS j
        ON p.ID = j.ProjectID
    INNER JOIN [Packages] AS pkg
        ON j.ID = pkg.JobID
    INNER JOIN [Items] AS i
        ON pkg.ID = i.PackageID
    INNER JOIN [Scans] AS s
        ON i.ID = s.ItemID
    INNER JOIN [Operations] AS o
        ON s.OperationID = o.ID
    INNER JOIN [Departments] AS d
        ON i.DepartmentID = d.ID;   

和聚集索引:

CREATE UNIQUE CLUSTERED INDEX [IDX_ItemScans] ON [ItemScans]
(
    [PackageID] ASC,
    [ItemID] ASC,
    [ScanID] ASC
)

这是通用存储过程之一。它获取已扫描并具有截止值的项目的计数:

PROCEDURE [ItemsGetFinalizedCount] 
    @FilterBy       int = NULL
    , @ID           int = NULL
    , @FilterBy2    int = NULL 
    , @ID2          sql_variant = NULL  
    , @GroupBy      int = NULL        
WITH RECOMPILE
AS
BEGIN

    SELECT
        CASE @GroupBy           
            WHEN 1 THEN
                CONVERT(sql_variant, LocationID)
            WHEN 2 THEN
                CONVERT(sql_variant, DepartmentID)
            -- other cases
       END AS [ID]
       , COUNT(DISTINCT ItemID) AS [COUNT]
    FROM
        [ItemScans] WITH (NOEXPAND)
    WHERE       
        (@ID IS NULL OR
        @ID = CASE @FilterBy            
            WHEN 1 THEN         
                ClientID
            WHEN 2 THEN
                ProjectID
            -- other cases
        END) 
        AND (@ID2 IS NULL OR
        @ID2 = CASE @FilterBy2          
            WHEN 1 THEN         
                CONVERT(sql_variant, ClientID)
            WHEN 2 THEN
                CONVERT(sql_variant, ProjectID)
            -- other cases
        END)
        AND Cutoff IS NOT NULL
    GROUP BY
        CASE @GroupBy           
            WHEN 1 THEN
                CONVERT(sql_variant, LocationID) 
            WHEN 2 THEN
                CONVERT(sql_variant, DepartmentID)
            -- other cases
        END
END

第一次运行查询并查看实际执行计划时,我创建了它建议的缺失索引:

CREATE NONCLUSTERED INDEX [IX_ItemScans_Counts] ON [ItemScans]
(
    [Cutoff] ASC
)
INCLUDE ([ClientID],[ProjectID],[JobID],[ItemID],[SegmentID],[DepartmentID],[LocationID]) 

创建索引将执行时间缩短到大约 5 秒,但这仍然是不可接受的(查询的“特定”版本运行亚秒级。)我尝试将不同的列添加到索引中,而不是只包含它们而不包含性能提升(我现在不知道自己在做什么,这并没有真正的帮助。)

这是查询计划:

这里是第一次索引查找的详细信息(它似乎返回了视图中 Cutoff IS NOT NULL 的所有行):

【问题讨论】:

【参考方案1】:

在这种情况下,通用 proc 可能不是一个坏主意,但您不必像当前那样将所有这些情况都放入最终查询中。我会尝试在您的通用 proc 中使用动态 SQL 构建您的“特定查询”,就像 Gail Shaw 在此处构建“包罗万象”查询一样:

SQL in the Wild - Catch-all queries

这样,您可以缓存查询计划并利用博文中所示的索引,并且您应该能够获得与您所追求的相同的亚秒级性能。

【讨论】:

感谢您的快速回复。我不得不承认,我什至从未考虑过动态 SQL。我知道它有时间和地点,但我仍然有“动态 SQL 总是邪恶的”在我脑海中回响。似乎无法动摇。 sp_executesql 带参数提供了 procs 的大部分性能,并且可以避免exec (@sql) 的大部分恐怖。我认为“必须......使用......PROCS!”的日子大部分都落后于我们,但不幸的是,我仍然在许多数据库中维护着数百个集合。 你会喜欢这个的......如果你看看我的存储过程,我在声明参数后立即使用 WITH RECOMPILE。如果我删除它并在存储过程的末尾添加 OPTION (RECOMPILE),它就会飞起来。我不够聪明,无法知道其中的区别,但我很高兴您发布了链接,让我访问了 Erland Sommarskog 的网站,在那里我注意到了不同之处。当人们指出我正确的方向而不是告诉我如何做某事时,我更喜欢它的另一个原因。再次感谢。 可选参数,例如: WHERE(@ID2 IS NULL OR ID =@ID2) 阻止 SQL Server 使用 ID 上的任何索引,除非在 SELECT 查询的末尾使用了 OPTION RECOMPILE。我不知道,但是是的,我也注意到在存储过程标头中使用 WITH RECOMPILE 也不能正常工作。

以上是关于SQL Server 性能和索引视图的主要内容,如果未能解决你的问题,请参考以下文章

SQL Server 索引优化——无用索引

关于视图和索引 (SQL Server 2008 R2)

在 SQL Server 中的架构绑定视图上创建索引

SQL Server 查询性能优化——创建索引原则

SQL Server-聚焦过滤索引提高查询性能

SQL Server 中的视图