Если вы когда-нибудь писали SQL-запросы, то знаете, что обычно они возвращают набор строк — результат, который можно обработать целиком. Но что делать, когда нужно пройтись по каждой строке отдельно, выполнить сложные вычисления или обновить данные с проверками?
Для таких случаев придумали курсоры. Это механизм, который позволяет обрабатывать строки одну за другой, как если бы вы листали страницы книги.
В этой статье я расскажу, что такое курсор в базах данных, как он работает, когда его стоит использовать, а когда лучше поискать другой путь. Вы узнаете о жизненном цикле курсора, типах, примерах для разных СУБД и, главное, о том, как не навредить производительности.
Что такое курсор в базах данных?
Курсор — это указатель на текущую строку в результирующем наборе данных. Представьте, что у вас есть список студентов, и вам нужно каждому начислить стипендию с учётом его баллов. Обычный SELECT вернёт всех студентов, но для расчёта вам нужно обработать каждого по отдельности. Курсор как раз даёт такую возможность: вы открываете набор, переходите к первой строке, делаете что-то, потом ко второй и так далее.
Главное отличие курсора от обычного набора результатов (set-based) — это построчная обработка. В set-based подходах вы работаете со всем набором сразу: UPDATE обновляет все строки, удовлетворяющие условию, за одну операцию. Курсор же — это итерация, цикл, который может быть медленнее, но даёт больше контроля.
Важно: курсоры предназначены для сценариев, где необходима обработка каждой строки отдельно, но злоупотребление ими может привести к серьезным проблемам с производительностью.
Простая аналогия: курсор — это как закладка в книге. Вы можете двигаться по страницам (строкам) вперёд, назад, перепрыгивать на определённую страницу. Без курсора вы бы просто получили всю книгу целиком и читали её как сплошной текст.
Ключевые характеристики курсора
Чтобы понимать, как работает курсор, разберём его основные свойства:
- Позиционирование на строке — курсор указывает на конкретную строку в наборе. Вы можете перемещать этот указатель.
- Последовательная или произвольная итерация — можно двигаться только вперёд (FORWARD_ONLY) или в любом направлении (SCROLL).
- Обновление и удаление текущей строки — некоторые курсоры позволяют изменять или удалять строку, на которой находится курсор.
- Чувствительность к изменениям — статический курсор показывает данные на момент открытия, а динамический — видит изменения, сделанные другими пользователями.
Позиционирование на строке
Курсор всегда указывает на одну строку. Вы можете перемещаться между строками с помощью команд FETCH. Это как курсор в текстовом редакторе: вы ставите его на нужное место и работаете.
Последовательная или произвольная итерация
Если курсор однонаправленный, вы можете только переходить к следующей строке. Прокручиваемый курсор даёт больше свободы: можно вернуться к предыдущей строке, перейти к первой или последней.
Обновление и удаление текущей строки

Курсоры, объявленные с опцией FOR UPDATE, позволяют изменять текущую строку. Это удобно, когда нужно обновить данные с проверками.
Чувствительность к изменениям (статический vs динамический)
Статический курсор — это снимок данных на момент открытия. Он не видит изменений, которые произошли после. Динамический курсор, наоборот, отражает все изменения. Выбор зависит от задачи: если данные меняются часто, динамический курсор даёт актуальную картину, но требует больше ресурсов.
Как работает курсор: жизненный цикл
Работа с курсором состоит из нескольких этапов. Пропуск хотя бы одного — частая ошибка, которая ведёт к проблемам. Давайте разберём каждый шаг.
| Этап | Команда | Описание |
|---|---|---|
| Объявление | DECLARE CURSOR | Определение курсора: задаётся запрос и параметры. |
| Открытие | OPEN | Выполнение запроса и формирование результирующего набора. |
| Извлечение | FETCH | Чтение текущей строки и перемещение указателя. |
| Закрытие | CLOSE | Освобождение результирующего набора (но не определения курсора). |
| Освобождение | DEALLOCATE | Удаление определения курсора и освобождение памяти. |
Частая ошибка: забывать закрывать и освобождать курсор — распространенная ошибка, приводящая к утечкам памяти и блокировкам.
Этап 1: Объявление курсора
На этом этапе вы говорите базе данных: «Я хочу создать курсор для такого-то запроса». Синтаксис в T-SQL выглядит так:
DECLARE cursor_name CURSOR FOR SELECT ...
Вы также можете указать тип курсора и его поведение. Например, SCROLL для прокрутки, FOR UPDATE для возможности изменять строки.
Синтаксис DECLARE CURSOR
В разных СУБД синтаксис может отличаться, но общая идея одинакова. В SQL Server: DECLARE cur CURSOR FOR SELECT ID, Name FROM Users. В PostgreSQL: DECLARE cur CURSOR FOR SELECT * FROM Users.
Параметры: FORWARD_ONLY, SCROLL

FORWARD_ONLY — курсор может двигаться только вперёд. SCROLL — можно перемещаться в любом направлении. SCROLL требует больше памяти, так как база данных хранит все строки.
Параметры: STATIC, DYNAMIC, KEYSET, FAST_FORWARD
STATIC — снимок данных, DYNAMIC — отражает изменения, KEYSET — фиксированный набор ключей, но данные подгружаются. FAST_FORWARD — оптимизированный вариант для последовательного чтения.
Этап 2: Открытие курсора
Команда OPEN выполняет запрос и формирует результирующий набор. Если курсор статический, данные копируются во временную таблицу. Это может быть затратно по памяти и времени.
Синтаксис OPEN
Просто: OPEN cursor_name. После этого курсор готов к извлечению данных.
Формирование результирующего набора
База данных выполняет SELECT, который вы указали при объявлении. Если запрос сложный, открытие может занять время.
Влияние на производительность

Открытие курсора может создавать блокировки, особенно если курсор объявлен с FOR UPDATE. Старайтесь минимизировать количество строк в курсоре.
Этап 3: Извлечение данных
Теперь самое интересное — мы начинаем читать строки. Команда FETCH извлекает текущую строку и перемещает указатель. Варианты: NEXT, PRIOR, FIRST, LAST, ABSOLUTE n, RELATIVE n.
Синтаксис FETCH
Пример: FETCH NEXT FROM cursor_name INTO @var1, @var2. Вы можете извлечь данные в переменные и обработать их.
Варианты позиционирования
NEXT — следующая строка, PRIOR — предыдущая, FIRST — первая, LAST — последняя. ABSOLUTE 5 — пятая строка, RELATIVE -2 — на две строки назад.
Обработка в цикле (WHILE @@FETCH_STATUS = 0)
Обычно используется цикл WHILE, который выполняется, пока FETCH возвращает успешный статус. В T-SQL это @@FETCH_STATUS = 0.
Этап 4: Закрытие и освобождение

После обработки нужно закрыть курсор (CLOSE) и освободить его (DEALLOCATE). CLOSE освобождает результирующий набор, но определение курсора остаётся. DEALLOCATE удаляет его полностью.
Синтаксис CLOSE
CLOSE cursor_name — освобождает ресурсы, связанные с набором данных.
Синтаксис DEALLOCATE
DEALLOCATE cursor_name — удаляет курсор. После этого его нельзя использовать снова.
Последствия пропуска
Если не закрыть курсор, он будет занимать память и может блокировать другие операции. В SQL Server это может привести к ошибкам блокировок.
Типы курсоров в SQL
Курсоры классифицируют по разным признакам. Давайте разберём основные типы.
| Критерий | Тип | Описание |
|---|---|---|
| По расположению | Серверные | Обрабатываются на стороне сервера БД. |
| Клиентские | Обрабатываются на стороне приложения. | |
| По поведению | Статические | Снимок данных на момент открытия. |
| Динамические | Отражают все изменения. | |
| Keyset | Фиксированный набор ключей, данные обновляются. | |
| Fast_Forward | Оптимизирован для последовательного чтения. |
Важно: выбор типа курсора напрямую влияет на нагрузку на сервер и согласованность данных.
По расположению: серверные и клиентские

Серверные курсоры обрабатываются на стороне базы данных. Они могут создавать блокировки, но обеспечивают согласованность. Клиентские курсоры загружают данные на сторону приложения, что снижает нагрузку на сервер, но увеличивает трафик.
Серверные курсоры (блокировки, нагрузка, согласованность)
Серверные курсоры хороши, когда нужно гарантировать, что данные не изменятся во время обработки. Но они могут блокировать строки, что снижает производительность.
Клиентские курсоры (гибкость, загрузка сети)
Клиентские курсоры более гибкие: вы можете обрабатывать данные на клиенте, не блокируя сервер. Но если данных много, сеть может быть перегружена.
По поведению: статические, динамические, keyset
Статический курсор — это снимок данных. Он быстрый, но не видит изменений. Динамический — актуальный, но медленный. Keyset — компромисс: ключи фиксируются, а данные подгружаются.
Static (INSENSITIVE): производительность, устаревшие данные
Статический курсор хорош для отчётов, где важна консистентность данных на момент запроса. Но если данные часто меняются, он может показывать устаревшую информацию.
Dynamic: актуальность, высокая нагрузка

Динамический курсор видит все изменения, но требует постоянной проверки. Это нагружает сервер.
Keyset: компромисс
Keyset-курсор фиксирует набор ключей, но значения подгружаются при каждом FETCH. Это быстрее динамического, но медленнее статического.
Fast_Forward: оптимизированный для последовательного чтения
Fast_Forward — это комбинация FORWARD_ONLY и READ_ONLY. Он самый быстрый для простого чтения.
Примеры использования курсоров: синтаксис в популярных СУБД
Теперь давайте посмотрим, как курсоры выглядят на практике в разных базах данных.
Совет: синтаксис и возможности курсоров могут отличаться в разных СУБД, всегда сверяйтесь с документацией.
Пример в T-SQL (SQL Server)
Предположим, нужно повысить зарплату сотрудникам на 10%, но только тем, кто работает больше года. С курсором это выглядит так:
DECLARE @id INT, @salary DECIMAL
DECLARE cur CURSOR FOR SELECT EmployeeID, Salary FROM Employees WHERE HireDate < DATEADD(YEAR, -1, GETDATE())
OPEN cur
FETCH NEXT FROM cur INTO @id, @salary
WHILE @@FETCH_STATUS = 0
BEGIN
UPDATE Employees SET Salary = @salary * 1.1 WHERE EmployeeID = @id
FETCH NEXT FROM cur INTO @id, @salary
END
CLOSE cur
DEALLOCATE cur
Объявление переменных

Сначала объявляем переменные для хранения данных из курсора.
Цикл обработки
Цикл WHILE выполняется, пока FETCH успешен. Внутри цикла — UPDATE.
Обработка ошибок
В реальном коде стоит добавить TRY…CATCH, чтобы обработать возможные ошибки.
Пример в PL/SQL (Oracle)
В Oracle часто используют цикл FOR, который автоматически открывает, извлекает и закрывает курсор:
DECLARE
CURSOR cur IS SELECT EmployeeID, Salary FROM Employees WHERE HireDate < ADD_MONTHS(SYSDATE, -12);
BEGIN
FOR rec IN cur LOOP
UPDATE Employees SET Salary = rec.Salary * 1.1 WHERE EmployeeID = rec.EmployeeID;
END LOOP;
END;
Объявление курсора в секции DECLARE
Курсор объявляется в секции DECLARE.
Цикл FOR record IN cursor

Цикл FOR сам открывает курсор, извлекает строки и закрывает его.
Обработка исключений
Можно добавить блок EXCEPTION для обработки ошибок.
Пример в MySQL
В MySQL курсоры используются только в хранимых процедурах. Пример:
DELIMITER //
CREATE PROCEDURE IncreaseSalary()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE id INT;
DECLARE salary DECIMAL(10,2);
DECLARE cur CURSOR FOR SELECT EmployeeID, Salary FROM Employees WHERE HireDate < DATE_SUB(NOW(), INTERVAL 1 YEAR);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO id, salary;
IF done THEN
LEAVE read_loop;
END IF;
UPDATE Employees SET Salary = salary * 1.1 WHERE EmployeeID = id;
END LOOP;
CLOSE cur;
END //
DELIMITER ;
DECLARE CURSOR
Курсор объявляется внутри процедуры.
DECLARE CONTINUE HANDLER
Обработчик для конца набора данных.
Цикл с FETCH

Цикл LOOP с условием выхода.
Пример в PostgreSQL
В PostgreSQL курсоры можно использовать в транзакциях. Пример:
BEGIN;
DECLARE cur CURSOR FOR SELECT EmployeeID, Salary FROM Employees WHERE HireDate < NOW() - INTERVAL '1 year';
FETCH NEXT FROM cur;
-- Обработка
CLOSE cur;
COMMIT;
Объявление курсора
DECLARE внутри транзакции.
FETCH NEXT
Извлечение следующей строки.
Закрытие курсора
CLOSE обязателен.
Когда нужно, а когда не нужно использовать курсоры

Курсоры — мощный инструмент, но их часто используют там, где можно обойтись без них. Давайте разберём, когда они оправданы, а когда — вредны.
Важно: всегда сначала пытайтесь решить задачу с помощью set-based операций (UPDATE, JOIN, подзапросы, оконные функции). Курсоры используйте только если это действительно необходимо.
Когда курсоры оправданы
- Сложные вычисления на каждой строке — например, расчёт налогов с учётом множества условий.
- Вызов хранимых процедур для каждой строки — если нужно выполнить процедуру, которая не может работать с набором.
- Обработка данных из разных таблиц с зависимостями — когда нужно проверить данные в нескольких таблицах перед обновлением.
- Реализация бизнес-логики, не выражаемой в SQL — например, сложные алгоритмы, которые проще написать на процедурном языке.
Построчная валидация
Если нужно проверить каждую строку на соответствие сложным правилам, курсор может быть удобен.
Генерация сложных отчетов с форматированием
Иногда отчёты требуют построчного форматирования, которое сложно сделать в SQL.
Каскадные обновления с проверками
Когда обновление одной таблицы зависит от данных в другой, курсор может помочь.
Когда курсоры вредны

- Массовые обновления — UPDATE без курсора работает в разы быстрее.
- Выборка данных для отчета — SELECT с GROUP BY и ORDER BY справится лучше.
- Агрегация и группировка — оконные функции и GROUP BY — ваши друзья.
«Курсор — это как молоток: если у вас есть только молоток, всё вокруг кажется гвоздями. Но в SQL гвозди часто забиваются set-based инструментами.»
Массовые обновления
Пример: обновить зарплату всем сотрудникам на 10% — это одна команда UPDATE, а не курсор.
Выборка данных для отчета
Отчёт можно сформировать одним SELECT с JOIN и агрегацией.
Агрегация и группировка
SUM, COUNT, AVG работают с наборами, а не со строками.
Производительность курсоров: как оптимизировать
Курсоры могут быть медленными, но есть способы ускорить их работу.
Совет: профилируйте запросы с курсорами с помощью инструментов (например, SQL Server Profiler, EXPLAIN ANALYZE).
Советы по оптимизации

- Используйте FAST_FORWARD — если не нужна прокрутка, это самый быстрый тип.
- Ограничьте количество строк — используйте TOP или WHERE, чтобы уменьшить набор.
- Используйте READ ONLY — если не нужно обновлять данные, это снижает блокировки.
- Закрывайте курсор сразу после обработки — не держите его открытым дольше, чем нужно.
- Рассмотрите временные таблицы — иногда быстрее загрузить данные во временную таблицу и обработать их.
Выбор оптимального типа курсора
Если вам нужно только читать данные вперёд, FAST_FORWARD — лучший выбор.
Минимизация блокировок
Используйте READ ONLY и избегайте FOR UPDATE, если это возможно.
Использование индексов
Убедитесь, что запрос курсора использует индексы. Это ускорит открытие.
Пакетная обработка (BULK COLLECT, пакетные обновления)
В Oracle можно использовать BULK COLLECT для загрузки данных в коллекцию, а затем обработать их пакетно.
Сравнение производительности: курсор vs set-based

Гипотетический пример: обновление 10 000 строк. Set-based UPDATE выполняется за доли секунды, курсор — несколько секунд или даже минут. Причина: курсор выполняет много операций (открытие, FETCH, проверка статуса), а set-based — одну.
Пример SQL-запроса set-based
UPDATE Employees SET Salary = Salary * 1.1 WHERE HireDate < '2020-01-01'
Пример с курсором
Как в примере выше: цикл, FETCH, UPDATE.
Оценка времени выполнения
Set-based может быть в 10-100 раз быстрее. Всегда тестируйте.
Альтернативы курсорам в SQL
Современные СУБД предлагают мощные альтернативы, которые часто быстрее и проще.
Важно: современные СУБД предоставляют мощные set-based инструменты, которые часто могут заменить курсор и работают гораздо быстрее.
Set-based операции

UPDATE, JOIN, подзапросы и агрегатные функции могут решить многие задачи. Например, обновление с условием — это один UPDATE.
Массовое обновление через UPDATE с JOIN
UPDATE t1 SET t1.value = t2.value FROM Table1 t1 JOIN Table2 t2 ON t1.id = t2.id
Агрегация через GROUP BY и оконные функции
GROUP BY для сумм, оконные функции для ранжирования.
Фильтрация через подзапросы
Подзапросы могут заменить курсор при выборе данных.
Оконные функции
ROW_NUMBER(), RANK(), LEAD(), LAG() позволяют выполнять построчные вычисления без курсора. Например, нумерация строк:
SELECT ROW_NUMBER() OVER (ORDER BY ID) AS RowNum, Name FROM Users
ROW_NUMBER() для нумерации

Просто и быстро.
LAG/LEAD для доступа к предыдущей/следующей строке
LAG позволяет получить значение из предыдущей строки — это часто заменяет курсор.
SUM() OVER() для скользящих сумм
Скользящие суммы без курсора.
Рекурсивные CTE
WITH RECURSIVE позволяет обходить иерархические данные, такие как деревья категорий.
Синтаксис WITH RECURSIVE
WITH RECURSIVE cte AS (SELECT ... UNION ALL SELECT ... FROM cte WHERE ...)
Пример: обход дерева категорий

Можно получить все подкатегории без курсора.
Пример: генерация последовательностей
Генерация чисел или дат.
BULK COLLECT (Oracle) и пакетная обработка
BULK COLLECT загружает данные в коллекцию, что уменьшает количество переключений контекста между SQL и PL/SQL.
Синтаксис BULK COLLECT INTO
SELECT ... BULK COLLECT INTO collection
Цикл FORALL
FORALL выполняет массовые операции над коллекцией.
Сравнение с обычным курсором

BULK COLLECT может быть в разы быстрее.
Частые ошибки при работе с курсорами
Даже опытные разработчики иногда допускают ошибки. Вот самые распространённые.
Частая ошибка: автоматизируйте закрытие курсоров через обертки (в хранимых процедурах) или используйте циклы FOR, которые делают это автоматически.
Забыл закрыть/освободить курсор
Это ведёт к утечке памяти и блокировкам. Всегда используйте блоки TRY…CATCH или обертки.
Пример с утечкой
Курсор открыт, но не закрыт — память занята.
Правильный подход
Закрывайте в блоке FINALLY или используйте циклы FOR.
Использование курсора для массовых операций

Вместо UPDATE с курсором используйте один UPDATE.
Плохой пример с курсором
Цикл по всем строкам для обновления.
Хороший пример с UPDATE
Одна команда.
Неправильный тип курсора
Использование SCROLL или DYNAMIC, когда достаточно FAST_FORWARD, увеличивает нагрузку.
Выбор FAST_FORWARD
Для простого чтения — лучший выбор.
Избегайте SCROLL без необходимости

SCROLL требует памяти.
Заключение: курсор как инструмент
Курсоры — мощный, но ресурсоёмкий инструмент. Их стоит использовать только когда set-based решения невозможны. Всегда ищите альтернативы и тщательно тестируйте производительность. Изучайте оконные функции и CTE — они часто решают задачу без курсора.
«Современная разработка на SQL стремится к set-based мышлению. Курсор — это исключение, а не правило.»
Если вы хотите глубже изучить тему, рекомендую прочитать о автоматизации тестирования с помощью плагинов и CI/CD-интеграций, где обсуждаются смежные аспекты работы с базами данных.
Часто задаваемые вопросы
Что такое курсор в SQL простыми словами?
Курсор — это указатель на строку в наборе данных. Он позволяет обрабатывать строки по одной, как если бы вы листали список.
Когда следует использовать курсоры?
Когда нужно выполнить сложную логику для каждой строки, которую нельзя выразить set-based операциями.
Какие есть альтернативы курсорам?

Set-based операции (UPDATE, JOIN), оконные функции, рекурсивные CTE, BULK COLLECT.
Почему курсоры медленные?
Потому что они выполняют много операций: открытие, FETCH, проверка статуса, закрытие. Set-based операции выполняются за один раз.
Какой тип курсора самый быстрый?
FAST_FORWARD — он оптимизирован для последовательного чтения без прокрутки.
Нужно ли закрывать курсор?
Да, обязательно. Иначе будут утечки памяти и блокировки.
Можно ли обновлять данные через курсор?
Да, если курсор объявлен с опцией FOR UPDATE.