SQL Server: быстрый, но медленный запрос - PullRequest
233 голосов
/ 14 января 2009

Запрос выполняется быстро:

DECLARE @SessionGUID uniqueidentifier
SET @SessionGUID = 'BCBA333C-B6A1-4155-9833-C495F22EA908'

SELECT *
FROM Report_Opener
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank

стоимость поддерева: 0.502

Но помещение одного и того же SQL-кода в хранимую процедуру выполняется медленно и с совершенно другим планом выполнения

CREATE PROCEDURE dbo.ViewOpener @SessionGUID uniqueidentifier AS
SELECT *
FROM Report_Opener
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank

EXECUTE ViewOpener @SessionGUID

Стоимость поддерева: 19,2

Я бегу

sp_recompile ViewOpener

И он все еще работает (плохо), и я также изменил сохраненный процедура до

CREATE PROCEDURE dbo.ViewOpener @SessionGUID uniqueidentifier AS
SELECT *, 'recompile please'
FROM Report_Opener
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank

И снова, пытаясь обмануть его в перекомпиляции.

Я отбросил и воссоздал хранимую процедуру, чтобы заставить ее сгенерировать новый план.

Я пытался принудительно перекомпилировать и предотвратить перехват параметров , используя переменную-ловушку:

CREATE PROCEDURE dbo.ViewOpener @SessionGUID uniqueidentifier AS

DECLARE @SessionGUIDbitch uniqueidentifier
SET @SessionGUIDbitch = @SessionGUID

SELECT *
FROM Report_Opener
WHERE SessionGUID = @SessionGUIDbitch
ORDER BY CurrencyTypeOrder, Rank

Я также пытался определить хранимую процедуру WITH RECOMPILE:

CREATE PROCEDURE dbo.ViewOpener @SessionGUID uniqueidentifier 
WITH RECOMPILE
AS
SELECT *
FROM Report_Opener
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank

Чтобы план никогда не кэшировался, и я попытался принудительно перекомпилировать команду execute:

EXECUTE ViewOpener @SessionGUID WITH RECOMPILE

Что не помогло.

Я пытался преобразовать процедуру в динамический SQL:

CREATE PROCEDURE dbo.ViewOpener @SessionGUID uniqueidentifier 
WITH RECOMPILE AS
DECLARE @SQLString NVARCHAR(500)

SET @SQLString = N'SELECT *
   FROM Report_OpenerTest
   WHERE SessionGUID = @SessionGUID
   ORDER BY CurrencyTypeOrder, Rank'

EXECUTE sp_executesql @SQLString,
N'@SessionGUID uniqueidentifier',
@SessionGUID

Что не помогло.

Сущность "Report_Opener" является представлением, которое не индексируется. Представление ссылается только на базовые таблицы. Ни одна таблица не содержит вычисляемых столбцов, проиндексированных или нет.

Черт возьми, я попытался создать представление с помощью

SET ANSI_NULLS ON
SET QUOTED_IDENTIFER ON

Это не исправило это.

Как получается, что

  • запрос быстрый
  • быстрое перемещение запроса в представление и быстрый выбор из него
  • выбор из хранимой процедуры в 40 раз медленнее?

Я попытался переместить определение представления непосредственно в хранимую процедуру (нарушив 3 бизнес-правила и нарушив важную инкапсуляцию), и это замедляет его только примерно в 6 раз.

Почему версия хранимой процедуры такая медленная? Что может быть причиной того, что SQL Server запускает специальный SQL быстрее, чем другой тип специального SQL?

Я бы действительно не хотел

  • встроить SQL в код
  • изменить код на всех

    Microsoft SQL Server  2000 - 8.00.2050 (Intel X86)
    Mar  7 2008 21:29:56
    Copyright (c) 1988-2003 Microsoft Corporation
    Standard Edition on Windows NT 5.2 (Build 3790: Service Pack 2)
    

Но что может быть причиной того, что SQL Server не может работать так же быстро, как SQL Sever, выполняющий запрос, если не анализировать параметры.


Моя следующая попытка будет иметь StoredProcedureA call StoredProcedureB call StoredProcedureC call StoredProcedureD для запроса представления.

И если это не удастся, вызовите хранимую процедуру к хранимой процедуре, вызовите UDF, вызовите UDF, вызовите хранимую процедуру, вызовите UDF для запроса представления.


Подводя итог, можно быстро выполнить следующее из QA, но медленно, если поместить его в хранимую процедуру:

Оригинал:

--Runs fine outside of a stored procedure
SELECT *
FROM Report_OpenerTest
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank

sp_executesql

--Runs fine outside of a stored procedure
DECLARE @SQLString NVARCHAR(500)
SET @SQLString = N'SELECT *
FROM Report_OpenerTest
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank'

EXECUTE sp_executesql @SQLString,
        N'@SessionGUID uniqueidentifier',
        @SessionGUID

EXEC(@sql):

--Runs fine outside of a stored procedure
DECLARE @sql NVARCHAR(500)
SET @sql = N'SELECT *
FROM Report_OpenerTest
WHERE SessionGUID = '''+CAST(@SessionGUID AS varchar(50))+'''
ORDER BY CurrencyTypeOrder, Rank'

EXEC(@sql)

Планы выполнения

План хорошо :

      |--Sort(ORDER BY:([Expr1020] ASC, [Currencies].[Rank] ASC))
           |--Compute Scalar(DEFINE:([Expr1020]=If ([Currencies].[CurrencyType]='ctCanadianCash') then 1 else If ([Currencies].[CurrencyType]='ctMiscellaneous') then 2 else If ([Currencies].[CurrencyType]='ctTokens') then 3 else If ([Currencies].[CurrencyType]
                |--Nested Loops(Left Outer Join, OUTER REFERENCES:([Openers].[OpenerGUID]))
                     |--Filter(WHERE:((([Currencies].[IsActive]<>0 AND [Currencies].[OnOpener]<>0) AND ((((((([Currencies].[CurrencyType]='ctUSCoin' OR [Currencies].[CurrencyType]='ctMiscellaneousUS') OR [Currencies].[CurrencyType]='ctUSCash') OR [Currencies].
                     |    |--Nested Loops(Left Outer Join, OUTER REFERENCES:([Currencies].[CurrencyGUID], [Openers].[OpenerGUID]) WITH PREFETCH)
                     |         |--Nested Loops(Left Outer Join)
                     |         |    |--Bookmark Lookup(BOOKMARK:([Bmk1016]), OBJECT:([GrobManagementSystemLive].[dbo].[Windows]))
                     |         |    |    |--Nested Loops(Inner Join, OUTER REFERENCES:([Openers].[WindowGUID]))
                     |         |    |         |--Bookmark Lookup(BOOKMARK:([Bmk1014]), OBJECT:([GrobManagementSystemLive].[dbo].[Openers]))
                     |         |    |         |    |--Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[Openers].[IX_Openers_SessionGUID]), SEEK:([Openers].[SessionGUID]=[@SessionGUID]) ORDERED FORWARD)
                     |         |    |         |--Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[Windows].[IX_Windows]), SEEK:([Windows].[WindowGUID]=[Openers].[WindowGUID]) ORDERED FORWARD)
                     |         |    |--Clustered Index Scan(OBJECT:([GrobManagementSystemLive].[dbo].[Currencies].[IX_Currencies_CurrencyType]))
                     |         |--Clustered Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[OpenerDetails].[IX_OpenerDetails_OpenerGUIDCurrencyGUID]), SEEK:([OpenerDetails].[OpenerGUID]=[Openers].[OpenerGUID] AND [OpenerDetails].[CurrencyGUID]=[Currenc
                     |--Hash Match(Cache, HASH:([Openers].[OpenerGUID]), RESIDUAL:([Openers].[OpenerGUID]=[Openers].[OpenerGUID]))
                          |--Stream Aggregate(DEFINE:([Expr1006]=SUM(If (((([Currencies].[CurrencyType]='ctMiscellaneous' OR [Currencies].[CurrencyType]='ctTokens') OR [Currencies].[CurrencyType]='ctChips') OR [Currencies].[CurrencyType]='ctCanadianCoin') OR [
                               |--Nested Loops(Inner Join, OUTER REFERENCES:([OpenerDetails].[CurrencyGUID]) WITH PREFETCH)
                                    |--Nested Loops(Inner Join)
                                    |    |--Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[Openers].[IX_Openers_OneOpenerPerSession]), SEEK:([Openers].[OpenerGUID]=[Openers].[OpenerGUID]) ORDERED FORWARD)
                                    |    |--Clustered Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[OpenerDetails].[IX_OpenerDetails_OpenerGUIDCurrencyGUID]), SEEK:([OpenerDetails].[OpenerGUID]=[Openers].[OpenerGUID]) ORDERED FORWARD)
                                    |--Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[Currencies].[PK_Currencies_CurrencyGUID]), SEEK:([Currencies].[CurrencyGUID]=[OpenerDetails].[CurrencyGUID]) ORDERED FORWARD)

плохо план

       |--Sort(ORDER BY:([Expr1020] ASC, [Currencies].[Rank] ASC))
            |--Compute Scalar(DEFINE:([Expr1020]=If ([Currencies].[CurrencyType]='ctCanadianCash') then 1 else If ([Currencies].[CurrencyType]='ctMiscellaneous') then 2 else If ([Currencies].[CurrencyType]='ctTokens') then 3 else If ([Currencies].[Currency
                 |--Nested Loops(Left Outer Join, OUTER REFERENCES:([Openers].[OpenerGUID]))
                      |--Filter(WHERE:((([Currencies].[IsActive]<>0 AND [Currencies].[OnOpener]<>0) AND ((((((([Currencies].[CurrencyType]='ctUSCoin' OR [Currencies].[CurrencyType]='ctMiscellaneousUS') OR [Currencies].[CurrencyType]='ctUSCash') OR [Currenc
                      |    |--Nested Loops(Left Outer Join, OUTER REFERENCES:([Currencies].[CurrencyGUID], [Openers].[OpenerGUID]) WITH PREFETCH)
                      |         |--Filter(WHERE:([Openers].[SessionGUID]=[@SessionGUID]))
                      |         |    |--Concatenation
                      |         |         |--Nested Loops(Left Outer Join)
                      |         |         |    |--Table Spool
                      |         |         |    |    |--Hash Match(Inner Join, HASH:([Windows].[WindowGUID])=([Openers].[WindowGUID]), RESIDUAL:([Windows].[WindowGUID]=[Openers].[WindowGUID]))
                      |         |         |    |         |--Clustered Index Scan(OBJECT:([GrobManagementSystemLive].[dbo].[Windows].[IX_Windows_CageGUID]))
                      |         |         |    |         |--Table Scan(OBJECT:([GrobManagementSystemLive].[dbo].[Openers]))
                      |         |         |    |--Table Spool
                      |         |         |         |--Clustered Index Scan(OBJECT:([GrobManagementSystemLive].[dbo].[Currencies].[IX_Currencies_CurrencyType]))
                      |         |         |--Compute Scalar(DEFINE:([Openers].[OpenerGUID]=NULL, [Openers].[SessionGUID]=NULL, [Windows].[UseChipDenominations]=NULL))
                      |         |              |--Nested Loops(Left Anti Semi Join)
                      |         |                   |--Clustered Index Scan(OBJECT:([GrobManagementSystemLive].[dbo].[Currencies].[IX_Currencies_CurrencyType]))
                      |         |                   |--Row Count Spool
                      |         |                        |--Table Spool
                      |         |--Clustered Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[OpenerDetails].[IX_OpenerDetails_OpenerGUIDCurrencyGUID]), SEEK:([OpenerDetails].[OpenerGUID]=[Openers].[OpenerGUID] AND [OpenerDetails].[CurrencyGUID]=[Cu
                      |--Hash Match(Cache, HASH:([Openers].[OpenerGUID]), RESIDUAL:([Openers].[OpenerGUID]=[Openers].[OpenerGUID]))
                           |--Stream Aggregate(DEFINE:([Expr1006]=SUM([partialagg1034]), [Expr1007]=SUM([partialagg1035]), [Expr1008]=SUM([partialagg1036]), [Expr1009]=SUM([partialagg1037]), [Expr1010]=SUM([partialagg1038]), [Expr1011]=SUM([partialagg1039]
                                |--Nested Loops(Inner Join)
                                     |--Stream Aggregate(DEFINE:([partialagg1034]=SUM(If (((([Currencies].[CurrencyType]='ctMiscellaneous' OR [Currencies].[CurrencyType]='ctTokens') OR [Currencies].[CurrencyType]='ctChips') OR [Currencies].[CurrencyType]='
                                     |    |--Nested Loops(Inner Join, OUTER REFERENCES:([OpenerDetails].[CurrencyGUID]) WITH PREFETCH)
                                     |         |--Clustered Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[OpenerDetails].[IX_OpenerDetails_OpenerGUIDCurrencyGUID]), SEEK:([OpenerDetails].[OpenerGUID]=[Openers].[OpenerGUID]) ORDERED FORWARD)
                                     |         |--Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[Currencies].[PK_Currencies_CurrencyGUID]), SEEK:([Currencies].[CurrencyGUID]=[OpenerDetails].[CurrencyGUID]) ORDERED FORWARD)
                                     |--Index Seek(OBJECT:([GrobManagementSystemLive].[dbo].[Openers].[IX_Openers_OneOpenerPerSession]), SEEK:([Openers].[OpenerGUID]=[Openers].[OpenerGUID]) ORDERED FORWARD)

Плохой хочет намотать 6 миллионов строк; другой нет.

Примечание: Это не вопрос настройки запроса. У меня есть запрос, который работает молниеносно. Я просто хочу, чтобы SQL Server быстро запускался из хранимой процедуры.

Ответы [ 12 ]

360 голосов
/ 08 июля 2009

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

Я нашел другой ответ здесь "Параметр нюхает" Спасибо Omnibuzz. Все сводится к использованию «локальных переменных» в ваших запросах к хранимым процедурам, но для большего понимания прочитайте оригинал, это отличная статья. например,

Медленный путь:

CREATE PROCEDURE GetOrderForCustomers(@CustID varchar(20))
AS
BEGIN
    SELECT * 
    FROM orders
    WHERE customerid = @CustID
END

Быстрый путь:

CREATE PROCEDURE GetOrderForCustomersWithoutPS(@CustID varchar(20))
AS
BEGIN
    DECLARE @LocCustID varchar(20)
    SET @LocCustID = @CustID

    SELECT * 
    FROM orders
    WHERE customerid = @LocCustID
END

Надеюсь, это поможет кому-то еще, благодаря этому мое время выполнения сократилось с 5+ минут до 6-7 секунд.

126 голосов
/ 14 января 2009

Я обнаружил проблему, вот скрипт медленной и быстрой версии хранимой процедуры:

dbo.ViewOpener__RenamedForCruachan__Slow.PRC

SET QUOTED_IDENTIFIER OFF 
GO
SET ANSI_NULLS OFF 
GO

CREATE PROCEDURE dbo.ViewOpener_RenamedForCruachan_Slow
    @SessionGUID uniqueidentifier
AS

SELECT *
FROM Report_Opener_RenamedForCruachan
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank
GO

SET QUOTED_IDENTIFIER OFF 
GO
SET ANSI_NULLS ON 
GO

dbo.ViewOpener__RenamedForCruachan__Fast.PRC

SET QUOTED_IDENTIFIER OFF 
GO
SET ANSI_NULLS ON 
GO

CREATE PROCEDURE dbo.ViewOpener_RenamedForCruachan_Fast
    @SessionGUID uniqueidentifier 
AS

SELECT *
FROM Report_Opener_RenamedForCruachan
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank
GO

SET QUOTED_IDENTIFIER OFF 
GO
SET ANSI_NULLS ON 
GO

Если вы не заметили разницу, я вас не виню. Разница не в хранимой процедуре вообще. Разница, которая превращает быстрый запрос на 0,5 стоимости в запрос с 6 миллионами строк:

Медленно: SET ANSI_NULLS OFF

Быстро: SET ANSI_NULLS ON


Этот ответ также может иметь смысл, поскольку в представлении есть предложение соединения, которое гласит:

(table.column IS NOT NULL)

Итак, в этом участвует NULL.


Это объяснение подтверждается возвращением в Query Analizer и выполнением

SET ANSI_NULLS OFF

.

DECLARE @SessionGUID uniqueidentifier
SET @SessionGUID = 'BCBA333C-B6A1-4155-9833-C495F22EA908'

.

SELECT *
FROM Report_Opener_RenamedForCruachan
WHERE SessionGUID = @SessionGUID
ORDER BY CurrencyTypeOrder, Rank

И запрос медленный.


Так что проблема не , потому что запрос выполняется из хранимой процедуры. Проблема в том, что по умолчанию для соединения Enterprise Manager выбрано ANSI_NULLS off, а не ANSI_NULLS on, что по умолчанию для QA.

Microsoft подтверждает этот факт в KB296769 (BUG: невозможно использовать SQL Enterprise Manager для создания хранимых процедур, содержащих связанные объекты сервера). Обходной путь - включить параметр ANSI_NULLS в диалоговом окне хранимой процедуры:

Set ANSI_NULLS ON
Go
Create Proc spXXXX as
....
16 голосов
/ 05 декабря 2009

Сделайте это для своей базы данных. У меня та же проблема - она ​​отлично работает в одной базе данных, но когда я копирую эту базу данных в другую, используя импорт служб SSIS (не обычное восстановление), эта проблема возникает с большинством моих хранимых процедур. Поэтому, погуглив еще немного, я нашел блог Пинала Дэйва (между прочим, я встретил большую часть его поста и очень мне помог, так что спасибо Пиналу Дейву) .

Я выполняю приведенный ниже запрос к моей базе данных, и это исправило мою проблему:

EXEC sp_MSforeachtable @command1="print '?' DBCC DBREINDEX ('?', ' ', 80)"
GO
EXEC sp_updatestats
GO 

Надеюсь, это поможет. Просто передаю помощь от других, которые мне помогли.

10 голосов
/ 31 мая 2017

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

https://stackoverflow.com/a/24016676/814299

В конце вашего запроса добавьте OPTION (OPTIMIZE FOR (@now UNKNOWN))

4 голосов
/ 08 июля 2009

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

3 голосов
/ 06 марта 2014

У меня возникла эта проблема. Мой запрос выглядел примерно так:

select a, b, c from sometable where date > '20140101'

Моя хранимая процедура была определена как:

create procedure my_procedure (@dtFrom date)
as
select a, b, c from sometable where date > @dtFrom

Я изменил тип данных на datetime и вуаля! Ездил от 30 минут до 1 минуты!

create procedure my_procedure (@dtFrom datetime)
as
select a, b, c from sometable where date > @dtFrom
1 голос
/ 15 июля 2015

Это может звучать глупо и кажется очевидным из названия SessionGUID, но является ли столбец уникальным идентификатором в Report_Opener? Если нет, вы можете попробовать привести его к правильному типу и сделать снимок или объявить переменную с правильным типом.

План, созданный как часть sproc, может работать не интуитивно и выполнять внутреннее приведение на большом столе.

1 голос
/ 14 января 2009

Это, вероятно, маловероятно, но, учитывая, что наблюдаемое вами поведение необычно, его необходимо проверить, и никто больше не упомянул об этом.

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

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

Обратите внимание, что в тексте вы выполняете некоторые вещи без указания владельца, например,

sp_recompile ViewOpener

если, например, там, где присутствуют две копии viewOpener, принадлежащие dbo и [другому пользователю], то какая из них вы на самом деле перекомпилируете, если не укажете, зависит от обстоятельств. То же самое с представлением Report_Opener - если там, где есть две копии (и они могут отличаться в спецификации или плане выполнения), то, что используется, зависит от обстоятельств - и, поскольку вы не указываете владельца, вполне возможно, что ваш запрос adhoc мог бы использовать одну и Скомпилированная процедура может использовать другой.

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

1 голос
/ 14 января 2009

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

Сам исходный запрос работает быстро, потому что оптимизатор может видеть, что параметр никогда не будет нулевым. В случае SP оптимизатор не может быть уверен, что параметр никогда не будет нулевым.

1 голос
/ 14 января 2009

Хотя я обычно против (хотя в этом случае кажется, что у вас есть подлинная причина), вы пытались предоставить какие-либо подсказки для запроса в версии SP запроса? Если SQL Server готовит другой план выполнения в этих двух экземплярах, можете ли вы использовать подсказку, чтобы сообщить ему, какой индекс использовать, чтобы план соответствовал первому?

Для некоторых примеров, вы можете перейти сюда .

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

SECOND: обновлена ​​ссылка для соответствия SQL-2000. Вам придется прокручивать пути вниз, но есть вторая, озаглавленная «Настольные подсказки», это то, что вы ищете.

THIRD: «Плохой» запрос, кажется, игнорирует [IX_Openers_SessionGUID] в таблице «Openers» - есть ли шанс добавить подсказку INDEX, чтобы заставить его использовать этот индекс, что-то изменит?

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...