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

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

Содержания:

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

Что такое курсор в базах данных?

Если вы когда-нибудь писали SQL-запрос, который возвращает несколько строк, вы работали с результирующим набором. Стандартные операции (SELECT, UPDATE, DELETE) по умолчанию обрабатывают весь набор как единое целое — это так называемый set-based подход. Но что делать, если нужно пройтись по строкам по одной, выполнив для каждой сложную логику, которую невозможно выразить одним UPDATE или INSERT? Вот тут на сцену выходит курсор.

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

Важно: Курсоры — это мощный, но ресурсоемкий инструмент. Используйте их только когда наборные операции (JOIN, подзапросы) не подходят.

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

Формально курсор — это область памяти, выделенная сервером для временного хранения результирующего набора запроса, плюс указатель на текущую строку внутри этого набора. Управляется курсор целиком на стороне СУБД, что накладывает ограничения на его производительность и потребление ресурсов.

  • Результирующий набор — данные, полученные после выполнения SELECT-запроса, который был указан при объявлении курсора.
  • Указатель на строку — внутренний индекс, который перемещается при каждом вызове FETCH.
  • Область памяти — может быть как в оперативной памяти сервера, так и на диске, в зависимости от размера набора и настроек СУБД.

Неявные и явные курсоры

Большинство разработчиков даже не подозревают, что уже используют курсоры — неявные. Они автоматически создаются СУБД для операций SELECT INTO (когда запрос возвращает одну строку) и для любых DML-операций (INSERT, UPDATE, DELETE). Вы не управляете ими напрямую — сервер делает это сам. А вот явные курсоры объявляются разработчиком в коде хранимой процедуры, функции или триггера. Именно они дают полный контроль над процессом выборки, но и требуют ручного управления жизненным циклом.

  • Неявный курсор (автоматический) — создаётся и уничтожается сервером без участия разработчика.
  • Явный курсор (ручное управление) — объявляется через DECLARE CURSOR, открывается, выбирает строки и закрывается вручную.

Как работает курсор: жизненный цикл

Работа с явным курсором всегда проходит через четыре обязательных этапа. Пропуск хотя бы одного — гарантированная проблема.

Этап Команда SQL Что происходит
1. Объявление DECLARE cursor_name CURSOR FOR SELECT … Определяется запрос, который сформирует результирующий набор. На этом этапе запрос не выполняется.
2. Открытие OPEN cursor_name Сервер выполняет запрос, выделяет память под набор строк, устанавливает указатель на первую строку.
3. Выборка FETCH NEXT FROM cursor_name INTO @var1, @var2 Извлекается текущая строка, указатель смещается на следующую. Повторяется в цикле до конца набора.
4. Закрытие и освобождение CLOSE cursor_name; DEALLOCATE cursor_name Освобождаются ресурсы (память, блокировки). DEALLOCATE удаляет определение курсора.

Частая ошибка: Незакрытый курсор может привести к утечке памяти и блокировкам. Всегда закрывайте курсор после использования.

Этап 1: Объявление курсора (DECLARE)

курсор как молоток для базы

Синтаксис зависит от СУБД, но общий принцип един: вы указываете имя курсора и SELECT-запрос. Например, в SQL Server это выглядит так:

DECLARE cur_users CURSOR FOR SELECT id, name FROM users WHERE active = 1;

Уже на этом этапе можно задать параметры, определяющие поведение курсора: будет ли он видеть изменения, сделанные другими сессиями (чувствительность), можно ли двигаться назад (прокрутка) и так далее.

  • Синтаксис DECLARE CURSOR — может включать ключевые слова INSENSITIVE, SCROLL, READ_ONLY и другие.
  • Параметры курсора — определяют тип курсора и его поведение.

Этап 2: Открытие курсора (OPEN)

Команда OPEN запускает выполнение запроса. Сервер БД выполняет SELECT, формирует результирующий набор и помещает его в выделенную область памяти (или на диск, если набор большой). Указатель устанавливается на первую строку. С этого момента курсор готов к выборке.

  • Выполнение запроса — сервер оптимизирует и исполняет запрос, возможно, с блокировками.
  • Выделение ресурсов — память, временные таблицы, блокировки — всё это потребляется с момента OPEN.

Этап 3: Выборка данных (FETCH)

Это сердце работы с курсором. Команда FETCH извлекает текущую строку и перемещает указатель. Направление перемещения зависит от типа курсора и указанного аргумента (NEXT, PRIOR, FIRST, LAST). Данные извлекаются в переменные, которые вы заранее объявили.

Пример на T-SQL:

FETCH NEXT FROM cur_users INTO @user_id, @user_name;

Обычно FETCH помещается в цикл WHILE или LOOP, который выполняется до тех пор, пока не будет достигнут конец набора (проверяется через системную переменную @@FETCH_STATUS в SQL Server или аналоги).

  • Направление выборки — NEXT, PRIOR, FIRST, LAST, RELATIVE, ABSOLUTE.
  • Переменные для приема данных — должны соответствовать по типу столбцам запроса.
  • Циклы — WHILE, LOOP, FOR — в зависимости от СУБД.

Этап 4: Закрытие и освобождение (CLOSE, DEALLOCATE)

После того как все строки обработаны, курсор нужно закрыть (CLOSE). Это освобождает текущий результирующий набор и снимает блокировки. Но само определение курсора остаётся в памяти — чтобы удалить его полностью, выполняется DEALLOCATE. Если не освободить курсор, он будет занимать ресурсы до конца сессии.

  • Освобождение ресурсов — CLOSE освобождает набор, DEALLOCATE — удаляет определение.
  • Различие между CLOSE и DEALLOCATE — после CLOSE курсор можно снова открыть, после DEALLOCATE — нет.

Типы курсоров и их особенности

Выбор типа курсора — это компромисс между производительностью, потреблением памяти и актуальностью данных. Разные СУБД предлагают разные наборы опций, но базовая классификация универсальна.

Критерий Тип Описание Когда использовать
По возможности изменения данных READ_ONLY Только чтение, нельзя обновлять строки через курсор. Когда нужно только читать данные, без изменений.
По возможности изменения данных UPDATE/DELETE Можно изменять или удалять текущую строку с помощью WHERE CURRENT OF. Когда нужно модифицировать строки в процессе обхода.
По чувствительности к изменениям Static (INSENSITIVE) Создаёт копию данных на момент OPEN. Не видит изменений других сессий. Когда важна консистентность снимка данных.
По чувствительности к изменениям Dynamic (SENSITIVE) Видит все изменения (вставки, обновления, удаления) других сессий. Когда нужны актуальные данные в реальном времени.
По чувствительности к изменениям Keyset-driven Фиксирует набор ключевых столбцов, но данные могут меняться. Компромисс между производительностью и актуальностью.
По возможности прокрутки FORWARD_ONLY Только движение вперёд (NEXT). Самый производительный тип, если не нужна навигация назад.
По возможности прокрутки SCROLL Полная навигация: NEXT, PRIOR, FIRST, LAST, RELATIVE, ABSOLUTE. Когда нужен произвольный доступ к строкам.

Совет: Тип курсора влияет на производительность и целостность данных. Выбирайте тип, исходя из задачи.

По возможности изменения данных

курсор выделяет одну строку

Если вы не планируете изменять данные в процессе обхода, всегда используйте READ_ONLY. Это снижает накладные расходы на блокировки и упрощает код. Обновляемые курсоры (FOR UPDATE) требуют осторожности — они могут удерживать блокировки на строки до завершения транзакции.

  • READ_ONLY курсор — не позволяет использовать WHERE CURRENT OF.
  • UPDATE/DELETE курсор — позволяет изменять текущую строку, но требует дополнительных блокировок.

По чувствительности к изменениям

Этот параметр определяет, видит ли курсор изменения, сделанные другими транзакциями после его открытия. Static курсор — это снимок данных на момент OPEN. Он безопасен с точки зрения консистентности, но может содержать устаревшие данные. Dynamic курсор всегда показывает актуальное состояние, но работает медленнее и может пропускать или дублировать строки, если другие сессии вставляют или удаляют записи.

  • Static cursor — копия данных, не видит изменения.
  • Dynamic cursor — видит все изменения.
  • Keyset-driven cursor — видит изменения только по ключевым столбцам.

По возможности прокрутки

FORWARD_ONLY курсор — самый быстрый. Он поддерживает только FETCH NEXT. Если вам нужно двигаться назад или перепрыгивать на произвольную позицию, выбирайте SCROLL. Но помните: SCROLL курсор потребляет больше памяти и ресурсов, так как сервер должен хранить весь набор строк для навигации.

  • FORWARD_ONLY курсор — только вперёд, минимальные накладные расходы.
  • SCROLL курсор — полная навигация, но дороже.

Когда использовать курсоры?

Курсоры — это инструмент для ситуаций, когда set-based подход (один UPDATE для всех строк) невозможен или приводит к нечитаемому, трудно поддерживаемому коду. Я выделяю три основных сценария.

«Курсоры — это крайняя мера. Всегда сначала рассматривайте наборные операции.» — практическое правило, которое я вывел за годы работы.

Пакетная обработка записей

Представьте, что вам нужно рассчитать бонус для каждого сотрудника по сложной формуле, которая зависит от данных из нескольких связанных таблиц и предыдущих расчётов. Одним UPDATE это не сделать — нужна последовательная обработка. Курсор здесь оправдан, особенно если количество строк невелико.

  • Обновление с условиями — когда условие для каждой строки вычисляется динамически.
  • Вызов хранимых процедур для каждой строки — например, при интеграции с внешними системами.

Миграция и трансформация данных

сет-бейс против курсора

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

  • ETL-процессы — извлечение, преобразование, загрузка.
  • Обработка ошибок по строкам — возможность откатить изменения для конкретной строки без потери остальных.

Генерация отчетов

Некоторые отчёты требуют построчного расчёта нарастающих итогов или сложных агрегаций, которые невозможно выразить оконными функциями (например, в старых версиях СУБД). В таких случаях курсор — допустимое решение, но я рекомендую сначала проверить, можно ли обойтись временными таблицами и рекурсивными CTE.

  • Финансовые отчеты — расчёт баланса на каждую дату.
  • Сводные таблицы — построчная консолидация данных из разных источников.

Производительность курсоров: плюсы и минусы

Курсоры часто критикуют за низкую производительность, и это справедливо. Но есть ситуации, когда они могут быть не медленнее, а иногда и быстрее альтернатив.

Аспект Курсор Наборная операция
Скорость выполнения Медленнее при большом количестве строк из-за накладных расходов на каждый FETCH. Быстрее, так как оптимизатор может эффективно использовать индексы и параллелизм.
Блокировки Может удерживать блокировки на строки дольше, увеличивая риск взаимоблокировок. Блокировки обычно кратковременные и на уровне страниц или таблиц.
Потребление памяти Зависит от типа курсора. Static курсор может потреблять много памяти для хранения копии набора. Минимальное, так как данные обрабатываются потоково.
Гибкость Высокая — можно выполнять любую логику на каждой строке. Низкая — ограничена выразительностью SQL.

Совет: Избегайте курсоров в высоконагруженных системах. Используйте bulk collect или временные таблицы для ускорения.

Недостатки курсоров

Основные минусы: медленная работа на больших наборах данных (каждая FETCH — это отдельное обращение к серверу), блокировки строк на время обработки, сложность кода и риск ошибок. В высоконагруженных OLTP-системах курсоры — табу.

  • Скорость выполнения — чем больше строк, тем заметнее разница с set-based подходом.
  • Блокировки таблиц — особенно опасны для обновляемых курсоров.
  • Нагрузка на сервер — каждый FETCH генерирует дополнительный сетевой трафик и потребляет CPU.

Когда курсоры быстрее

Парадоксально, но иногда курсор может оказаться быстрее набора операций. Например, если вам нужно выполнить для каждой строки вызов внешнего API или сложную хранимую процедуру, а set-based подход потребует создания огромной временной таблицы и нескольких проходов по данным. В таких случаях курсор может быть оправдан.

  • Сложная логика на строку — когда логика не выражается одним SQL-выражением.
  • Обработка ошибок на уровне строк — возможность продолжить выполнение при ошибке в одной строке.

Советы по оптимизации

осторожное использование курсора

Если без курсора не обойтись, вот несколько проверенных приёмов:

  • Выбор правильного типа — используйте FORWARD_ONLY, READ_ONLY, если не нужна прокрутка и обновление.
  • Bulk collect в Oracle — позволяет извлекать сразу несколько строк в коллекцию, уменьшая количество FETCH.
  • Минимизация сетевых вызовов — старайтесь выполнять всю логику внутри хранимой процедуры, а не на клиенте.

Альтернативы курсорам в SQL

Прежде чем писать курсор, я всегда задаю себе вопрос: «А можно ли это сделать одним запросом?» В 90% случаев ответ — да. Вот основные альтернативы.

Альтернатива Описание Пример
Наборные операции UPDATE с JOIN, INSERT SELECT, DELETE с подзапросом. UPDATE users SET balance = balance + 100 WHERE active = 1;
Оконные функции ROW_NUMBER, RANK, SUM OVER для вычислений без курсора. SELECT id, ROW_NUMBER() OVER (ORDER BY date) as rn FROM orders;
Рекурсивные CTE Для иерархических данных (деревья, графы). WITH RECURSIVE tree AS (…) SELECT …
Временные таблицы Сохранение промежуточных результатов во временную таблицу. CREATE TEMP TABLE temp AS SELECT …; UPDATE … FROM temp;

Совет: Наборные операции почти всегда эффективнее курсоров. Освойте оконные функции и CTE.

Наборные операции (Set-based operations)

Это основной подход в SQL. Вместо того чтобы обновлять строки по одной, вы пишете один UPDATE, который затрагивает все строки сразу. SQL-оптимизатор умеет эффективно выполнять такие запросы, используя индексы и параллелизм.

  • UPDATE с JOIN — обновление на основе данных из другой таблицы.
  • INSERT SELECT — вставка результата запроса.
  • DELETE с подзапросом — удаление строк, удовлетворяющих условию.

Оконные функции

Оконные функции (аналитические) позволяют выполнять вычисления над набором строк, связанных с текущей строкой, без необходимости явного курсора. Например, ROW_NUMBER() нумерует строки, SUM() OVER вычисляет нарастающий итог.

  • ROW_NUMBER() — нумерация строк в окне.
  • SUM() OVER — нарастающий итог.
  • LAG/LEAD — доступ к предыдущей/следующей строке.

Рекурсивные CTE

Common Table Expressions с рекурсией — мощный инструмент для работы с иерархическими данными (категории, организационные структуры). Они позволяют обходить дерево без курсора.

  • WITH RECURSIVE — объявление рекурсивного CTE.
  • Обход дерева — от корня к листьям и обратно.

Временные таблицы

курсор в сложном SQL запросе

Иногда полезно сохранить промежуточный результат во временную таблицу, а затем обработать его наборной операцией. Это может быть быстрее, чем курсор, особенно если данные нужно использовать несколько раз.

  • CREATE TEMP TABLE — создание временной таблицы.
  • Операции с временными таблицами — UPDATE, DELETE, JOIN с временной таблицей.

Примеры использования курсора в разных СУБД

Синтаксис курсора отличается в разных СУБД. Я приведу простые рабочие примеры для четырёх популярных систем.

Важно: Синтаксис курсора может сильно отличаться между СУБД. Всегда проверяйте документацию.

Курсор в MySQL

MySQL поддерживает курсоры только в хранимых процедурах и функциях. Курсоры в MySQL — только FORWARD_ONLY, без прокрутки. Пример:

CREATE PROCEDURE process_users()
BEGIN
DECLARE done INT DEFAULT FALSE;
DECLARE user_id INT;
DECLARE cur CURSOR FOR SELECT id FROM users;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
OPEN cur;
read_loop: LOOP
FETCH cur INTO user_id;
IF done THEN
LEAVE read_loop;
END IF;
-- обработка user_id
END LOOP;
CLOSE cur;
END;

  • Синтаксис — DECLARE CURSOR, OPEN, FETCH, CLOSE.
  • Пример — обход таблицы users с обработкой каждой строки.

Курсор в PostgreSQL

PostgreSQL поддерживает курсоры как в процедурах, так и на уровне сессии. Можно использовать SCROLL для прокрутки. Пример:

DO $$
DECLARE
cur CURSOR FOR SELECT id, name FROM users;
rec RECORD;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO rec;
EXIT WHEN NOT FOUND;
RAISE NOTICE 'User: %', rec.name;
END LOOP;
CLOSE cur;
END $$;

  • Синтаксис — DECLARE CURSOR, OPEN, FETCH, CLOSE.
  • Пример — вывод имён пользователей.

Курсор в SQL Server

SQL Server предлагает богатый набор опций для курсоров: LOCAL, GLOBAL, FORWARD_ONLY, SCROLL, STATIC, DYNAMIC и другие. Пример с WHILE:

DECLARE @user_id INT, @user_name NVARCHAR(100);
DECLARE cur CURSOR FOR SELECT id, name FROM users;
OPEN cur;
FETCH NEXT FROM cur INTO @user_id, @user_name;
WHILE @@FETCH_STATUS = 0
BEGIN
PRINT @user_name;
FETCH NEXT FROM cur INTO @user_id, @user_name;
END;
CLOSE cur;
DEALLOCATE cur;

  • Синтаксис — DECLARE CURSOR, OPEN, FETCH, CLOSE, DEALLOCATE.
  • Пример — обход с WHILE и проверкой @@FETCH_STATUS.

Курсор в Oracle

курсор может сломать базу

В Oracle курсоры используются в PL/SQL. Особенность — неявные курсоры (SELECT INTO) и явные с LOOP. Пример:

DECLARE
CURSOR cur IS SELECT id, name FROM users;
v_id users.id%TYPE;
v_name users.name%TYPE;
BEGIN
OPEN cur;
LOOP
FETCH cur INTO v_id, v_name;
EXIT WHEN cur%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(v_name);
END LOOP;
CLOSE cur;
END;

  • Синтаксис — CURSOR … IS, OPEN, FETCH, CLOSE.
  • Пример — обход с LOOP и атрибутом %NOTFOUND.

Распространенные ошибки и как их избежать

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

Незакрытый курсор

Самая распространённая ошибка — забыть закрыть курсор. Это приводит к утечке памяти и блокировкам, которые могут «подвесить» базу данных. Решение простое: всегда используйте CLOSE и DEALLOCATE, желательно в блоке TRY/CATCH или с обработчиком ошибок.

  • Проверка на закрытие — в SQL Server можно проверить через sys.dm_exec_cursors.
  • Использование блоков TRY/CATCH — гарантирует закрытие даже при ошибке.

Бесконечный цикл

Если не проверять условие окончания набора (NOT FOUND), цикл будет выполняться бесконечно. В T-SQL проверяйте @@FETCH_STATUS, в Oracle — %NOTFOUND, в MySQL — CONTINUE HANDLER.

  • Условие выхода — EXIT WHEN NOT FOUND или WHILE @@FETCH_STATUS = 0.
  • Обработка NOT FOUND — обязательно проверяйте после каждого FETCH.

Неправильный тип курсора

Использование SCROLL там, где достаточно FORWARD_ONLY, или DYNAMIC вместо STATIC — лишняя нагрузка на сервер. Всегда выбирайте минимально необходимый тип.

  • Выбор минимально необходимого типа — FORWARD_ONLY, READ_ONLY, если не нужна прокрутка и обновление.
  • Избегание динамических курсоров без нужды — они самые дорогие.

Заключение и лучшие практики

чек-лист безопасной работы с курсором

Курсоры — мощный, но опасный инструмент. Они нужны в специфических сценариях, но злоупотребление ими ведёт к проблемам с производительностью и поддерживаемостью кода. Главный принцип, который я вынес из практики: «Думайте наборами, а не строками». Прежде чем писать курсор, убедитесь, что альтернативы (оконные функции, CTE, временные таблицы) не подходят.

«Помните: курсор — это не зло, но его использование должно быть осознанным.»

Краткий чек-лист

  • Проверка необходимости — можно ли обойтись set-based операцией?
  • Выбор типа — FORWARD_ONLY, READ_ONLY, если нет особых требований.
  • Обработка ошибок — используйте TRY/CATCH и проверяйте NOT FOUND.
  • Закрытие — всегда CLOSE и DEALLOCATE.
  • Тестирование — сначала на небольшом наборе данных.

Надеюсь, эта статья поможет вам принимать взвешенные решения при работе с курсорами. Если вы хотите глубже разобраться в смежных темах, рекомендую почитать про ИИ-ассистентов Copilot и их влияние на подход к разработке, а также про ускорение написания кода с Cursor.

Часто задаваемые вопросы

Что такое курсор в SQL простыми словами?

Курсор — это механизм, который позволяет обрабатывать строки из результата запроса по одной, как если бы вы перебирали элементы списка в цикле.

Чем отличается явный курсор от неявного?

Неявный курсор создаётся автоматически для SELECT INTO и DML-операций. Явный курсор объявляется разработчиком и требует ручного управления (OPEN, FETCH, CLOSE).

Когда не стоит использовать курсор?

альтернативы курсору

Когда задачу можно решить одним UPDATE, SELECT с оконными функциями, рекурсивным CTE или временной таблицей. Курсоры оправданы только при сложной построчной логике.

Какой тип курсора самый быстрый?

FORWARD_ONLY, READ_ONLY — он не требует хранения всего набора и не поддерживает прокрутку.

Что будет, если не закрыть курсор?

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

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

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

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