SQL Server не использует индекс в хранимой процедуре

Я не решил эту проблему с помощью хранимой процедуры, но мы решили превзойти SP и просто выполнить простой OL SQL

См. схему расширенной таблицы ниже
Изменить 2: обновить индекс (чтобы больше не использовать actieGroep)
NB. SQL Server 2005 Enterprise 9.00.4035.00
NB2. Кажется, связано с http://www.sqlservercentral.com/Forums/Topic781451-338-1.aspx

У меня есть два индекса на таблице:

  • Кластеризованный индекс PK для statistiekId
  • Некластеризованный индекс для 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

Теперь он запускает сканирование индексов кластеризованного индекса в моем индексе PK! Wtf продолжается?

Итак, я изменил 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

Он выполняет кластерное сканирование индексов!

Ответ 1

[EDIT]

Проблема PERSISTED-not-being, используемая ниже, встречается только с actieGroep/actieId в моей системе (SQL 2008). Но возможно, что такая же проблема может произойти и в вашей системе SQL 2005 с столбцами dagnummer/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 должен будет прочитать ваш кластерный индекс, чтобы получить значение этого столбца. Это замедлит ваш запрос, так как поиск в кластеризованном индексе довольно медленный.

[END EDIT]

Вот куча идей, которые могут помочь вам исправить это, сначала с наиболее вероятными/легкими вещами:

  • Когда я попытался воспроизвести используемую схему и запросы (с поддельными сгенерированными данными), я вижу, что ваш вычисленный столбец actieGroep PERSISTED повторно обрабатывается во время выполнения вместо используемого значения. Это выглядит как ошибка в оптимизаторе SQL Server. Поскольку базовое значение столбца actieGroep отсутствует в вашем индексе индекса покрытия IX_STAT_Statistieken_1 (имеется только вычисляемый столбец), если SQL Server решает, что ему нужно извлечь этот дополнительный столбец, SQL может считать кластеризованный индекс более дешевым, чем использование ваш некластеризованный индекс и затем поиск actieId для каждой соответствующей строки в индексе кластера. Это связано с тем, что поиск в кластеризованном индексе очень дорог относительно последовательных операций ввода-вывода, поэтому любой план, который требует поиска более нескольких процентов строк, вероятно, дешевле, чем при сканировании. В любом случае, если это действительно проблема, которую вы видите, то добавление actieGroep в качестве столбца INCLUDE-d вашего индекса 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 - это строка, но вы сравниваете ее с целыми числами (например, IN (1,2,3)) в предложениях WHERE и CASE. Если 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 post http://blogs.msdn.com/craigfr/archive/2009/04/28/implied-predicates-and-query-hints.aspx, который описывает общие причины сообщения об ошибке, которое вы получаете, когда RECOMPILE используемый. Вы можете просмотреть эту запись и убедиться, что используете последние обновления для SQL Server.

  • Я уверен, что вы уже это сделали, но вам может понадобиться создать "чистую комнату" версию ваших данных, делая то, что мы делаем: создаем новую БД, используем DDL в ваш вопрос о создании таблиц, а затем заполнение таблиц данными. Если результаты, полученные вами, различны, посмотрите на схему в своей реальной таблице и индексах и посмотрите, отличаются ли они.

Если ничего из этого не получится, прокомментируйте, и я могу предложить еще несколько диких идей.: -)

Кроме того, добавьте точный уровень и уровень обновления SQL Server на свой вопрос!

Ответ 2

Какой тип данных является foreignId в таблице? Если это int, вы, скорее всего, получите неявное преобразование, которое предотвращает поиск индекса. Если тип данных в таблице является int, переопределите параметр и int, и вы должны получить поиск индекса (не сканирование индекса) для этого запроса.

Ответ 4

Во-первых, я должен сказать, что созданные вами индексы не являются оптимальными, поскольку они могут использоваться только для фильтрации на foreignId.

SQL Server не может выполнить SKIP SCAN, и у вас есть secondaryId в вашем индексе, который не фильтруется с условием диапазона.

Поэтому ваше условие на foreignId, actieGroep, dagNummer не дает ограниченного числа диапазонов и не полностью поддается. Он может фильтровать только на foreignId, а не на весь набор.

Теперь, к вашему текущему индексу.

Я только что создал ваши таблицы и заполнил их случайными данными, используя этот script:

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 со значением RANGE_HI чуть выше, чуть ниже и равным 873926?

Ответ 5

Сообщение об ошибке, которое вы получили, может быть сгенерировано, если в вашем запросе есть конфликтующие подсказки.

Можно ли выполнить запрос, включая подсказку, за пределами хранимой процедуры?

Альтернативный ход мыслей, проверяли ли вы свою хранимую процедуру с другим значением параметра? Возможно, что значение параметра, используемое для создания исходного плана выполнения, не является подходящим кандидатом для всех видов деятельности. Возможно, вы захотите перекомпилировать хранимую процедуру, чтобы узнать, создается ли другой план выполнения между различными запусками с разными параметрами.

Если вы хотите убедиться, что новый план запроса рассчитан для каждого выполнения вашей хранимой процедуры, вы можете использовать предложение WITH RECOMPILE. Это должно быть исключением и НЕ нормой. Подтвердите поведение вашей процедуры и планируйте ее через тестирование.

Ответ 6

Попробуйте это и сообщите нам результат:

DBCC FLUSHPROCINDB: используется для очистки кеша хранимой процедуры для конкретной базы данных на SQL Server, а не всего SQL Server. Идентификационный номер базы данных, который должен быть затронут, должен быть введен как часть команды.

Вы можете использовать эту команду перед тестированием, чтобы гарантировать, что предыдущие планы хранимых процедур не будут отрицательно влиять на результаты тестирования.

Пример:

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

Ответ 7

Я видел подобное поведение раньше, когда он действительно использовал бы подсказку индекса и делал с ней что-то еще (нефильтрованное сканирование индекса при поиске по закладкам).

Один из этих четырех должен помочь:

1) Добавить -T4102; -T4118 в параметры запуска SQL Server 2005 (может применяться к SQL 2008). Примечание: это приводит к плохой обработке SQL 2000 запросов IN и NOT IN в SQL 2005.

2) СТАТИСТИКА ОБНОВЛЕНИЯ [dbo]. [STAT_Statistieken] WITH FULLSCAN

3) OPTION (MAXDOP 1) - иногда parallelism вызывает действительно глупые запросы, которые будут сгенерированы

4) Убедитесь, что индекс включен.

Также обратите внимание, что если вы создаете индекс в таблице, созданной в хранимой процедуре, этот индекс не существует при компиляции запросов хранимой процедуры, поэтому он не будет использоваться. Так как ваша таблица создается глобально в dbo, я предполагаю, что здесь не так.

РЕДАКТИРОВАТЬ: иногда мне жаль, что не было реального силового плана, в котором вы могли бы напрямую включить план и какой-либо возможный план будет выполнен: вроде типа сборки для БД.

Ответ 8

Когда вы передаете параметр, сколько строк в таблице соответствует JOIN по отношению к общему количеству строк в таблице? SQL Server выбирает индекс, среди прочего, отношение совпадающих строк, возвращаемых JOIN, к общему количеству строк в таблице. Если имеется большое количество строк, возвращаемых относительно общего числа в таблице, индекс может быть проигнорирован как индексы предпочтений SQL Server, где число совпадающих строк меньше по отношению к сумме.

Итак, если ваш SELECT и ваш вызов с сохраненной процедурой используют разные значения для @fid, вы иногда можете использовать индекс и другие времена. Если это звучит как ваша проблема, посмотрите на "коэффициент избирательности" в google.

Удачи!

Ответ 9

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')

Попробуйте это и проверьте, содержит ли индекс NON CLUSTERED больше страниц, чем CLUSTERED INDEX (ЭТО ОЗНАЧАЕТ, ЧТО ЭТО НЕИСПРАВНО ПРОЧИТАТЬ КЛАСТЕРИРОВАННЫЙ ИНДЕКС)

Ответ 10

Попробуйте создать свой индекс следующим образом:

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]

И заново создайте свою процедуру