Database
 sql >> база данни >  >> RDS >> Database

Случай на употреба за sp_prepare / sp_prepexec

Има функции, от които много от нас избягват, като курсори, тригери и динамичен SQL. Няма съмнение, че всеки от тях има своите случаи на употреба, но когато видим тригер с курсор в динамичния SQL, това може да ни накара да се потреперим (троен удар).

Плановите ръководства и sp_prepare са в подобна лодка:ако ме видите да използвам един от тях, ще повдигнете вежда; ако ме видиш да ги използвам заедно, вероятно ще провериш температурата ми. Но, както при курсорите, тригерите и динамичния SQL, те имат своите случаи на употреба. И наскоро попаднах на сценарий, при който използването им заедно беше от полза.

Фон

Имаме много данни. И много приложения, работещи срещу тези данни. Някои от тези приложения са трудни или невъзможни за промяна, особено готови приложения от трета страна. Така че, когато компилираното им приложение изпраща ad hoc заявки към SQL Server, особено като подготвен оператор, и когато нямаме свободата да добавяме или променяме индекси, няколко възможности за настройка веднага отпадат.

В този случай имахме таблица с няколко милиона реда. Опростена и дезинфекцирана версия:

CREATE TABLE dbo.TheThings
(
  ThingID    bigint NOT NULL,
  TypeID     uniqueidentifier NOT NULL,
  dt1        datetime NOT NULL DEFAULT sysutcdatetime(),
  dt2        datetime NOT NULL DEFAULT sysutcdatetime(),
  dt3        datetime NOT NULL DEFAULT sysutcdatetime(),
  CONSTRAINT PK_TheThings PRIMARY KEY (ThingID)
);
 
CREATE INDEX ix_type ON dbo.TheThings(TypeID);
 
SET NOCOUNT ON;
GO
 
DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4',
        @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1000) 1000 + ROW_NUMBER() OVER (ORDER BY name), @guid1
    FROM sys.all_columns;
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1) 2500, @guid2
    FROM sys.all_columns;
 
INSERT dbo.TheThings(ThingID, TypeID)
  SELECT TOP (1000) 3000 + ROW_NUMBER() OVER (ORDER BY name), @guid1
    FROM sys.all_columns;

Подготвеното изявление от приложението изглеждаше така (както се вижда в кеша на плана):

(@P0 varchar(8000))SELECT * FROM dbo.TheThings WHERE TypeID = @P0

Проблемът е, че за някои стойности на TypeID , ще има много хиляди редове. За други стойности би имало по-малко от 10. Ако грешен план бъде избран (и използван повторно) въз основа на един тип параметър, това може да бъде проблем за останалите. За заявката, която извлича шепа редове, искаме търсене на индекс с търсения, за да извлече допълнителните необхванати колони, но за заявката, която връща 700 000 реда, искаме просто клъстерно сканиране на индекса. (В идеалния случай индексът ще обхваща, но тази опция не беше в картите този път.)

На практика приложението винаги получаваше вариацията за сканиране, въпреки че това беше необходимото около 1% от времето. 99% от заявките са използвали сканиране на 2 милиона реда, когато са можели да използват търсене + 4 или 5 търсения.

Бихме могли лесно да възпроизведем това в Management Studio, като изпълним тази заявка:

DBCC FREEPROCCACHE;
DECLARE @P0 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4';
SELECT * FROM dbo.TheThings WHERE TypeID = @P0;
GO
 
DBCC FREEPROCCACHE;
DECLARE @P0 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
SELECT * FROM dbo.TheThings WHERE TypeID = @P0;
GO

Плановете се върнаха така:

Оценката и в двата случая беше 1000 реда; предупрежденията вдясно се дължат на остатъчно I/O.

Как бихме могли да се уверим, че заявката е направила правилния избор в зависимост от параметъра? Ще трябва да го направим повторно компилира, без да добавяме намеци към заявката, да включваме флагове за проследяване или да променяме настройките на базата данни.

Ако изпълних заявките независимо с помощта на OPTION (RECOMPILE) , бих получил търсенето, когато е подходящо:

DBCC FREEPROCCACHE;
 
DECLARE @guid1 uniqueidentifier = 'EE81197A-B2EA-41F4-882E-4A5979ACACE4',
        @guid2 uniqueidentifier = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
SELECT * FROM dbo.TheThings WHERE TypeID = @guid1 OPTION (RECOMPILE);
SELECT * FROM dbo.TheThings WHERE TypeID = @guid2 OPTION (RECOMPILE);

С RECOMPILE получаваме по-точни оценки и търсене, когато имаме нужда.

Но отново не можахме да добавим намека към заявката директно.

Нека опитаме ръководство за план

Много хора предупреждават срещу ръководствата за планове, но тук бяхме някак в ъгъла. Определено бихме предпочели да променим заявката или индексите, ако можем. Но това може да е следващото най-добро нещо.

EXEC sys.sp_create_plan_guide   
  @name   = N'TheThingGuide',
  @stmt   = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0',
  @type   = N'SQL',
  @params = N'@P0 varchar(8000)',
  @hints  = N'OPTION (RECOMPILE)';

Изглежда просто; тестването е проблемът. Как да симулираме подготвено изявление в Management Studio? Как можем да сме сигурни, че приложението получава ръководния план и че това е изрично заради ръководството за плана?

Ако се опитаме да симулираме тази заявка в SSMS, това се третира като ad hoc изявление, а не като подготвено изявление и не можах да получа това, за да взема ръководството за план:

DECLARE @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- also tried uniqueidentifier
SELECT * FROM dbo.TheThings WHERE TypeID = @P0

Динамичният SQL също не работи (това също се третира като ad hoc изявление):

DECLARE @sql nvarchar(max) = N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0', 
        @params nvarchar(max) = N'@P0 varchar(8000)', -- also tried uniqueidentifier
        @P0 varchar(8000) = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';
 
EXEC sys.sp_executesql @sql, @params, @P0;

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

SELECT * FROM TheThings WHERE TypeID = 'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F';

Не мога да проверя кеша на плана за заявките, изпълнявани от приложението, тъй като кешираният план не посочва нищо за използването на ръководството за план (SSMS инжектира тази информация в XML вместо вас, когато генерирате действителен план). И ако заявката наистина спазва намека RECOMPILE, който предавам в ръководството за плана, как изобщо бих могъл да видя някакво доказателство в кеша на плана?

Нека опитаме sp_prepare

Използвал съм sp_prepare по-малко в кариерата си, отколкото ръководства за планове, и не бих препоръчал да го използвате за код на приложение. (Както посочва Ерик Дарлинг, оценката може да бъде извлечена от вектора на плътността, а не от подушването на параметъра.)

В моя случай не искам да го използвам от съображения за производителност, искам да го използвам (заедно със sp_execute), за да симулирам подготвеното изявление, идващо от приложението.

DECLARE @o int;
EXEC sys.sp_prepare @o OUTPUT, N'@P0 varchar(8000)',
     N'SELECT * FROM dbo.TheThings WHERE TypeID = @P0';
 
EXEC sys.sp_execute @o,  'EE81197A-B2EA-41F4-882E-4A5979ACACE4'; -- PK scan
EXEC sys.sp_execute @o,  'D989AADB-5C34-4EE1-9BE2-A88B8F74A23F'; -- IX seek + lookup

SSMS ни показва, че ръководството за плана е използвано и в двата случая.

Няма да можете да проверите кеша на плана за тези резултати поради повторното компилиране. Но в сценарий като моя трябва да можете да видите ефектите при наблюдение, изрична проверка чрез разширени събития или наблюдаване на облекчаването на симптома, който ви накара да проучите тази заявка на първо място (само имайте предвид, че средното време на изпълнение, заявка статистиката и т.н. може да бъде повлияна от допълнителна компилация).

Заключение

Това беше един от случаите, когато ръководството за план беше полезно, а sp_prepare беше полезен при потвърждаването, че ще работи за приложението. Те не са често полезни и по-рядко заедно, но за мен това беше интересна комбинация. Дори и без ръководството за плана, ако искате да използвате SSMS за симулиране на приложение, изпращащо подготвени изявления, sp_prepare е ваш приятел. (Вижте също sp_prepexec, който може да бъде пряк път, ако не се опитвате да потвърдите два различни плана за една и съща заявка.)

Имайте предвид, че това упражнение не е непременно за постигане на по-добро представяне през цялото време – то е за изравняване на разликите в представянето. Прекомпилирането очевидно не е безплатно, но ще платя малка неустойка, за да накарам 99% от моите заявки да се изпълнят за 250 мс и 1% да се изпълнят за 5 секунди, вместо да остана с план, който е абсолютно ужасен за 99% от заявките или 1% от заявките.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Анализ на данни срещу наука за данни:Каква е разликата?

  2. SQL МЕЖДУ - интелигентни съвети за сканиране за диапазон от стойности

  3. Кой е най-бързият начин за изчисляване на медианата?

  4. Променливата на средата LD_DEBUG

  5. Създаване на нови таблици в IRI Workbench