使用SQL Server 2008。
我有多个位置,每个位置包含多个部门,每个部门又包含多个物品,这些物品可以有零到多个扫描记录。每个扫描与特定的操作相关联,该操作可能具有截止时间,也可能没有。每个物品还属于一个特定的包裹,该包裹属于一个特定的作业,该作业属于一个特定的项目,而该项目则属于一个特定的客户。每个作业包含一个或多个包裹,每个包裹包含一个或多个物品。
items表中大约有2400万条记录,scans表中大约有4800万条记录。每天都会不时批量插入新项目,通常一次插入数以万计。新扫描数据每小时批量插入,每个扫描项目的数量从几百到数十万不等。
这些表被频繁查询,并按各种方式进行切片和分组。我曾编写非常具体的存储过程,但随着时间推移,这变成了维护噩梦,因为我快要写出一百个存储过程,而且看不到尽头(例如类似于ScansGetDistinctCountByProjectIDByDepartmentIDGroupedByLocationID、ScansGetDistinctCountByPackageIDByDepartmentIDGroupedByLocationID等)。不幸的是,需求(感觉上)几乎每天都在变化,每次我必须更改/添加/删除列时,我就会束手无策。
因此,我创建了一个索引视图和一些带参数的通用存储过程来确定过滤和分组。不幸的是,性能下降得厉害。我想问的第一个问题是,由于选择性能至关重要,我是否应该坚持具体的方法并处理底层表的更改?还是说,可以采取措施加快索引视图/通用查询方法的速度?除了减轻维护噩梦的压力外,我实际上还希望索引视图也能提高性能。
以下是生成视图的代码:
创建索引将执行时间缩短到约5秒,但仍然不可接受(“特定”版本的查询运行在子秒级)。我尝试添加不同的列到索引中,而不是只是将它们包含其中,但没有性能上的提升(此时我真的不知道该怎么办了)。
以下是查询计划:
以下是第一个索引查找的详细信息(它似乎返回视图中所有Cutoff IS NOT NULL的行):
我有多个位置,每个位置包含多个部门,每个部门又包含多个物品,这些物品可以有零到多个扫描记录。每个扫描与特定的操作相关联,该操作可能具有截止时间,也可能没有。每个物品还属于一个特定的包裹,该包裹属于一个特定的作业,该作业属于一个特定的项目,而该项目则属于一个特定的客户。每个作业包含一个或多个包裹,每个包裹包含一个或多个物品。
+=============+ +=============+
| Projects | --> | Clients |
+=============+ +=============+
^
|
+=============+ +=============+
| Locations | | Jobs |
+=============+ +=============+
^ ^
| |
+=============+ +=============+ +=============+
| Departments | <-- | Items | --> | Packages |
+=============+ +=============+ +=============+
^
|
+=============+ +=============+
| Scans | --> | Operations |
+=============+ +=============+
items表中大约有2400万条记录,scans表中大约有4800万条记录。每天都会不时批量插入新项目,通常一次插入数以万计。新扫描数据每小时批量插入,每个扫描项目的数量从几百到数十万不等。
这些表被频繁查询,并按各种方式进行切片和分组。我曾编写非常具体的存储过程,但随着时间推移,这变成了维护噩梦,因为我快要写出一百个存储过程,而且看不到尽头(例如类似于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秒,但仍然不可接受(“特定”版本的查询运行在子秒级)。我尝试添加不同的列到索引中,而不是只是将它们包含其中,但没有性能上的提升(此时我真的不知道该怎么办了)。
以下是查询计划:
![queryplan](https://istack.dev59.com/McscZ.webp)
![operation](https://istack.dev59.com/QXG2V.webp)
sp_executesql
可以提供与存储过程相当的性能,并且可以避免大部分exec (@sql)
带来的问题。我认为“必须使用存储过程”的时代已经基本过去了,尽管我仍然在许多数据库中维护着数百个存储过程的集合,这是不幸的。 - Tim Lehner