Cursor в действии: пошаговое руководство по созданию запросов и управлению данными

Как практик, работающий с базами данных более десяти лет, я часто сталкиваюсь с ситуациями, когда обычные SQL-запросы не справляются с задачей. Курсоры — это мощный, но коварный инструмент. В этой статье я разберу, как использовать курсоры для построчной обработки данных, когда это оправдано, а когда лучше искать альтернативу.

Содержания:

Вы узнаете, как избежать типичных ошибок, повысить производительность и принять взвешенное решение о применении курсоров в ваших проектах.

Введение в курсоры: зачем и когда использовать

Курсор в SQL — это механизм, позволяющий обрабатывать строки результирующего набора одну за другой. В отличие от set-based запросов, которые работают со всем набором данных сразу, курсор даёт контроль над каждой строкой. Это может быть критически важно для сложной бизнес-логики, но за это приходится платить производительностью.

Важно: Используйте курсоры только тогда, когда set-based операции невозможны или слишком сложны.

Что такое курсор в SQL

Определение курсора

Курсор — это указатель на результирующий набор запроса. Он позволяет последовательно «проходить» по строкам, извлекая их и выполняя действия. Представьте себе итератор в программировании: курсор — это аналог для SQL.

Отличие от set-based запросов

Set-based запросы (например, UPDATE с WHERE) обрабатывают все строки за одну операцию. Курсор же обрабатывает строки по одной, что может быть в десятки раз медленнее. Однако, когда нужно выполнить для каждой строки различные действия (вызов хранимой процедуры, сложные вычисления), курсор незаменим.

Когда оправдано применение курсора

рука рисует курсор SQL
  • Построчная логика: необходимо выполнить для каждой строки разные действия в зависимости от её содержимого.
  • Интеграция с внешними системами: для каждой строки нужно вызвать 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. Он не видит изменений, сделанных другими сессиями. Поддерживает все операции выборки. Идеален для отчётов, где важна консистентность.

Динамический курсор

весы курсора и set-based

Динамический курсор отражает все изменения (вставки, обновления, удаления) в реальном времени. Он медленный, потому что каждый 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.

Виталий/ автор статьи

Руководитель проектов, эксперт по веб-разработке В коммерческой веб-разработке с 2018 года. Специализируюсь на создании цифровых продуктов, которые решают задачи бизнеса: увеличивают конверсию, автоматизируют продажи и масштабируют трафик. За плечами - управление портфелем из 150+ медиапроектов, что дало глубокое понимание механик поискового продвижения и работы с большими объемами данных. Этот опыт я трансформировал в системный подход к созданию коммерческих сайтов: каждый этап разработки - от прототипа до запуска - оцениваю через призму окупаемости и удобства для конечного пользователя.
Мой приоритет: предсказуемый результат для заказчика. Фиксированные сроки, прозрачная смета и сайт, который работает как отлаженный механизм продаж, а не просто «визитка в интернете».

Понравилась статья? Поделиться с друзьями: