Как практик, работающий с базами данных более десяти лет, я часто сталкиваюсь с ситуациями, когда обычные SQL-запросы не справляются с задачей. Курсоры — это мощный, но коварный инструмент. В этой статье я разберу, как использовать курсоры для построчной обработки данных, когда это оправдано, а когда лучше искать альтернативу.
Вы узнаете, как избежать типичных ошибок, повысить производительность и принять взвешенное решение о применении курсоров в ваших проектах.
Введение в курсоры: зачем и когда использовать
Курсор в SQL — это механизм, позволяющий обрабатывать строки результирующего набора одну за другой. В отличие от set-based запросов, которые работают со всем набором данных сразу, курсор даёт контроль над каждой строкой. Это может быть критически важно для сложной бизнес-логики, но за это приходится платить производительностью.
Важно: Используйте курсоры только тогда, когда set-based операции невозможны или слишком сложны.
Что такое курсор в SQL
Определение курсора
Курсор — это указатель на результирующий набор запроса. Он позволяет последовательно «проходить» по строкам, извлекая их и выполняя действия. Представьте себе итератор в программировании: курсор — это аналог для SQL.
Отличие от set-based запросов
Set-based запросы (например, UPDATE с WHERE) обрабатывают все строки за одну операцию. Курсор же обрабатывает строки по одной, что может быть в десятки раз медленнее. Однако, когда нужно выполнить для каждой строки различные действия (вызов хранимой процедуры, сложные вычисления), курсор незаменим.
Когда оправдано применение курсора

- Построчная логика: необходимо выполнить для каждой строки разные действия в зависимости от её содержимого.
- Интеграция с внешними системами: для каждой строки нужно вызвать API или отправить email.
- Устаревшие процедуры: при миграции с legacy-систем, где курсоры были единственным способом.
Альтернативы курсорам: set-based операции
Прежде чем писать курсор, всегда стоит подумать об альтернативах. Оконные функции (ROW_NUMBER(), SUM() OVER), CTE (Common Table Expressions) и рекурсивные запросы, временные таблицы — всё это может решить задачу быстрее. Например, BULK COLLECT в Oracle позволяет собрать данные в массив и обработать их в цикле, что быстрее, чем построчный FETCH.
Жизненный цикл курсора: объявление, открытие, выборка, закрытие
Стандартный алгоритм работы с курсором состоит из четырёх этапов. Рассмотрим их на примере популярных СУБД.
Совет: Всегда закрывайте и освобождайте курсоры, чтобы избежать утечек ресурсов и блокировок.
Объявление курсора (DECLARE)
Синтаксис DECLARE
В SQL Server: DECLARE cursor_name CURSOR FOR SELECT .... В Oracle: CURSOR cursor_name IS SELECT .... В PostgreSQL: DECLARE cursor_name CURSOR FOR SELECT .... В MySQL: DECLARE cursor_name CURSOR FOR SELECT ... (только в хранимых процедурах).
Опции курсора

Опции определяют поведение: FORWARD_ONLY (только вперёд), SCROLL (можно перемещаться в любом направлении), STATIC (снимок данных), DYNAMIC (видит изменения), KEYSET (фиксирует ключи). Выбор опции влияет на производительность и согласованность.
Локальные vs глобальные курсоры
Локальный курсор виден только в текущем пакете или хранимой процедуре. Глобальный — во всех последующих пакетах. В SQL Server по умолчанию курсор локальный, если не указано GLOBAL.
Открытие курсора (OPEN)
Команда OPEN выполняет запрос и формирует результирующий набор. Если запрос некорректен, возникнет ошибка. В Oracle при открытии курсора можно передать параметры, если он объявлен с параметрами.
Выборка данных (FETCH)
Направления выборки
FETCH может извлекать строки в разных направлениях: NEXT (следующая), PRIOR (предыдущая), FIRST, LAST, ABSOLUTE n (n-я строка), RELATIVE n (через n строк от текущей). Для FORWARD_ONLY доступен только NEXT.
FETCH INTO

Извлечённые данные помещаются в переменные: FETCH NEXT FROM cursor_name INTO @var1, @var2. Количество и типы переменных должны совпадать с колонками запроса.
Проверка состояния
После FETCH нужно проверять, удалось ли извлечь строку. В SQL Server используется @@FETCH_STATUS (0 — успешно, -1 — нет строк, -2 — строка удалена). В других СУБД — аналоги: SQL%FOUND в Oracle, NOT FOUND в PostgreSQL.
Закрытие и освобождение курсора (CLOSE, DEALLOCATE)
CLOSE освобождает результирующий набор, но сохраняет определение курсора. DEALLOCATE удаляет определение. В Oracle достаточно CLOSE, в SQL Server и PostgreSQL нужно и CLOSE, и DEALLOCATE.
Типы курсоров и их особенности
Выбор типа курсора напрямую влияет на производительность и согласованность данных. Ниже — сравнительная таблица.
| Тип курсора | Поведение при изменениях | Производительность | Поддерживаемые операции выборки |
|---|---|---|---|
| Статический | Не видит изменений (снимок) | Высокая (копия данных) | Все (NEXT, PRIOR, FIRST, LAST, ABSOLUTE, RELATIVE) |
| Динамический | Видит все изменения | Низкая (постоянная проверка) | NEXT (иногда PRIOR) |
| Keyset-driven | Видит изменения для существующих строк | Средняя | Все |
| Forward-only | Зависит от реализации | Самая высокая | Только NEXT |
Важно: Выбор типа курсора напрямую влияет на производительность и согласованность данных. Динамические курсоры медленнее, но видят изменения.
Статический курсор
Статический курсор создаёт копию данных в tempdb. Он не видит изменений, сделанных другими сессиями. Поддерживает все операции выборки. Идеален для отчётов, где важна консистентность.
Динамический курсор

Динамический курсор отражает все изменения (вставки, обновления, удаления) в реальном времени. Он медленный, потому что каждый FETCH проверяет актуальность строк. Используется в системах реального времени, где нужна актуальность.
Keyset-driven курсор
Keyset-driven курсор фиксирует набор ключевых столбцов при открытии. Он видит изменения только для существующих строк (новые строки не добавляются, удалённые пропускаются). Компромисс между статическим и динамическим.
Forward-only курсор
Forward-only курсор — самый быстрый. Он позволяет двигаться только вперёд. В SQL Server это тип по умолчанию. Подходит для простых циклов.
Практические примеры: пошаговые сценарии
Рассмотрим три реальных сценария с полным кодом. В примерах обязательно используем обработку ошибок и проверку на конец набора.
Частая ошибка: В примерах обязательно используйте обработку ошибок и проверку на конец набора.
Пример 1: Построчное обновление с условием
Сценарий: увеличить цену на 10% только для товаров с остатком менее 100.
DECLARE @product_id INT, @current_price DECIMAL(10,2), @stock INT
DECLARE price_cursor CURSOR FOR
SELECT ProductID, Price, Stock FROM Products WHERE Stock < 100
OPEN price_cursor
FETCH NEXT FROM price_cursor INTO @product_id, @current_price, @stock
WHILE @@FETCH_STATUS = 0
BEGIN
UPDATE Products SET Price = @current_price * 1.1 WHERE ProductID = @product_id
FETCH NEXT FROM price_cursor INTO @product_id, @current_price, @stock
END
CLOSE price_cursor
DEALLOCATE price_cursor
В этом примере мы обновляем цены только для тех товаров, где остаток меньше 100. После каждого FETCH проверяем @@FETCH_STATUS. Если он не равен 0, цикл завершается.
Пример 2: Генерация отчета с накопительным итогом

Сценарий: для каждого клиента вычислить сумму заказов и нарастающий итог.
DECLARE @customer_id INT, @order_amount DECIMAL(10,2), @running_total DECIMAL(10,2) = 0
DECLARE order_cursor CURSOR FOR
SELECT CustomerID, Amount FROM Orders ORDER BY CustomerID, OrderDate
OPEN order_cursor
FETCH NEXT FROM order_cursor INTO @customer_id, @order_amount
WHILE @@FETCH_STATUS = 0
BEGIN
SET @running_total = @running_total + @order_amount
PRINT 'Customer ' + CAST(@customer_id AS VARCHAR) + ': Amount ' + CAST(@order_amount AS VARCHAR) + ', Running Total ' + CAST(@running_total AS VARCHAR)
FETCH NEXT FROM order_cursor INTO @customer_id, @order_amount
END
CLOSE order_cursor
DEALLOCATE order_cursor
Этот пример показывает, как можно использовать курсор для вычисления накопительного итога. Обратите внимание, что мы сортируем данные по CustomerID и OrderDate, чтобы итог был корректным.
«Курсоры — это как ручная коробка передач: дают полный контроль, но требуют навыка и внимания. Если вы не уверены — лучше используйте автомат (set-based).»
Пример 3: Массовая рассылка уведомлений
Сценарий: для каждой строки из таблицы пользователей вызвать хранимую процедуру отправки email.
DECLARE @user_id INT, @email VARCHAR(255)
DECLARE user_cursor CURSOR FOR
SELECT UserID, Email FROM Users WHERE IsActive = 1
OPEN user_cursor
FETCH NEXT FROM user_cursor INTO @user_id, @email
WHILE @@FETCH_STATUS = 0
BEGIN
BEGIN TRY
EXEC SendEmail @user_id, @email
END TRY
BEGIN CATCH
PRINT 'Error sending email to user ' + CAST(@user_id AS VARCHAR) + ': ' + ERROR_MESSAGE()
END CATCH
FETCH NEXT FROM user_cursor INTO @user_id, @email
END
CLOSE user_cursor
DEALLOCATE user_cursor
Здесь мы используем TRY…CATCH, чтобы при ошибке отправки одного email продолжить обработку остальных пользователей. Это критически важно для массовых операций.
Обработка ошибок и лучшие практики
Типичные ошибки при работе с курсорами: бесконечный цикл, потеря данных при сбое, блокировки. Рассмотрим, как их избежать.
Частая ошибка: Никогда не забывайте про EXIT WHEN NOTFOUND или WHILE @@FETCH_STATUS = 0, иначе цикл станет бесконечным.
Типичные ошибки и как их избежать
- Бесконечный цикл: всегда проверяйте @@FETCH_STATUS после FETCH.
- Незакрытый курсор: используйте CLOSE и DEALLOCATE в блоке TRY…CATCH или в конце процедуры.
- Блокировки: минимизируйте время жизни курсора, используйте READ UNCOMMITTED для отчётов.
Рекомендации по производительности
- Используйте FORWARD_ONLY, если не нужна прокрутка.
- Минимизируйте количество строк: используйте WHERE, чтобы отфильтровать данные.
- Избегайте курсоров в триггерах — это может привести к блокировкам.
- Предпочитайте BULK COLLECT в Oracle или пакетную обработку.
| Проблема | Решение |
|---|---|
| Бесконечный цикл | Проверять @@FETCH_STATUS после каждого FETCH |
| Блокировки | Использовать READ UNCOMMITTED или NOLOCK |
| Низкая производительность | Использовать FORWARD_ONLY, уменьшать набор данных |
Обработка исключений в цикле курсора

Используйте TRY…CATCH внутри цикла, чтобы при ошибке продолжить обработку следующих строк. Пример с логированием ошибок в таблицу ErrorLog.
BEGIN TRY
-- код цикла
END TRY
BEGIN CATCH
INSERT INTO ErrorLog (ErrorMessage, ErrorDate) VALUES (ERROR_MESSAGE(), GETDATE())
END CATCH
Сравнение курсоров в различных СУБД
Синтаксис курсоров сильно различается между СУБД. При миграции кода обязательно адаптируйте.
Совет: Синтаксис курсоров сильно различается между СУБД. При миграции кода обязательно адаптируйте.
Курсоры в SQL Server
Синтаксис: DECLARE cursor_name CURSOR [LOCAL | GLOBAL] [FORWARD_ONLY | SCROLL] [STATIC | KEYSET | DYNAMIC | FAST_FORWARD] FOR SELECT .... FAST_FORWARD — оптимизированный FORWARD_ONLY. Пример: DECLARE cur CURSOR FAST_FORWARD FOR SELECT ....
Курсоры в Oracle
В Oracle есть явные (объявленные) и неявные курсоры (для SELECT INTO). REF CURSOR — мощный инструмент для передачи курсора между процедурами. Пример: OPEN cursor_name FOR SELECT ....
Курсоры в PostgreSQL
PostgreSQL поддерживает DECLARE CURSOR, FETCH, CLOSE. Особенность: WITH HOLD позволяет сохранять курсор между транзакциями. Пример: DECLARE cur CURSOR WITH HOLD FOR SELECT ....
Курсоры в MySQL

В MySQL курсоры доступны только в хранимых процедурах. Обязателен DECLARE CONTINUE HANDLER для обработки конца набора. Пример: DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;.
Заключение: когда курсор — ваш друг, а когда враг
Курсоры — мощный, но ресурсоёмкий инструмент. Перед использованием всегда оценивайте возможность set-based подхода. Профилируйте оба варианта.
Важно: Перед использованием курсора всегда оцените возможность set-based подхода. Профилируйте оба варианта.
«Хороший разработчик SQL — это тот, кто умеет обходиться без курсоров. Но великий — тот, кто знает, когда их всё-таки применить.»
Чек-лист выбора подхода
- Можно ли решить задачу одним UPDATE или DELETE?
- Будет ли курсор обрабатывать более 1000 строк? Если да, подумайте об альтернативах.
- Есть ли альтернативы: оконные функции, CTE, временные таблицы?
- Как часто выполняется код? Для ежедневных отчётов курсор может быть неприемлем.
Итоговые рекомендации
- Используйте курсоры только для сложной бизнес-логики, где set-based операции невозможны.
- Всегда закрывайте и освобождайте курсоры.
- Тестируйте на небольших данных перед запуском на production.
- Изучите альтернативы: Cursor: ИИ-инструмент для автоматизации кода в 2026 может предложить новые подходы к обработке данных.
Для углублённого изучения рекомендую ознакомиться с Как Cursor меняет подход к написанию программ и Обзор Cursor для разработчиков: AI-редактор кода.
Часто задаваемые вопросы
Что такое курсор в SQL простыми словами?

Курсор — это механизм, который позволяет обрабатывать строки результата запроса по одной. Это как итератор в программировании.
Когда курсор лучше, чем set-based запрос?
Когда нужно выполнить для каждой строки разные действия (вызов хранимой процедуры, сложные вычисления). Но всегда проверяйте, можно ли решить задачу без курсора.
Как избежать бесконечного цикла в курсоре?
Всегда проверяйте @@FETCH_STATUS (или аналог) после каждого FETCH. Если он не равен 0, выходите из цикла.
Какой тип курсора самый быстрый?
Forward-only курсор — самый быстрый, так как он не поддерживает прокрутку и не отслеживает изменения.
Можно ли использовать курсор в MySQL?
Да, но только в хранимых процедурах. Обязательно объявляйте обработчик NOT FOUND.