Cursor: что это такое и как работает инструмент для работы с базами данных

Если вы когда-нибудь писали 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 Оптимизирован для последовательного чтения.

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

По расположению: серверные и клиентские

программист пишет SQL код

Серверные курсоры обрабатываются на стороне базы данных. Они могут создавать блокировки, но обеспечивают согласованность. Клиентские курсоры загружают данные на сторону приложения, что снижает нагрузку на сервер, но увеличивает трафик.

Серверные курсоры (блокировки, нагрузка, согласованность)

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

Клиентские курсоры (гибкость, загрузка сети)

Клиентские курсоры более гибкие: вы можете обрабатывать данные на клиенте, не блокируя сервер. Но если данных много, сеть может быть перегружена.

По поведению: статические, динамические, 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.

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

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

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