Здравствуй, человек-читатель блогов на Community.
Хочу рассказать о своем недавнем опыте оптимизации курсора в SQL Server.
Первое, что нужно знать, курсор – это не хорошо, а плохо. Где есть возможность заменить курсор на INSERT SELECT или на использование временной таблицы, там нужно это сделать (за редким исключением). Курсор – это почти всегда дополнительные ресурсы сервера и резкое падение производительности по сравнению с другими решениями.
Второе, иногда без курсора не обойтись – там где не обойтись без построчного прохода по результату выборки. Вот в таких случаях очень важно правильно создать нужный тип курсора – тот, который соответствует решаемой задаче. Общий синтаксис объявления курсора имеет вид:
[ LOCAL | GLOBAL ]
[ FORWARD_ONLY | SCROLL ]
[ STATIC | KEYSET | DYNAMIC | FAST_FORWARD ]
[ READ_ONLY | SCROLL_LOCKS | OPTIMISTIC ]
[ TYPE_WARNING ]
FOR select_statement
[ FOR UPDATE [ OF column_name [ ,...n ] ] ] [;]
Остановлюсь на первых трех строчках ключевых параметров.
LOCAL или GLOBAL: если хотим, чтобы курсор был доступен другим процедурам, функциям, пакетам в рамках нашей сессии, то GLOBAL – в этом случае за удалением курсора следим сами (команда DEALLOCATE). Во всех остальных случаях (т.е. в подавляющем своем большинстве) – LOCAL. Внимание, по умолчанию создается именно GLOBAL курсор!
FORWARD_ONLY или SCROLL: если хотим ходить по курсору, как ненормальные, туда-сюда, то SCROLL, иначе – FORWARD_ONLY. Внимание, по умолчанию создается SCROLL курсор!
STATIC или KEYSET, DYNAMIC, FAST_FORWARD: если хотим, чтобы при проходе по курсору отображалась актуальная информация из таблицы (т.е., если после открытия курсора, мы поменяли информацию в одном из полей таблицы и хотим, чтобы при проходе по курсору в нужной строчке курсора была уже обновленная информация), то используем или KEYSET (если КАЖДАЯ таблица, участвующая в выборке, имеет уникальный индекс) или DYNAMIC (самый медленный тип). Если же нам нужен снимок результата выборки после открытия курсора – STATIC (самый быстрый тип – копия результата выборки копируется в базу tempdb и работаем уже с ней). FAST_FORWARD = KEYSET+FORWARD_ONLY+READ_ONLY – пацаны из инета пишут, что STATIC дольше открывается (т.к. создается копия в tempdb), но быстрее работает, а FAST_FORWARD – наоборот. Так что если количество записей велико (насколько большое показывает практика), то применяем STATIC, иначе – FAST_FORWARD. Внимание, по умолчанию создается DYNAMIC курсор.
Таким образом, для большого кол-ва записей в большинстве случаев мой выбор:
DECLARE cursor_name CURSOR LOCAL FORWARD_ONLY STATIC FOR
select_statement
для небольшого кол-ва записей:
DECLARE cursor_name CURSOR LOCAL FAST_FORWARD FOR
select_statement
Теперь перейдем к практике (что собственно и подтолкнуло меня к писанине сего).
Испокон веков при объявлении курсора я применял конструкцию DECLARE ... CURSOR LOCAL FOR...
При разработке интеграции с одной очень нехорошей базой, в которой нет ни одного индекса и не одного ключа, я применил тот же подход при объявлении курсоров, что и всегда. Выборка одного курсора содержала 225 000 записей. В результате процесс импорта данных из такой базы занял 15 часов 14 минут !!! И хотя импорт и был первичный (т.е. одноразовый), но даже для нормального тестирования такого импорта потребовалось бы несколько суток! После замены вышеназванной конструкции при объявлении курсора на DECLARE .. CURSOR LOCAL FORWARD_ONLY STATIC FOR.. весь процесс импорта занял ... внимание ... 10 минут 5 секунд !!! Так что игра точно стоит свеч.
Хочу повториться, что идеальный вариант - это все же не использовать курсоры вообще - для СУБД MS SQL намного роднее реляционный, а не навигационный подход.
Артем, классно, сколько пишу на T-SQL почти никогда не применял данные типы курсоров - всегда считал что это медленно и все старался заменить на insert select и декартово произведение, но оказывается эти параметры сильно ускоряют! Интересно! Надо будет запомнить. Есть правда еще один способ который ускоряет. Как известно если явно не стартовать транзакцию, то все запросы выполняются в рамках неявных транзакций, что сильно "бьет" по производительности, так вот если завести счетчик и стартовать транзакцию и коммитить после каждой 1000-й вставки, то скорость возрастает на порядки. Интересно попробовать это в случае с LOCAL FORWARD_ONLY STATIC :)
Только для надежности надо установить
SET CURSOR_CLOSE_ON_COMMIT OFF
А то курсор закроется после коммита...
Артем, спасибо! Очень полезная информация.
Олег Лабьяк,
разработчик,
3-я линия Службы поддержки Terrasoft.
"Репко Артём" написал:если хотим ходить по курсору, как ненормальные, туда-сюда, то SCROLL
Артем, объясни, пожалуйста, как это понимать?
"Раловец Ольга" написал:как это понимать?
Ольга, простите за вольный стиль. Я имел ввиду, что если курсор SCROLL, то по нему можно двигаться как вперед, так и назад, а если FORWARD_ONLY - только вперед.
Думаю, Артём имел в виду использование не только с начала и до конца FETCH NEXT, а и таких возможностей, как FETCH FIRST, FETCH LAST, FETCH PRIOR, FETCH ABSOLUTE n, FETCH RELATIVE n.
Если коротко, цитата из хелпа по SQL:
Cursor Classifications
A cursor can be classified by the fetch options it supports:
Forward-only
Rows must be fetched serially from the first row to the last row. FETCH NEXT is the only fetch operation allowed.
Scrollable
Rows can be randomly fetched from anywhere in the cursor. All the fetch operations are allowed (except that dynamic cursors do not support fetch absolute).
Scrollable cursors are especially useful for supporting online applications. A cursor can be mapped to a grid or list box in the application. As the user scrolls up and down and all around the grid, the application uses scroll fetches to retrieve the rows from the cursor the user wants to see.
Олег Лабьяк,
разработчик,
3-я линия Службы поддержки Terrasoft.
Спасибо, Артем, спасибо, Олег. :)
"Репко Артём" написал:Ольга, простите за вольный стиль
Ваш стиль кажется мне приемлемым.
Коллеги, если всем понравилось, голосуем! +1
Артёму большое спасибо!
Огромнейшее спасибо! На "раскидывание" импортированных данных по таблицам (на 30 000 записей) вместо почти 5 часов ушло 50 минут.