SQL Server在存储过程中不使用索引。

11

虽然我尚未通过使用存储过程解决此问题,但我们已决定跳过存储过程,直接执行普通的SQL语句。

请参阅下面的扩展表方案
编辑2:更新索引(不再使用actieGroep)
NB. SQL Server 2005 Enterprise 9.00.4035.00
NB2. 看起来与http://www.sqlservercentral.com/Forums/Topic781451-338-1.aspx有关

我在一个表上有两个索引:

  • statistiekId上的聚集PK索引
  • foreignId上的非聚集索引

我有以下代码:

DECLARE @fid BIGINT
SET @fid = 873926

SELECT foreignId
FROM STAT_Statistieken
WHERE foreignId = @fid

这段代码执行的很好,它指向了正确的索引,且它所做的只是扫描该索引。

现在我正在创建一个存储过程:

ALTER PROCEDURE MyProcedure (@fid BIGINT)
AS BEGIN
    SELECT foreignId
    FROM STAT_Statistieken
    WHERE foreignId = @fid
END

运行该事物:

EXEC MyProcedure @fid = 873926

现在它正在对我的主键索引运行聚集索引扫描!到底发生了什么?

因此,我将SP更改为

SELECT foreignId
FROM STAT_Statistieken
    WITH (INDEX(IX_STAT_Statistieken_2))
WHERE foreignId = @fid

现在它会给出以下错误信息:查询处理器无法生成查询计划,因为此查询中定义了提示。请重新提交查询,不要指定任何提示并且不要使用SET FORCEPLAN。当直接执行此函数时,相同的函数应该正常运行。


额外信息:可以复现此行为的完整方案(注释中的英文名称)

CREATE TABLE [dbo].[STAT_Statistieken](
    [statistiekId] [bigint] IDENTITY(1,1) NOT NULL,
    [foreignId] [bigint] NOT NULL,
    [datum] [datetime] NOT NULL, --date
    [websiteId] [int] NOT NULL,
    [actieId] [int] NOT NULL, --actionId
    [objectSoortId] [int] NOT NULL, --kindOfObjectId
    [aantal] [bigint] NOT NULL, --count
    [secondaryId] [int] NOT NULL DEFAULT ((0)),
    [dagnummer]  AS (datediff(day,CONVERT([datetime],'2009-01-01 00:00:00.000',(121)),[datum])) PERSISTED, --daynumber
    [actieGroep]  AS (substring(CONVERT([varchar](4),[actieId],0),(1),(1))) PERSISTED,
    CONSTRAINT [STAT_Statistieken_PK] PRIMARY KEY CLUSTERED --actionGroup
    (
        [statistiekId] ASC
    )WITH (PAD_INDEX  = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
    ) ON [PRIMARY]

索引

CREATE NONCLUSTERED INDEX [IX_STAT_Statistieken_foreignId_dagnummer_actieId_secondaryId] ON [dbo].[STAT_Statistieken] 
(
    [foreignId] ASC,
    [dagnummer] ASC,
    [actieId] ASC,
    [secondaryId] ASC
)WITH (PAD_INDEX  = ON, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, IGNORE_DUP_KEY = OFF, FILLFACTOR = 80, ONLINE = OFF) ON [PRIMARY]

执行

SET NOCOUNT ON;

    DECLARE @maand INT, @jaar INT, @foreignId BIGINT
    SET @maand = 9
    SET @jaar = 2009
    SET @foreignId = 828319


DECLARE @startDate datetime, @endDate datetime
SET @startDate = DATEADD(month, -1, CONVERT(datetime,CAST(@maand AS varchar(3))+'-01-'+CAST(@jaar AS varchar(5))))
SET @endDate = DATEADD(month, 1, CONVERT(datetime,CAST(@maand AS varchar(3))+'-01-'+CAST(@jaar AS varchar(5))))

DECLARE @firstDayDezeMaand datetime
SET @firstDayDezeMaand = CONVERT(datetime, CAST(@jaar AS VARCHAR(4)) + '/' + CAST(@maand AS VARCHAR(2)) + '/1')

DECLARE @daynumberFirst int
set @daynumberFirst = DATEDIFF(day, '2009/01/01', @firstDayDezeMaand)

DECLARE @startDiff int
SET @startDiff = DATEDIFF(day, '2009/01/01', @startDate)

DECLARE @endDiff int
SET @endDiff = DATEDIFF(day, '2009/01/01', @endDate)

SELECT @foreignId AS foreignId,
    SUM(CASE WHEN dagnummer >= @daynumberFirst THEN (CASE WHEN actieId BETWEEN 100 AND 199 THEN aantal ELSE 0 END) ELSE 0 END) as aantalGevonden, 
    SUM(CASE WHEN dagnummer >= @daynumberFirst THEN (CASE WHEN actieId BETWEEN 200 AND 299 THEN aantal ELSE 0 END) ELSE 0 END) as aantalBekeken, 
    SUM(CASE WHEN dagnummer >= @daynumberFirst THEN (CASE WHEN actieId BETWEEN 300 AND 399 THEN aantal ELSE 0 END) ELSE 0 END) as aantalContact,
    SUM(CASE WHEN dagnummer < @daynumberFirst THEN (CASE WHEN actieId BETWEEN 100 AND 199 THEN aantal ELSE 0 END) ELSE 0 END) as aantalGevondenVorige, 
    SUM(CASE WHEN dagnummer < @daynumberFirst THEN (CASE WHEN actieId BETWEEN 200 AND 299 THEN aantal ELSE 0 END) ELSE 0 END) as aantalBekekenVorige, 
    SUM(CASE WHEN dagnummer < @daynumberFirst THEN (CASE WHEN actieId BETWEEN 300 AND 399 THEN aantal ELSE 0 END) ELSE 0 END) as aantalContactVorige
FROM STAT_Statistieken
WHERE
    dagnummer >= @startDiff
    AND dagnummer < @endDiff
    AND foreignId = @foreignId 
OPTION(OPTIMIZE FOR (@foreignId = 837334, @startDiff = 200, @endDiff = 300))

DBCC统计信息

Name                                                          | Updated               | Rows      | Rows smpl | Steps | Density | Avg. key | String index
IX_STAT_Statistieken_foreignId_dagnummer_actieId_secondaryId    Oct  6 2009  3:46PM 1245058    1245058    92    0,2492834    28    NO

All Density  | Avg. Length | Columns
3,227035E-06    8    foreignId
2,905271E-06    12    foreignId, dagnummer
2,623274E-06    16    foreignId, dagnummer, actieId
2,623205E-06    20    foreignId, dagnummer, actieId, secondaryId
8,031755E-07    28    foreignId, dagnummer, actieId, secondaryId, statistiekId

RANGE HI | RANGE_ROWS | EQ_ROWS | DISTINCT_RANGE_ROWS | AVG_RANGE ROWS
-1         0            2         0                     1
1356       3563         38        1297                  2,747109
8455       14300        29        6761                  2,115072

并且索引的使用如执行计划所示。当我将其封装在一个带有这些参数的过程中:

@foreignId bigint,
@maand int, --month
@jaar int --year

使用_SP_TEMP @foreignId = 873924, @maand = 9, @jaar = 2009运行它。

它执行了聚集索引扫描!


@Jan:使用聚集索引和常规索引时,您的查询运行时间是多少? - Lieven Keersmaekers
2
@Jan:可能是一个无用的问题,但你是否更新了统计信息,碎片整理了索引(尤其是聚集索引)...? - Lieven Keersmaekers
1
如果我使用有限的信息模拟架构,就不会出现问题,它会按预期工作。关于查询缓存/查询计划命中/参数嗅探的问题已经发布了,所以如果这不能解决问题,您可以提供一个更少抽象的版本和架构,因为在抽象化时可能会削减需要帮助的信息。 - Andrew
1
请问您能否发布查询和存储过程生成的“XML”计划吗?只需运行“SET SHOWPLAN_XML ON GO SELECT ...”和“SET SHOWPLAN_XML ON GO EXECUTE _SP_TEMP ...”。 - Quassnoi
嗨Jan - 謝謝你提供的額外資訊...非常有用。我剛剛修改了我的答案,提出了另一個可能導致這個問題並解決它的建議。請參考以下內容。另外,@Quassnoi提議發布XML計劃是一個好主意。這絕對能幫助我們更好地診斷你的問題。 - Justin Grant
显示剩余6条评论
10个回答

7

[编辑]

下面提到的PERSISTED未使用问题仅在我的系统(SQL 2008)中针对actieGroep/actieId发生。但是,你的SQL 2005系统可能也会出现与dagnummer/datum列相关的同样问题。如果确实如此,那么这就可以解释你看到的行为,因为需要进行聚集索引扫描以过滤datum值。要诊断是否确实发生了这种情况,只需将datum列作为INCLUDE-d列添加到索引中,如下所示:

CREATE NONCLUSTERED INDEX [IX_STAT_Statistieken_1] ON [dbo].[STAT_Statistieken]  
(  
    [foreignId] DESC,  
    [dagnummer] DESC,  
    [actieId] ASC,   
    [aantal] ASC    
) INCLUDE (datum)  ON [PRIMARY]

如果通过这个索引修订后问题得到解决,那么你就知道dagnummer是问题所在——你甚至可以从索引中删除dagnummer,因为SQL根本没有使用它。
另外,修改索引以添加actieId是一个好主意,因为它避免了下面提到的问题。但在此过程中,你还需要将aantal列保留在索引中,以便你的索引成为此查询的覆盖索引。否则,SQL将不得不读取聚集索引以获取该列的值。这会减慢你的查询速度,因为对聚集索引的查找非常缓慢。
以下是一些可能/最容易的建议,可以帮助你修复问题:
当我尝试使用模式和查询(使用虚假生成的数据)来重现您的问题时,我发现您的PERSISTED计算列actieGroep在运行时重新计算,而不是使用持久化值。这看起来像是SQL Server优化器中的一个错误。由于底层列值actieGroep不存在于您的覆盖索引IX_STAT_Statistieken_1索引中(只有计算列存在),如果SQL Server决定需要获取该附加列,则SQL可能认为聚集索引比使用非聚集索引并查找每个匹配行的actieId更便宜。这是因为相对于顺序I/O,聚集索引查找非常昂贵,因此任何需要查找超过几个百分点的行的计划可能比扫描更便宜。无论如何,如果这确实是您所看到的问题,那么将actieGroep作为包含在IX_STAT_Statistieken_1索引中的列应该可以解决该问题。像这样:
CREATE NONCLUSTERED INDEX [IX_STAT_Statistieken_1] ON [dbo].[STAT_Statistieken] ( [foreignId] DESC, [secondaryId] ASC, [actieGroep] ASC, [dagnummer] DESC, [aantal] ASC ) INCLUDE (actieId) ON [PRIMARY]
计算列actieGroep的数据类型是字符串,但您在WHERE子句和CASE语句中将其与整数(例如IN(1,2,3))进行比较。如果SQL决定转换列而不是常量,则会损害查询性能,并可能使计算列扩展问题(上述描述)更有可能发生。我强烈建议将计算列定义更改为整数类型,例如:
CASE WHEN actieId BETWEEN 0 AND 9 THEN actieId WHEN actieId BETWEEN 10 AND 99 THEN actieId/10 WHEN actieId BETWEEN 100 AND 999 THEN actieId/100 WHEN actieId BETWEEN 1000 AND 9999 THEN actieId/1000 WHEN actieId BETWEEN 10000 AND 99999 THEN actieId/10000 WHEN actieId BETWEEN 100000 AND 999999 THEN actieId/100000 WHEN actieId BETWEEN 1000000 AND 9999999 THEN actieId/1000000 ELSE actieId/10000000 END
您正在对只有一个可能值的列进行GROUP BY。因此,GROUP BY是不必要的。希望优化器足够聪明以知道这一点,但您永远不能确定。
尝试使用OPTIMIZE FOR提示而不是直接强制索引,这可能可以解决您在提示中遇到的错误。
Craig Freedman的文章http://blogs.msdn.com/craigfr/archive/2009/04/28/implied-predicates-and-query-hints.aspx描述了提示相关错误消息的常见原因,这是当使用RECOMPILE时出现的错误消息。您可能需要查看该文章,并确保正在运行最新的SQL Server更新。
我相信您已经这样做了,但是您可能希望通过执行以下操作构建数据的“干净室”版本:创建一个新的DB,使用问题中的DDL创建表,然后使用数据填充表。如果您得到的结果不同,请仔细查看实际表格和索引中的模式,看看它们是否不同。
如果这些都不起作用,请评论,我可以提出更多的疯狂想法。 :-)
此外,请在您的问题中添加SQL Server的确切版本和更新级别!

好的。我从查询中删除了actieGroep语句,只使用了actieId,它是一个真正的int字段。我删除了GROUP BY,这确实是多余的,为foreignId和daynumbers添加了OPTIMIZE FOR提示。但没有起到作用。SQL Server的版本是SQL Server 2005 Enterprise 9.00.4035.00。 - Jan Jongboom
感谢提供额外信息。我编辑了我的答案,提出了另一个可能的解决方案,完全符合您所看到的行为。请参见上面的答案。祝你好运解决这个有趣的问题! - Justin Grant
好的。我无法解决这个问题,但是因为你提供了最可行的资源,所以我会授予你赏金 :-) 我最终只是绕过了存储过程,自己执行了SQL。 - Jan Jongboom
谢谢!这确实是个难题。我仍然很好奇到底发生了什么。你有机会尝试在索引中包含数据列吗?如果没有,没关系,我只是在想。不管怎样,很高兴你能找到一个解决办法。 - Justin Grant

6

表中的foreignId是什么数据类型?如果它是int类型,那么你很可能会得到一个隐式转换,这会阻止索引查找。如果表中的数据类型是int,则重新定义参数为int,然后你应该可以在此查询中获得索引查找(而不是索引扫描)。


太好了,盖尔,我没有想到那个办法。 :-) - RBarryYoung
这是我能想到的唯一可能导致 SQL 扫描索引的情况,即当索引覆盖并且在索引键上直接使用等值谓词时。这不应该是参数嗅探,因为对于如此简单的查询,执行计划是相同的,无论行数如何,因为不需要进行书签查找。 - GilaMonster
是的,对我来说也不像参数嗅探。 - RBarryYoung
foreignId 是一个 bigint,我在之前的存储过程版本中看到过,并且当时进行了更改。 - Jan Jongboom
你知道的,整个架构比你最初描述的要复杂得多... - GilaMonster

2

通过在调用SQL语句的末尾添加WITH RECOMPILE,可以有效地关闭参数嗅探。 - Joshua

2
首先,我应该说你创建的索引不是最优的,因为它们只能用于筛选foreignIdSQL Server无法执行SKIP SCAN,而且你的索引中有一个secondaryId没有被用于过滤范围条件。
因此,你对foreignId,actieGroep,dagNummer的条件不能产生有限数量的范围,并且不能完全进行Sargable操作。它只能在foreignID上进行过滤,而不能在整个集合上进行过滤。
现在,谈谈你当前的索引。
我刚刚使用这个脚本创建了你的表并填充了随机数据:
DROP TABLE STAT_Statistieken

CREATE TABLE [dbo].[STAT_Statistieken](
    [statistiekId] [bigint] IDENTITY(1,1) NOT NULL,
    [foreignId] [bigint] NOT NULL,
    [datum] [datetime] NOT NULL, --date
    [websiteId] [int] NOT NULL,
    [actieId] [int] NOT NULL, --actionId
    [objectSoortId] [int] NOT NULL, --kindOfObjectId
    [aantal] [bigint] NOT NULL, --count
    [secondaryId] [int] NOT NULL DEFAULT ((0)),
    [dagnummer]  AS (datediff(day,CONVERT([datetime],'2009-01-01 00:00:00.000',(121)),[datum])) PERSISTED, --daynumber
    [actieGroep]  AS (substring(CONVERT([varchar](4),[actieId],0),(1),(1))) PERSISTED,
    CONSTRAINT [STAT_Statistieken_PK] PRIMARY KEY CLUSTERED --actionGroup
    (
        [statistiekId] ASC
    )WITH (PAD_INDEX  = OFF, IGNORE_DUP_KEY = OFF) ON [PRIMARY]
    ) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_STAT_Statistieken_1] ON [dbo].[STAT_Statistieken] 
(
    [foreignId] DESC,
    [secondaryId] ASC,
    [actieGroep] ASC,
    [dagnummer] DESC, 
    [aantal] ASC --count
)WITH (PAD_INDEX  = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, IGNORE_DUP_KEY = OFF,  ONLINE = OFF) ON [PRIMARY]

CREATE NONCLUSTERED INDEX [IX_STAT_Statistieken_2] ON [dbo].[STAT_Statistieken] 
(
    [foreignId] DESC,
    [secondaryId] ASC,
    [actieId] ASC,
    [dagnummer] DESC,
    [aantal] ASC -- count
)WITH (PAD_INDEX  = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF) ON [PRIMARY]

;WITH    nums AS
        (
        SELECT  1 AS num
        UNION ALL
        SELECT  num + 1
        FROM    nums
        )
INSERT
INTO    STAT_Statistieken (
        [foreignId], [datum], [websiteId], [actieId],
        [objectSoortId], [aantal])
SELECT  TOP 100000
        500, GETDATE(), num, num, num, num % 5
FROM    nums
UNION ALL
SELECT  TOP 100000
        num % 1000, GETDATE(), num, num, num, num % 5
FROM    nums
OPTION (MAXRECURSION 0)

UPDATE STATISTICS STAT_Statistieken

无论如何,它都使用INDEX SEEK,这很可能意味着问题出在数据分布上。

我建议你创建一个额外的索引,将secondaryID删除,像这样:

CREATE NONCLUSTERED INDEX [IX_STAT_Statistieken_3] ON [dbo].[STAT_Statistieken] 
(
    [foreignId] DESC,
    [actieGroep] ASC,
    [dagnummer] DESC, 
    [aantal] ASC --count
)

如果您仍想使用当前的索引,请运行以下命令:

DBCC SHOW_STATISTICS ('STAT_Statistieken', 'IX_STAT_Statistieken_1')
DBCC SHOW_STATISTICS ('STAT_Statistieken', 'IX_STAT_Statistieken_2')

每个命令将输出三个结果集。
请从每个命令中发布结果集1和结果集2,并从结果集3中提取值为“873926”的RANGE_HI正上方,正下方和相等的三行。

有关跳过扫描的 Microsoft Connect 项目:https://connect.microsoft.com/SQLServer/feedback/details/695044/implement-index-skip-scan - usr

1

您收到的错误信息可能是由于查询中存在冲突的查询提示而生成的。

您能否在存储过程之外运行包括提示的查询?

另一种思路是,您是否使用不同的参数值测试/运行存储过程?原始执行计划所使用的参数值可能不适用于所有活动。您可以考虑重新编译存储过程,以查看在使用不同参数运行时是否产生了不同的执行计划。

如果您希望确保每次执行存储过程时都计算一个新的查询计划,则可以使用WITH RECOMPILE子句。这应该是例外而不是常规情况。通过测试验证您的过程和计划生成的行为。


是的,我实际上可以在存储过程之外使用提示来运行查询。即使使用“WITH RECOMPILE”,我也无法让提示起作用。出现了相同的错误。 - Jan Jongboom
重新编译不会解决查询提示的问题。每次执行存储过程时重新编译将确保生成一个新的执行计划,该执行计划针对给定执行所提供的特定参数进行了调整。 - John Sansom

1

请尝试这个命令并告诉我们结果:

DBCC FLUSHPROCINDB:用于清除 SQL Server 上特定数据库中存储过程缓存,而不是整个 SQL Server。要受影响的数据库 ID 必须作为命令的一部分输入。

在测试之前使用此命令可以确保以前的存储过程计划不会对测试结果产生负面影响。

示例:

DECLARE @intDBID INTEGER SET @intDBID = (SELECT dbid FROM master.dbo.sysdatabases WHERE name = 'database_name') DBCC FLUSHPROCINDB (@intDBID)


虽然没有帮助,但我认为RECOMPILE会产生同样的效果。 - Jan Jongboom

1

我之前见过类似的行为,它实际上会采用索引提示并使用更糟糕的方法(未过滤的索引扫描和书签查找)。

以下四个建议中的一个可能有所帮助:

1)将;-T4102;-T4118追加到SQL Server 2005启动参数中(可能适用于SQL 2008)。注意:这会将SQL 2000在SQL 2005中对IN和NOT IN查询的错误处理方式带回来。

2)UPDATE STATISTICS [dbo].[STAT_Statistieken] WITH FULLSCAN

3)OPTION (MAXDOP 1) - 有时并行性会导致生成非常愚蠢的查询

4)确保索引处于在线状态。

还要注意,如果您在存储过程中创建表的索引,则在编译存储过程查询时该索引不存在,因此不会被使用。由于您的表是在dbo中全局创建的,我假设这里不是这种情况。

编辑:有时我希望有一种真正的forceplan,您可以直接输入计划并执行任何可能的计划:这是一种类似于数据库的汇编语言。


更新了统计信息,添加了存储过程选项,但仍然需要进行完整的表扫描;索引在线。在存储过程之外,查询执行良好(而且速度更快)。 - Jan Jongboom
似乎您的 WITH INDEX 与索引名称不匹配。我开始觉得这不是将内容复制粘贴到 stackoverflow 中出现的错误。 - Joshua
没错,我改变了索引和名称以解决我们遇到的另一个问题。但这并没有改变什么。我将整个存储过程的内容复制到一个新窗口中,可以使用索引并具有完全相同的信息来执行。 - Jan Jongboom

0
select AU.*
FROM SYS.Allocation_units AS AU
INNER JOIN SYS.Partitions AS P
ON AU.Container_id = P.Partition_id
WHERE Object_ID = object_id('STAT_Statistieken')

尝试这个并检查非聚集索引是否比聚集索引有更多的页面(这意味着读取聚集索引更便宜)


一个在非聚集索引上的查找仍然比在聚集索引上的扫描更快。 - Joshua
嗯,不对,非聚集索引在其叶级别上具有聚集索引键,这意味着如果您使用不包含选择的索引(称为覆盖索引)进行选择,则选择将转到非聚集索引,然后将查找聚集索引中的其他字段。 (由于索引未包括actieGroep字段,因此您的索引无法覆盖您的选择) - Gabriel Guimarães
我从上面的查询中没有得到任何结果(在主服务器上?);完全没有行。 - Jan Jongboom
表在主数据库中不存在,请在您的数据库上使用查询。 - Gabriel Guimarães

0
尝试按照以下方式创建您的索引:

CREATE NONCLUSTERED INDEX [IX_STAT_Statistieken_2] ON [dbo].[STAT_Statistieken] 
(
    [foreignId] DESC,
    [secondaryId] ASC,
    [actieId] ASC,
    [dagnummer] DESC,
    [aantal] ASC -- count
)
INCLUDE (actieGroep);       
WITH (PAD_INDEX  = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, IGNORE_DUP_KEY = OFF, ONLINE = OFF) ON [PRIMARY]

然后重新创建您的过程


我已更改索引和存储过程,不再使用actieGroep,但问题仍然存在。 - Jan Jongboom

0
当你传递参数时,表中与JOIN相匹配的行数相对于表中总行数的比率是多少?SQL Server会使用匹配行数与表中总行数的比率来选择索引。如果返回的行数相对于表中的总行数很多,则该索引可能会被忽略,因为SQL Server偏爱匹配行数相对于总行数较低的索引。
因此,如果您的SELECT语句和存储过程调用为@fid使用了不同的值,那么有时候您可能会使用索引,而其他时候则不会。如果这听起来像您的问题,请在Google上查看“选择性比率”。
祝你好运!

非存储过程和存储过程都使用相同的值,我在存储过程中添加了一个OPTIMIZE FOR提示,并附加了一个提示来使用该foreignId。 - Jan Jongboom
返回的行数大约只有1,500,000条记录中的10条。 - Jan Jongboom

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接