Как практик, работающий с SQL-решениями в продуктовых командах и на проектах миграции данных, я регулярно сталкиваюсь с ситуацией, когда разработчик или аналитик впервые открывает для себя курсоры — и либо влюбляется в них за гибкость, либо проклинает за тормоза. Опираясь на опыт внедрения подобных механизмов в высоконагруженных системах, разберу, что такое курсор на самом деле, как с ним работать без боли и, главное, когда его использование — осознанный выбор, а не ошибка новичка.
Эта статья — не пересказ документации. Это взгляд инженера, который не раз видел, как один неудачный курсор валит прод, а другой — спасает задачу, нерешаемую set-based методами. Вы узнаете, как объявлять, открывать, извлекать и закрывать курсор, чем отличаются типы курсоров, как их оптимизировать и — что важнее — когда от них стоит отказаться в пользу альтернатив.
Cursor: что это такое в базах данных
Курсор — это объект базы данных, который позволяет приложению или хранимой процедуре обрабатывать результаты запроса построчно, а не набором целиком. Если обычный SELECT возвращает вам таблицу, то курсор даёт указатель на конкретную строку и возможность двигаться по строкам по одной — вперёд, назад, прыгать к началу или концу.
- Аналогия: представьте, что вы читаете книгу. SELECT — это получить сразу все страницы. Курсор — читать по одной строке, иногда возвращаясь назад.
- Зачем это нужно? SQL по своей природе — язык работы с наборами. Но бизнес-логика часто требует пошаговых действий: для каждой строки вызвать внешний API, проверить сложное условие, накопить промежуточный результат.
Важно: Курсоры потребляют ресурсы и могут снижать производительность, поэтому их следует использовать только когда set-based решения невозможны.
Определение курсора и его роль в SQL
Курсор — это область памяти, в которую СУБД помещает результирующий набор запроса, и указатель на текущую строку. Вы управляете этим указателем через команды FETCH. Курсор живёт в контексте сессии или хранимой процедуры.
Курсор как указатель на строку
Когда вы объявляете курсор, вы связываете его с SELECT-запросом. После OPEN курсор «запоминает» все строки (или только ключи, в зависимости от типа), и вы можете по одной их извлекать.
Отличие курсора от обычного SELECT
SELECT возвращает вам весь набор сразу — вы либо получаете его в приложении, либо используете в подзапросе. Курсор же даёт контроль над итерацией: вы сами решаете, когда взять следующую строку, когда остановиться, а когда — изменить текущую строку через UPDATE WHERE CURRENT OF.
Типовые сценарии использования курсоров

- Построчная валидация с вызовом хранимых процедур или внешних сервисов.
- Миграция данных, когда нужно преобразовать каждую запись по сложному алгоритму.
- Генерация отчётов с накоплением промежуточных итогов (хотя оконные функции часто решают это лучше).
- Обработка иерархий, где рекурсивный CTE неудобен.
Основные этапы работы с курсором
Жизненный цикл курсора состоит из пяти шагов. Пропуск любого из них ведёт к утечке ресурсов или ошибкам.
DECLARE CURSOR: синтаксис и параметры
Объявляете курсор, указывая SELECT-запрос и опции (тип, прокрутка, обновление). Пример на T-SQL:
DECLARE cur_orders CURSOR FOR
SELECT OrderID, Total FROM Orders WHERE Status = 'Pending'; OPEN CURSOR: выполнение запроса
Команда OPEN выполняет запрос и формирует результирующий набор. С этого момента курсор начинает потреблять ресурсы (память, блокировки).
FETCH NEXT: извлечение строки
FETCH NEXT перемещает указатель на следующую строку и помещает значения в переменные. После каждого FETCH проверяйте статус (например, @@FETCH_STATUS = 0 — успех).
CLOSE и DEALLOCATE: очистка ресурсов

CLOSE освобождает результирующий набор, но сам курсор остаётся. DEALLOCATE удаляет курсор полностью. Забытый DEALLOCATE — одна из самых частых причин утечек памяти в хранимых процедурах.
Типы курсоров в SQL
Разные СУБД предлагают разные типы курсоров. От выбора типа зависит, увидите ли вы изменения данных, сделанные другими сессиями, сможете ли перемещаться назад и насколько быстро будет работать курсор.
| Тип курсора | Видит изменения? | Прокрутка | Производительность |
|---|---|---|---|
| Статический (STATIC) | Нет (снимок на момент OPEN) | Полная | Высокая (данные в tempdb) |
| Динамический (DYNAMIC) | Да | Полная | Низкая (частые блокировки) |
| Keyset (KEYSET) | Изменения строк — да, добавление/удаление — нет | Полная | Средняя |
| Fast_Forward (FAST_FORWARD) | Нет (только вперёд, только чтение) | Только NEXT | Максимальная |
Важно: Выбор типа курсора напрямую влияет на производительность и корректность работы. Например, статические курсоры быстрее, но не видят изменений, а динамические — наоборот.
Статические и динамические курсоры
Статический курсор копирует все строки во временную таблицу (в tempdb) в момент OPEN. Он не видит вставок, удалений или обновлений, сделанных другими сессиями после открытия. Идеален для отчётов, где нужна консистентность снимка.
Динамический курсор, наоборот, каждый раз при FETCH проверяет актуальное состояние строки. Это дорого: СУБД вынуждена держать блокировки или использовать версионность. Подходит для задач, где критична свежесть данных (например, обработка очереди заказов).
Статический курсор: преимущества и недостатки
- Плюсы: высокая скорость чтения, отсутствие блокировок на исходных таблицах.
- Минусы: устаревшие данные, большой расход tempdb.
Динамический курсор: когда нужна актуальность данных
- Плюсы: видит все изменения, включая вставки и удаления.
- Минусы: медленный, может блокировать строки.
Прокручиваемые и однонаправленные курсоры

Однонаправленный курсор (FAST_FORWARD, FORWARD_ONLY) позволяет двигаться только вперёд. Это самый быстрый тип, так как СУБД не нужно хранить позиции для возврата.
Прокручиваемый курсор (SCROLL) даёт команды FETCH PRIOR, FIRST, LAST, ABSOLUTE n, RELATIVE n. Он требует больше памяти, так как СУБД хранит все строки или ключи.
Совет: Если вам не нужна прокрутка назад, всегда используйте FAST_FORWARD. Это повышает производительность в разы.
Однонаправленный курсор (FAST_FORWARD)
Оптимален для простых циклов: прочитать строку, обработать, перейти к следующей. Не поддерживает UPDATE WHERE CURRENT OF (в некоторых СУБД).
Полностью прокручиваемый курсор (SCROLL)
Нужен для навигации по набору: например, интерфейс постраничного просмотра, где пользователь может вернуться к предыдущей странице.
Курсоры только для чтения и для обновления
Курсор READ_ONLY не позволяет модифицировать данные через него. Курсор FOR UPDATE (или с ключевым словом FOR UPDATE OF) даёт возможность использовать UPDATE WHERE CURRENT OF, что удобно для массовых корректировок с дополнительной логикой.
Курсор READ_ONLY
Безопасен, быстр, не накладывает блокировок на обновление. Используйте, если не планируете менять строки.
Курсор FOR UPDATE

Позволяет обновлять текущую строку. Пример: пересчёт бонусов для каждого клиента с проверкой лимитов.
Как использовать курсор: полный пример работы с базой данных
Рассмотрим сценарий: у нас есть таблица Orders с заказами, и нужно применить скидку 10% для всех заказов старше 30 дней, но только если сумма заказа меньше 1000. Каждый заказ обрабатывается индивидуально, так как скидка может быть скорректирована вручную (сложная бизнес-логика).
Важно: Всегда проверяйте @@FETCH_STATUS (или аналог) для корректного завершения цикла, чтобы избежать бесконечного цикла.
Пример курсора в T-SQL (SQL Server)
DECLARE @OrderID INT, @Total DECIMAL(10,2), @NewTotal DECIMAL(10,2)
DECLARE cur_discount CURSOR FAST_FORWARD READ_ONLY FOR
SELECT OrderID, Total FROM Orders WHERE OrderDate < DATEADD(DAY, -30, GETDATE()) AND Total < 1000
OPEN cur_discount
FETCH NEXT FROM cur_discount INTO @OrderID, @Total
WHILE @@FETCH_STATUS = 0
BEGIN
SET @NewTotal = @Total * 0.9
UPDATE Orders SET Total = @NewTotal WHERE OrderID = @OrderID
FETCH NEXT FROM cur_discount INTO @OrderID, @Total
END
CLOSE cur_discount
DEALLOCATE cur_discount
Объявление переменных
Создаём переменные для хранения значений текущей строки.
Объявление курсора с SELECT
Указываем FAST_FORWARD и READ_ONLY для максимальной производительности.
Открытие и первый FETCH

OPEN выполняет запрос, FETCH берёт первую строку.
Цикл WHILE с проверкой @@FETCH_STATUS
Пока FETCH успешен — обрабатываем строку и переходим к следующей.
Закрытие и освобождение ресурсов
CLOSE и DEALLOCATE обязательны даже при ошибке. Используйте TRY…CATCH для гарантии.
Пример курсора в MySQL (хранимая процедура)
В MySQL синтаксис отличается: нужно объявить CONTINUE HANDLER для NOT FOUND.
DELIMITER //
CREATE PROCEDURE apply_discount()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE v_order_id INT;
DECLARE v_total DECIMAL(10,2);
DECLARE cur CURSOR FOR SELECT OrderID, Total FROM Orders WHERE OrderDate < DATE_SUB(NOW(), INTERVAL 30 DAY) AND Total < 1000;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO v_order_id, v_total;
IF done THEN
LEAVE read_loop;
END IF;
UPDATE Orders SET Total = v_total * 0.9 WHERE OrderID = v_order_id;
END LOOP;
CLOSE cur;
END //
DELIMITER ;
Объявление переменных и курсора
Аналогично T-SQL, но с DECLARE CONTINUE HANDLER.
Обработчик NOT FOUND

Устанавливает флаг done, когда строки заканчиваются.
Цикл LOOP с FETCH
Выход из цикла через LEAVE при done = TRUE.
Закрытие курсора
Обязательно после обработки.
Когда использовать курсор, а когда лучше избегать
Курсор — это молоток, но не каждый гвоздь — задача для курсора. Я выделяю три сценария, где курсор оправдан, и три, где он — чистое зло.
Важно: В большинстве случаев set-based операции работают быстрее и эффективнее курсоров. Прежде чем использовать курсор, подумайте, можно ли решить задачу одним UPDATE или SELECT.
Сценарии, где курсор уместен
- Построчная валидация с вызовом внешних сервисов: например, проверка адреса через API геокодера для каждой записи.
- Обработка иерархических данных: когда рекурсивный CTE неудобен из-за сложной логики пересчёта.
- Миграция данных с преобразованием: перенос из старой схемы в новую с зависимостью от предыдущих строк.
«В одном проекте миграции CRM мы использовали курсор, чтобы для каждого клиента пересчитать бонусный баланс с учётом истории транзакций — set-based запрос с оконными функциями оказался в 10 раз быстрее, но требовал полной перестройки логики. Курсор был компромиссом для сжатых сроков.» — из опыта автора.
Построчная валидация с вызовом API

Если нужно для каждой строки вызвать внешний сервис (например, проверка email через сторонний API), курсор — единственный вариант.
Обработка деревьев и графов
Хотя рекурсивные CTE существуют, иногда проще написать курсор для обхода дерева с дополнительными действиями на каждом узле.
Миграция данных с преобразованием
Когда строки зависят друг от друга (например, пересчёт остатков на складе), курсор может быть проще для понимания.
Сценарии, где курсор вреден
- Массовое обновление: один UPDATE с JOIN или CASE решит задачу за секунды вместо минут.
- Агрегация: GROUP BY, SUM, AVG — всё это делается без курсора.
- Постраничный вывод: используйте OFFSET/FETCH (или LIMIT/OFFSET).
Массовое обновление: замена на UPDATE с JOIN
Вместо курсора с UPDATE WHERE CURRENT OF напишите:
UPDATE o SET Total = o.Total * 0.9
FROM Orders o
JOIN SomeTable s ON o.OrderID = s.OrderID
WHERE o.Total < 1000;
Агрегация: замена на GROUP BY

Курсор для подсчёта суммы по группам — антипаттерн. Используйте GROUP BY.
Постраничный вывод: замена на OFFSET/FETCH
В SQL Server: ORDER BY … OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY.
Альтернативы курсорам в SQL
Прежде чем писать курсор, я всегда проверяю три альтернативы: set-based, оконные функции и рекурсивные CTE. Часто одна из них решает задачу быстрее и чище.
| Альтернатива | Когда использовать | Пример |
|---|---|---|
| Set-based (UPDATE, INSERT…SELECT) | Массовые изменения, фильтрация, соединения | UPDATE с JOIN |
| Оконные функции | Нумерация, накопительные суммы, сравнение строк | ROW_NUMBER, SUM OVER, LAG |
| Рекурсивный CTE | Иерархии, деревья, графы | WITH RECURSIVE |
| Временные таблицы / табличные переменные | Промежуточные результаты, пакетная обработка | SELECT INTO #temp |
Важно: Использование альтернатив часто не только ускоряет выполнение, но и упрощает код и его поддержку.
Set-based операции вместо курсора
Один UPDATE может заменить десятки строк кода с курсором. Пример: начисление бонуса всем клиентам с суммой покупок > 5000.
UPDATE Customers SET Bonus = 100 WHERE CustomerID IN (SELECT CustomerID FROM Orders GROUP BY CustomerID HAVING SUM(Total) > 5000); UPDATE с подзапросом
Быстро, атомарно, без блокировок на каждую строку.
INSERT…SELECT с агрегацией

Вставка отчётных данных сразу из SELECT с GROUP BY.
MERGE (upsert) вместо курсора
Синхронизация таблиц: MERGE позволяет вставить, обновить или удалить строки за один проход.
Оконные функции как замена курсору
Оконные функции дают доступ к соседним строкам без курсора. Например, ROW_NUMBER для нумерации, LAG для сравнения с предыдущей строкой.
ROW_NUMBER для нумерации
SELECT ROW_NUMBER() OVER (ORDER BY OrderDate) AS RowNum, … — замена курсору с FETCH PRIOR.
LAG и LEAD для доступа к соседним строкам
LAG(Total) OVER (ORDER BY OrderDate) даёт значение предыдущего заказа без курсора.
SUM OVER для накопительных сумм

SUM(Total) OVER (ORDER BY OrderDate ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) — скользящий итог.
Рекурсивные CTE и временные таблицы
Рекурсивный CTE — мощный инструмент для иерархий (например, дерево категорий). Временные таблицы — для разбиения задачи на этапы.
Рекурсивный CTE для иерархии
WITH RECURSIVE OrgTree AS (…) — обход структуры подчинения.
Временная таблица для пакетной обработки
SELECT … INTO #temp — обработать большие данные частями, не держа курсор открытым.
Табличная переменная для небольших наборов
DECLARE @temp TABLE (…) — для маленьких объёмов (до 1000 строк).
Производительность курсоров и оптимизация

Курсоры медленны не потому, что они «плохие», а потому что построчная обработка — это O(n) операций с накладными расходами на каждую строку: проверка статуса, блокировки, передача данных между клиентом и сервером.
Важно: Даже оптимизированный курсор обычно медленнее set-based подхода. Всегда тестируйте производительность на реальных данных.
Причины низкой производительности курсоров
- Блокировки и конкуренция: динамический курсор может удерживать блокировки на каждой строке.
- Число итераций и накладные расходы: каждая итерация — вызов FETCH, проверка статуса, работа с переменными.
- Отсутствие параллелизма: курсор выполняется последовательно, в одном потоке.
Блокировки и конкуренция
Динамический курсор без READ_ONLY может блокировать строки, вызывая взаимные блокировки (deadlocks).
Число итераций и накладные расходы
Для 10 000 строк курсор выполнит 10 000 FETCH — каждый с затратами на контекст.
Отсутствие параллелизма
Set-based запросы могут использовать параллельные планы, курсор — нет.
Рекомендации по оптимизации курсоров

- Используйте FAST_FORWARD и READ_ONLY — это самый быстрый тип.
- Ограничьте набор данных — чем меньше строк, тем быстрее.
- Выбирайте только нужные столбцы — не делайте SELECT *.
- Рассмотрите пакетную обработку — вместо одной строки за раз обрабатывайте по 100–1000 строк (BULK COLLECT в Oracle).
Использование FAST_FORWARD и READ_ONLY
Эти опции отключают прокрутку и блокировки, делая курсор максимально лёгким.
Ограничение набора данных
Добавьте WHERE, чтобы обрабатывать только нужные строки.
Пакетный FETCH (BULK COLLECT)
В Oracle: FETCH cur BULK COLLECT INTO … LIMIT 100 — сокращает число обращений к СУБД.
Частые ошибки при работе с курсорами и как их избежать
За годы я видел одни и те же грабли. Вот топ-3 ошибки, которые превращают курсор в источник проблем.
Частая ошибка: Не забывайте про CLOSE и DEALLOCATE — это предотвращает утечку ресурсов и блокировки.
Ошибка: бесконечный цикл

Если вы забыли FETCH NEXT внутри цикла или неправильно проверяете статус, цикл будет выполняться вечно.
«Однажды я видел, как курсор с пропущенным FETCH NEXT положил сервер за 15 минут — tempdb выросла до 50 ГБ.» — из практики.
Пропущенный FETCH внутри цикла
Всегда ставьте FETCH NEXT в конце тела цикла.
Неправильная проверка статуса
Используйте @@FETCH_STATUS = 0 (успех), а не 0.
Ошибка: неосвобожденные ресурсы
Если курсор не закрыт и не удалён, он продолжает занимать память и блокировки до конца сессии.
Забытый CLOSE и DEALLOCATE
CLOSE освобождает результирующий набор, а DEALLOCATE удаляет курсор. Забытый DEALLOCATE — одна из самых частых причин утечек памяти в хранимых процедурах.
Использование TRY…CATCH для гарантии

В T-SQL оборачивайте код в TRY…CATCH, а в CATCH закрывайте курсор.
Ошибка: модификация данных без учета транзакций
Если внутри курсора происходит несколько UPDATE, а в середине случается ошибка, часть строк будет обновлена, часть — нет.
Частичное обновление при ошибке
Используйте BEGIN TRAN и COMMIT/ROLLBACK.
Отсутствие BEGIN TRAN и COMMIT
Всегда оборачивайте цикл в транзакцию, если изменения должны быть атомарными.
Заключение: стоит ли использовать курсоры в современных базах данных
Курсоры — это инструмент, который требует осознанного подхода. Они не «зло» и не «панацея». В современных СУБД акцент смещён в сторону декларативных подходов, но курсоры остаются незаменимыми для определённых задач.
Совет: Помните, что производительность базы данных — приоритет. Используйте курсоры осознанно и только там, где это действительно необходимо.
Чек-лист перед использованием курсора

Прежде чем писать DECLARE CURSOR, задайте себе четыре вопроса:
- Можно ли решить задачу одним UPDATE или INSERT? Если да — не пишите курсор.
- Можно ли использовать оконную функцию (ROW_NUMBER, LAG, SUM OVER)? Если да — это будет быстрее.
- Можно ли использовать рекурсивный CTE? Если да — для иерархий это лучший выбор.
- Можно ли разбить на пакеты (chunking)? Если да — используйте временную таблицу и цикл по пакетам.
Если все ответы «нет» — курсор допустим, но выберите оптимальный тип (FAST_FORWARD, READ_ONLY) и не забудьте про CLOSE/DEALLOCATE.
Проверка на set-based альтернативы
Это первый и самый важный шаг.
Оценка объема данных
Если строк меньше 1000, курсор может быть приемлем. Если больше — ищите альтернативу.
Выбор оптимального типа курсора
FAST_FORWARD + READ_ONLY — ваш друг. SCROLL и DYNAMIC — только если без них никак.
Перспективы использования курсоров
Современные СУБД (SQL Server 2022, PostgreSQL 16, MySQL
продолжают поддерживать курсоры, но оптимизаторы всё лучше справляются с set-based запросами. Тренд — минимизация курсоров в пользу оконных функций и CTE. Однако в legacy-системах и при работе с внешними сервисами курсоры ещё долго будут актуальны.
Если вы хотите глубже разобраться в смежных темах, рекомендую прочитать про Cursor: ИИ-инструмент для автоматизации кода в 2026 — это про другой, но тоже полезный инструмент для разработчика. А также Как Cursor меняет подход к написанию программ и Обзор Cursor для разработчиков: AI-редактор кода.
Часто задаваемые вопросы

Что такое курсор в SQL простыми словами?
Курсор — это механизм, который позволяет обрабатывать результаты SQL-запроса по одной строке, а не всем набором сразу. Вы как будто «проходитесь» по строкам, читая или изменяя каждую отдельно.
В чём разница между курсором и обычным SELECT?
SELECT возвращает все строки сразу. Курсор даёт контроль над итерацией: вы сами решаете, когда взять следующую строку, можете вернуться назад (если курсор прокручиваемый) и изменять текущую строку.
Когда нельзя использовать курсор?
Избегайте курсоров при массовых обновлениях (используйте UPDATE), агрегации (GROUP BY), постраничном выводе (OFFSET/FETCH) и простых фильтрациях (WHERE). В большинстве случаев set-based операции быстрее.
Какой тип курсора самый быстрый?
FAST_FORWARD с READ_ONLY — самый быстрый. Он не поддерживает прокрутку назад и не позволяет изменять данные, но работает максимально эффективно.
Что будет, если не закрыть курсор?

Курсор продолжит занимать память и ресурсы (блокировки) до завершения сессии. Это может привести к утечкам памяти и взаимным блокировкам. Всегда выполняйте CLOSE и DEALLOCATE.