За някои хора това е грешен въпрос. SQL CURSOR IS грешката. Дяволът е в детайлите! Можете да прочетете всякакви богохулства в цялата SQL блогосфера от името на SQL CURSOR.
Ако се чувствате по същия начин, какво ви накара да стигнете до това заключение?
Ако е от доверен приятел и колега, не мога да ви виня. Случва се. Понякога много. Но ако някой ви е убедил с доказателство, това е друга история.
не сме се срещали преди. Не ме познаваш като приятел. Но се надявам, че мога да го обясня с примери и да ви убедя, че SQL CURSOR има своето място. Не е много, но това малко място в нашия код има правила.
Но първо нека ви разкажа моята история.
Започнах да програмирам с бази данни, използвайки xBase. Това беше в колежа до първите ми две години професионално програмиране. Казвам ви това, защото навремето обработвахме данните последователно, а не в набори като SQL. Когато научих SQL, това беше като промяна на парадигмата. Двигателят на базата данни решава вместо мен със своите команди, базирани на набори, които издадох. Когато научих за SQL CURSOR, усетих, че се върнах към старите, но удобни начини.
Но някои старши колеги ме предупредиха:„Избягвайте SQL CURSOR на всяка цена!“ Получих няколко устни обяснения и това беше всичко.
SQL CURSOR може да е лош, ако го използвате за грешна работа. Като използването на чук за рязане на дърва, това е смешно. Разбира се, могат да се случат грешки и там ще бъде фокусът ни.
1. Използването на SQL CURSOR, когато се задават базирани команди, е подходящо
Не мога да подчертая това достатъчно, но ТОВА е сърцето на проблема. Когато за първи път научих какво представлява SQL CURSOR, светна крушка. „Примки! Знам това!" Обаче не и докато не ме заболя главата и възрастните ми се скараха.
Виждате ли, подходът на SQL е базиран на множество. Издавате команда INSERT от стойности на таблицата и тя ще свърши работата без цикли във вашия код. Както казах по-рано, това е работата на двигателя на базата данни. Така че, ако принудите цикъл да добави записи към таблица, вие заобикаляте този авторитет. Ще стане грозно.
Преди да опитаме нелеп пример, нека подготвим данните:
SELECT TOP (500)
val = ROW_NUMBER() OVER (ORDER BY sod.SalesOrderDetailID)
, modified = GETDATE()
, status = 'inserted'
INTO dbo.TestTable
FROM AdventureWorks.Sales.SalesOrderDetail sod
CROSS JOIN AdventureWorks.Sales.SalesOrderDetail sod2
SELECT
tt.val
,GETDATE() AS modified
,'inserted' AS status
INTO dbo.TestTable2
FROM dbo.TestTable tt
WHERE CAST(val AS VARCHAR) LIKE '%2%'
Първото изявление ще генерира 500 записа с данни. Вторият ще получи подмножество от него. Тогава, ние сме готови. Ще вмъкнем липсващите данни от TestTable в TestTable2 използвайки SQL CURSOR. Вижте по-долу:
DECLARE @val INT
DECLARE test_inserts CURSOR FOR
SELECT val FROM TestTable tt
WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
WHERE tt1.val = tt.val)
OPEN test_inserts
FETCH NEXT FROM test_inserts INTO @val
WHILE @@fetch_status = 0
BEGIN
INSERT INTO TestTable2
(val, modified, status)
VALUES
(@val, GETDATE(),'inserted')
FETCH NEXT FROM test_inserts INTO @val
END
CLOSE test_inserts
DEALLOCATE test_inserts
Ето как да завъртите, като използвате SQL CURSOR, за да вмъкнете един по един липсващ запис. Доста дълго, нали?
Сега, нека опитаме по-добър начин - базираната на набор алтернатива. Ето го:
INSERT INTO TestTable2
(val, modified, status)
SELECT val, GETDATE(), status
FROM TestTable tt
WHERE NOT EXISTS(SELECT val FROM TestTable2 tt1
WHERE tt1.val = tt.val)
Това е кратко, спретнато и бързо. Колко бързо? Вижте фигура 1 по-долу:
Използвайки xEvent Profiler в SQL Server Management Studio, сравних данните за времето на процесора, продължителността и логическите четения. Както можете да видите на фигура 1, използването на базираната на set команда за INSERT записи печели теста за производителност. Цифрите говорят сами за себе си. Използването на SQL CURSOR консумира повече ресурси и време за обработка.
Следователно, преди да използвате SQL CURSOR, опитайте първо да напишете команда, базирана на набор. Това ще се изплати по-добре в дългосрочен план.
Но какво ще стане, ако имате нужда от SQL CURSOR, за да свършите работата?
2. Не се използват подходящи опции за SQL CURSOR
Друга грешка, която дори аз направих в миналото, беше, че не използвах подходящи опции в DECLARE CURSOR. Има опции за обхват, модел, паралелност и дали може да се превърта или не. Тези аргументи са незадължителни и е лесно да ги игнорирате. Въпреки това, ако SQL CURSOR е единственият начин за изпълнение на задачата, трябва да сте изрични с намерението си.
Така че, запитайте се:
- Когато преминавате през цикъла, ще навигирате ли по редовете само напред или ще преминете към първия, последния, предишния или следващия ред? Трябва да посочите дали КЪРСОРЪТ е само напред или с възможност за превъртане. Това е DECLARE
CURSOR FORWARD_ONLY или DECLARECURSOR SCROLL . - Ще актуализирате ли колоните в CURSOR? Използвайте READ_ONLY, ако не може да се актуализира.
- Имате ли нужда от най-новите стойности, докато преминавате през цикъла? Използвайте STATIC, ако стойностите няма да имат значение дали са последни или не. Използвайте DYNAMIC, ако други транзакции актуализират колони или изтриете редове, които използвате в CURSOR, и имате нужда от най-новите стойности. Забележка :DYNAMIC ще бъде скъпо.
- Курсорът глобален ли е за връзката или локален за партидата или съхранената процедура? Посочете дали LOCAL или GLOBAL.
За повече информация относно тези аргументи потърсете препратката от Microsoft Docs.
Пример
Нека опитаме пример, сравняващ три CURSOR за времето на процесора, логическите четения и продължителността с помощта на xEvents Profiler. Първият няма да има подходящи опции след ДЕКЛАРИРАНЕ НА КЪРСОР. Вторият е LOCAL STATIC FORWARD_ONLY READ_ONLY. Последният е LOtyuiCAL FAST_FORWARD.
Ето първото:
-- NOTE: Don't just COPY and PASTE this code then run in your machine. Read and assess.
-- DECLARE CURSOR with no options
SET NOCOUNT ON
DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
,Command NVARCHAR(2000)
);
INSERT INTO #commands (Command)
VALUES (@command)
INSERT INTO #commands (Command)
SELECT
'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
+ ' - ' + CHAR(39)
+ ' + cast(count(*) as varchar) from '
+ a.TABLE_SCHEMA + '.' + a.TABLE_NAME
FROM INFORMATION_SCHEMA.tables a
WHERE a.TABLE_TYPE = 'BASE TABLE';
DECLARE command_builder CURSOR FOR
SELECT
Command
FROM #commands
OPEN command_builder
FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
PRINT @command
FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder
DROP TABLE #commands
GO
Има по-добър вариант от горния код, разбира се. Ако целта е просто да се генерира скрипт от съществуващи потребителски таблици, SELECT ще свърши работа. След това поставете изхода в друг прозорец на заявка.
Но ако трябва да генерирате скрипт и да го стартирате наведнъж, това е различна история. Трябва да оцените изходния скрипт дали ще облага вашия сървър или не. Вижте грешка №4 по-късно.
За да ви покажем сравнението на три КУРСОРА с различни опции, това ще стане.
Сега, нека имаме подобен код, но с LOCAL STATIC FORWARD_ONLY READ_ONLY.
--- STATIC LOCAL FORWARD_ONLY READ_ONLY
SET NOCOUNT ON
DECLARE @command NVARCHAR(2000) = N'SET NOCOUNT ON;'
CREATE TABLE #commands (
ID INT IDENTITY (1, 1) PRIMARY KEY CLUSTERED
,Command NVARCHAR(2000)
);
INSERT INTO #commands (Command)
VALUES (@command)
INSERT INTO #commands (Command)
SELECT
'SELECT ' + CHAR(39) + a.TABLE_SCHEMA + '.' + a.TABLE_NAME
+ ' - ' + CHAR(39)
+ ' + cast(count(*) as varchar) from '
+ a.TABLE_SCHEMA + '.' + a.TABLE_NAME
FROM INFORMATION_SCHEMA.tables a
WHERE a.TABLE_TYPE = 'BASE TABLE';
DECLARE command_builder CURSOR LOCAL STATIC FORWARD_ONLY READ_ONLY FOR SELECT
Command
FROM #commands
OPEN command_builder
FETCH NEXT FROM command_builder INTO @command
WHILE @@fetch_status = 0
BEGIN
PRINT @command
FETCH NEXT FROM command_builder INTO @command
END
CLOSE command_builder
DEALLOCATE command_builder
DROP TABLE #commands
GO
Както можете да видите по-горе, единствената разлика от предишния код е LOCAL STATIC FORWARD_ONLY READ_ONLY аргументи.
Третият ще има LOCAL FAST_FORWARD. Сега, според Microsoft, FAST_FORWARD е FORWARD_ONLY, READ_ONLY CURSOR с активирани оптимизации. Ще видим как ще се справи с първите две.
Как се сравняват? Вижте Фигура 2:
Този, който отнема по-малко време и продължителност на процесора, е ЛОКАЛНИЯТ СТАТИЧЕН FORWARD_ONLY READ_ONLY CURSOR. Имайте предвид също, че SQL Server има стойности по подразбиране, ако не посочите аргументи като STATIC или READ_ONLY. Това има ужасни последици, както ще видите в следващия раздел.
Какво разкри sp_describe_cursor
sp_describe_cursor е съхранена процедура отмастера база данни, която можете да използвате, за да получите информация от отворения CURSOR. И ето какво разкри от първата партида заявки без опции CURSOR. Вижте Фигура 3 за резултата от sp_describe_cursor :
Прекаляване много? Вие залагате. Курсорът от първата партида заявки е:
- глобално спрямо съществуващата връзка.
- динамичен, което означава, че проследява промените в таблицата #commands за актуализации, изтривания и вмъквания.
- оптимистично, което означава, че SQL Server добави допълнителна колона към временна таблица, наречена CWT. Това е колона за контролна сума за проследяване на промените в стойностите на таблицата #commands.
- с възможност за превъртане, което означава, че можете да преминете към предишния, следващия, горния или долния ред в курсора.
Абсурд? Силно съм съгласен. Защо се нуждаете от глобална връзка? Защо трябва да проследявате промените във временната таблица #commands? Превъртахме ли някъде другаде освен следващия запис в КЪРСОРА?
Тъй като SQL Server определя това вместо нас, цикълът CURSOR се превръща в ужасна грешка.
Сега разбирате защо изричното посочване на опциите на SQL CURSOR е толкова важно. Така че отсега нататък винаги посочвайте тези аргументи CURSOR, ако трябва да използвате CURSOR.
Планът за изпълнение разкрива повече
Действителният план за изпълнение има нещо повече да каже за това какво се случва всеки път, когато се изпълнява FETCH NEXT FROM command_builder INTO @command. На фигура 4 се вмъква ред в клъстерирания индекс CWT_PrimaryKey в tempdb таблица CWT :
Записванията се случват на tempdb при всяко ИЗВЛЕЧВАНЕ СЛЕДВАЩО. Освен това има още. Помните ли, че КУРСОРЪТ е ОПТИМИСТИЧЕН на фигура 3? Свойствата на Clustered Index Scan в най-дясната част на плана разкриват допълнителната неизвестна колона, наречена Chk1002 :
Може ли това да е колоната Контролна сума? План XML потвърждава, че това наистина е така:
Сега сравнете действителния план за изпълнение на FETCH NEXT, когато курсорът е LOCAL STATIC FORWARD_ONLY READ_ONLY:
Той използва tempdb също, но е много по-просто. Междувременно, Фигура 8 показва плана за изпълнение, когато се използва LOCAL FAST_FORWARD:
Вземане за вкъщи
Една от подходящите употреби на SQL CURSOR е генериране на скриптове или изпълнение на някои административни команди към група обекти от база данни. Дори ако има незначителни употреби от него, първата ви опция е да използвате LOCAL STATIC FORWARD_ONLY READ_ONLY CURSOR или LOCAL FAST_FORWARD. Този с по-добър план и логично четене ще спечели.
След това заменете някое от тях с подходящото според нуждите. Но знаеш ли какво? В моя личен опит използвах само локален КУРСОР само за четене с обход само напред. Никога не ми се налагаше да направя CURSOR глобален и обновяем.
Освен използването на тези аргументи, времето на изпълнението е от значение.
3. Използване на SQL CURSOR за ежедневни транзакции
аз не съм администратор. Но имам представа как изглежда натовареният сървър от инструментите на DBA (или от това колко децибела крещят потребителите). При тези обстоятелства ще искате ли да добавите допълнителна тежест?
Ако се опитвате да създадете кода си с CURSOR за ежедневни транзакции, помислете отново. КУРСОРИТЕ са подходящи за еднократно изпълнение на по-малко натоварен сървър с малки набори от данни. Въпреки това, в типичен натоварен ден, CURSOR може:
- Заключване на редове, особено ако аргументът за едновременност SCROLL_LOCKS е изрично посочен.
- Причини високо натоварване на процесора.
- Използвайте tempdb обширно.
Представете си, че няколко от тях работят едновременно в един типичен ден.
На път сме да приключим, но има още една грешка, за която трябва да говорим.
4. Неоценяване на въздействието SQL CURSOR носи
Знаете, че опциите на CURSOR са добри. Смятате ли, че е достатъчно да ги уточните? Вече видяхте резултатите по-горе. Без инструментите нямаше да стигнем до правилното заключение.
Освен това има код вътре в КУРСОРА . В зависимост от това какво прави, той добавя повече към консумираните ресурси. Те може да са били налични за други процеси. Цялата ви инфраструктура, хардуерът и конфигурацията на SQL Server ще добавят повече към историята.
Какво ще кажете за обема данни ? Използвах SQL CURSOR само за няколкостотин записа. При вас може да е различно. Първият пример взе само 500 записа, защото това беше числото, което бих се съгласил да чакам. 10 000 или дори 1000 не го отрязаха. Те се представиха зле.
В крайна сметка, независимо колко по-малко или повече, проверката на логическите показания, например, може да направи разлика.
Ами ако не проверите плана за изпълнение, логическите показания или изминалото време? Какви ужасни неща могат да се случат, освен замръзването на SQL Server? Можем само да си представим всякакви сценарии на съдния ден. Разбрахте смисъла.
Заключение
SQL CURSOR работи чрез обработка на данни ред по ред. Има си място, но може да е лошо, ако не внимавате. Това е като инструмент, който рядко излиза от кутията с инструменти.
Така че, първо, опитайте да разрешите проблема, като използвате базирани на набор команди. Той отговаря на повечето от нашите SQL нужди. И ако някога използвате SQL CURSOR, използвайте го с правилните опции. Оценете въздействието с плана за изпълнение, STATISTICS IO и xEvent Profiler. След това изберете точното време за изпълнение.
Всичко това ще направи използването на SQL CURSOR малко по-добре.