如何解决存储过程中的性能问题?

时间:2019-06-19 06:37:34

标签: sql sql-server tsql

我在存储过程上遇到了很大的麻烦。因为当我检查基准测试结果时,我意识到“ MatchxxxReferencesByIds”的平均LastElapsedTimeInSecond为“ 240.25”毫秒。您可以检查我的存储过程吗?我需要您的帮助来改善我的水平。

ALTER PROCEDURE [Common].[MatchxxxReferencesByIds]
    (@refxxxIds VARCHAR(MAX),
     @refxxxType NVARCHAR(250))
BEGIN
    SET NOCOUNT ON;

    BEGIN TRAN

    DECLARE @fake_tbl TABLE (xxxid NVARCHAR(50))

    INSERT INTO @fake_tbl  
        SELECT LTRIM(RTRIM(split.a.value('.', 'NVARCHAR(MAX)'))) AS fqdn   
        FROM 
            (SELECT 
                 CAST ('<M>' + REPLACE(@refxxxIds, ',', '</M><M>') + '</M>' AS XML) AS data
            ) AS a   
        CROSS APPLY 
            data.nodes ('/M') AS split(a)

    SELECT [p].[ReferencedxxxId]  
    FROM [Common].[xxxReference] AS [p] 
    WHERE ([p].[IsDeleted] = 0) 
      AND (([p].[ReferencedxxxType] COLLATE Turkish_CI_AS  = @refxxxType COLLATE Turkish_CI_AS ) 
      AND [p].[ReferencedxxxId] COLLATE Turkish_CI_AS  IN (SELECT ft.xxxid COLLATE Turkish_CI_AS FROM @fake_tbl ft))

    COMMIT;
END;

4 个答案:

答案 0 :(得分:4)

一个人只能在不知道表的架构,索引和数据大小的情况下进行假设。

硬编码归类可以防止使用在ReferencedEntityId列上的任何索引来防止查询优化器。字段名称和示例数据'423423,423423,423432,23423'仍建议这是一个数字列(int?bigint?)。不需要排序规则,变量的列类型应该与表的类型匹配。

最后,a.value可以直接返回intbigint,这意味着拆分查询可以重写为:

declare @refEntityIds nvarchar(max)='423423,423423,423432,23423';

DECLARE @fake_tbl TABLE (entityid bigint PRIMARY KEY, INDEX IX_TBL(Entityid))

INSERT INTO @fake_tbl  
SELECT split.a.value('.', 'bigint') AS fqdn   
FROM 
    (SELECT 
            CAST ('<M>' + REPLACE(@refEntityIds, ',', '</M><M>') + '</M>' AS XML) AS data
    ) AS a   
CROSS APPLY 
    data.nodes ('/M') AS split(a)

输入数据包含一些重复项,因此entityid不能是主键。

之后,查询可以更改为:

SELECT [p].[ReferencedEntityId]  
FROM [Common].[EntityReference] AS [p] 
WHERE [p].[IsDeleted] = 0
  AND [p].[ReferencedEntityType] COLLATE Turkish_CI_AS  = @refEntityType COLLATE Turkish_CI_AS 
  AND [p].[ReferencedEntityId]  IN (SELECT ft.entityid FROM @fake_tbl ft)

下一个问题是硬编码排序规则。除非它与列的实际排序规则匹配,否则它会阻止服务器使用覆盖该列的任何索引。如何解决此问题取决于实际的数据统计。也许列的排序规则必须更改,或者用ReferencedEntityId过滤后的行太少,以至于没有好处。

最后,IsDeleted无法建立索引。它是{/ {1}}列,其值为1/0,或者是另一个仍包含0/1的数字列。查询优化器不会使用在选择行方面如此糟糕的索引,因为它实际上更快来仅扫描其他条件返回的行。

一般规则是将最有选择性的索引列放在第一位。数据库将所有列组合在一起以创建一个“键”值,并从中构造一个B +树索引。密钥的选择性越高,需要扫描的索引节点越少。

bit仍可以在filtered index中用于仅索引删除的列。这使查询优化器可以消除搜索中不需要的列。结果索引也将更小,这意味着相同数量的IO操作将在内存中加载更多索引页并允许更快的查找。

所有这些都意味着IsDeleted应该具有这样的索引。

EntityReference

如果排序规则不匹配,则CREATE NONCLUSTERED INDEX IX_EntityReference_ReferenceEntityID ON Common.EntityReference (ReferenceEntityId, ReferenceEntityType) WHERE IsDeleted =0; 将不会用于搜索。如果这是最常见的情况,我们可以从索引中删除ReferenceEntityType并将其放在ReferenceEntityType子句中。尽管该字段仍可用于过滤而不必从实际表中加载数据,但它不会成为索引键的一部分:

INCLUDE

当然,如果是最常见的情况,则应改为更改列的排序规则

答案 1 :(得分:2)

  1. 我认为您不需要TRAN。您只是将逗号分隔的值“切碎”到@variable表中。并进行选择。 TRAN不需要这里。

  2. 尝试exists

SELECT [p].[ReferencedEntityId]  
    FROM [Common].[EntityReference] AS [p] 
    WHERE ([p].[IsDeleted] = 0) 
      AND (([p].[ReferencedEntityType] COLLATE Turkish_CI_AS  = @refEntityType COLLATE Turkish_CI_AS ) 
      AND EXISTS (SELECT 1 FROM @fake_tbl ft WHERE ft.entityid COLLATE Turkish_CI_AS =  [p].[ReferencedEntityId] COLLATE Turkish_CI_AS  )

3。

请参见https://www.sqlshack.com/efficient-creation-parsing-delimited-strings/

以不同方式解析定界字符串。

文章引用:

  

Microsoft的内置功能提供了方便的解决方案   并表现良好。它并不比XML快,但显然   的编写方式提供了易于优化的执行计划。   逻辑读取也更高。虽然我们看不到   涵盖并确切了解Microsoft如何实现此功能,我们   最不方便的功能是拆分字符串   SQL Server附带。请注意,分隔符已传入此   函数的大小必须为1。换句话说,您不能使用   带有多字符定界符的STRING_SPLIT,例如“”,“”。

  1. 发布执行计划的屏幕快照。如果您没有正确的索引(或者您有阻止使用索引的“提示”),那么您的查询将永远无法正常运行。

答案 2 :(得分:2)

根据存储过程的执行计划,使它执行缓慢的原因是您要使用XML的部分。

让我们重新思考解决方案:

我创建了一个这样的表:

CREATE TABLE [Common].[EntityReference]
(
    IsDeleted BIT,
    ReferencedEntityType VARCHAR(100),
    ReferencedEntityId VARCHAR(10)
);
GO

并这样操作(将1M条记录插入其中):

DECLARE @i INT = 1000000;
DECLARE @isDeleted BIT,
        @ReferencedEntityType VARCHAR(100),
        @ReferencedEntityId VARCHAR(10);
WHILE @i > 0
BEGIN
    SET @isDeleted =(SELECT @i % 2);
    SET @ReferencedEntityType = 'TEST' + CASE WHEN @i % 2 = 0 THEN '' ELSE CAST(@i % 2 AS VARCHAR(100)) END;
    SET @ReferencedEntityId = CAST(@i AS VARCHAR(10));
    INSERT INTO [Common].[EntityReference]
    (
        IsDeleted,
        ReferencedEntityType,
        ReferencedEntityId
    )
    VALUES (@isDeleted, @ReferencedEntityType, @ReferencedEntityId);

    SET @i = @i - 1;
END;

让我们分析您的代码:

您有一个用逗号分隔的输入(@refEntityIds),您想将其分割,然后针对这些值运行查询。 (您的PC中SP的子树成本大约为376)要这样做,您可以采用不同的方法:

1。将表变量传递给包含refEntityIds的存储过程

2。利用STRING_SPLIT函数分割字符串 让我们看一下示例查询:

INSERT INTO @fake_tbl
      SELECT value
        FROM STRING_SPLIT(@refEntityIds, ',');

使用此功能,您的代码将获得显着的性能提升。(子树成本:6.19,不包含以下索引)但是此功能在SQL Server 2008中不可用!

您可以使用此函数的替代项(请参见:https://stackoverflow.com/a/54926996/1666800),并将查询更改为此函数(子树成本仍然约为6.19):

INSERT INTO @fake_tbl
    SELECT value FROM dbo.[fn_split_string_to_column](@refEntityIds,',')

在这种情况下,您将再次看到显着的性能改进。

您还可以在[Common].[EntityReference]表上创建非聚集索引,该索引也有一点点的性能提升。但是请先考虑创建索引,然后再创建索引,这可能会对您的DML操作产生负面影响:

CREATE NONCLUSTERED INDEX [Index Name] ON [Common].[EntityReference]
(
    [IsDeleted] ASC
)
INCLUDE ([ReferencedEntityType],[ReferencedEntityId]) 

如果我没有此索引(假设我已用我的替换拆分解决方案),则子树成本为:6.19,当我添加上述索引时,子树成本降至4.70,最后当我更改索引时索引到下一个,子树代价是5.16

CREATE NONCLUSTERED INDEX [Index Name] ON [Common].[EntityReference]
(
    [ReferencedEntityType] ASC,
    [ReferencedEntityId] ASC
)
INCLUDE ([IsDeleted]) 

由于@PanagiotisKanavos,以下索引的性能甚至比上述索引更好(子树成本:3.95):

CREATE NONCLUSTERED INDEX IX_EntityReference_ReferenceEntityID  
    ON Common.EntityReference (ReferencedEntityId)  
    INCLUDE(ReferencedEntityType)
    WHERE IsDeleted =0; 

还请注意,对本地表变量使用事务几乎没有效果,可能您可以忽略它。

答案 3 :(得分:2)

如果[p]。[ReferencedEntityId]将为整数,则无需应用COLLATE子句。您可以直接应用IN条件。

  1. 您可以使用表值函数将简单的逗号分隔值转换为整数列表。有many samples。请将ID的数据类型保留为整数,以避免应用排序规则。
[p].[ReferencedEntityId] IN (SELECT ft.entityid AS FROM @fake_tbl ft))