Cursor: что это и как использовать в программировании

Как практик, который последние несколько лет проектирует и сопровождает системы обработки данных в нескольких коммерческих проектах (от логистических платформ до финтех-решений), я сталкиваюсь с курсорами почти ежедневно. Тема эта — не про «теорию», а про реальные боли: как эффективно перебрать миллион записей, не убив базу данных, и когда курсор — зло, а когда — единственный рабочий инструмент.

Содержания:

В этой статье разберу, что такое курсор на самом деле, как его правильно готовить и, что важнее, когда от него лучше отказаться. Материал рассчитан на разработчиков, которые хотят не просто скопировать пример из документации, а понять внутреннюю кухню: trade-offs, грабли и приёмы, проверенные в бою.

Что такое курсор в программировании: базовое определение

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

  • В SQL — это механизм для последовательной обработки строк результата запроса.
  • В языках программирования (Python, Java, C#) — абстракция над курсором БД или файловым потоком.
  • В UI — курсор мыши, но это уже другая история.

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

Основные характеристики курсора

Чтобы не путаться, нужно чётко понимать четыре базовых свойства любого курсора.

Состояние курсора

Курсор может быть открыт (активен, готов к выборке) или закрыт (ресурсы освобождены). Забыли закрыть — получите утечку соединений или блокировки таблиц. В языках типа Python с with это решается автоматически, но в PL/SQL или T-SQL — зона ответственности разработчика.

Направление движения

Большинство курсоров движутся только вперёд (forward-only). Но есть прокручиваемые (scrollable): можно прыгнуть на любую строку, назад или вперёд. Платите за это памятью и производительностью.

Привязка к набору данных

программист смотрит на курсор над таблицей данных

Курсор всегда привязан к результату конкретного запроса или к открытому файлу. Если изменить данные в таблице после открытия курсора, поведение зависит от типа: чувствительный (sensitive) увидит изменения, нечувствительный (insensitive) — нет.

Возможность обновления

Некоторые курсоры позволяют не только читать, но и изменять данные через UPDATE ... WHERE CURRENT OF cursor. Это удобно для каскадных обновлений, но требует осторожности: легко создать взаимные блокировки.

Типы курсоров в SQL: явные и неявные

В SQL курсоры делятся на два лагеря: явные (программист управляет всем циклом) и неявные (СУБД делает всё сама). Выбор между ними — компромисс между контролем и простотой.

Характеристика Явный курсор Неявный курсор
Управление Полный контроль: DECLARE, OPEN, FETCH, CLOSE Автоматическое: создаётся и закрывается СУБД
Производительность Ниже (накладные расходы на управление) Выше (оптимизировано для одиночных запросов)
Гибкость Можно обрабатывать по одной строке, прерывать, изменять Только чтение одной строки или массовая операция
Типичное использование Сложная бизнес-логика, построчная обработка SELECT INTO, DML-операции

Явные курсоры: полный контроль

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

DECLARE CURSOR

Объявляете курсор и привязываете к нему SQL-запрос. В этот момент запрос не выполняется — только компилируется.

DECLARE cur_orders CURSOR FOR SELECT order_id, total FROM orders WHERE status = 'NEW';

OPEN cursor

лупа над SQL кодом с курсором

Открываете курсор — СУБД выполняет запрос и создаёт результирующий набор (но не обязательно загружает все строки в память).

OPEN cur_orders;

FETCH INTO variables

Извлекаете очередную строку в переменные. Обычно в цикле.

FETCH cur_orders INTO v_order_id, v_total;

CLOSE cursor

Закрываете курсор, освобождая ресурсы.

CLOSE cur_orders;

Атрибуты курсора (FOUND, NOTFOUND, ROWCOUNT) помогают контролировать цикл. Например, EXIT WHEN cur_orders%NOTFOUND — стандартный способ выйти из цикла.

Неявные курсоры: автоматизация

Неявные курсоры создаются автоматически для одиночных SELECT ... INTO или DML-операций (INSERT, UPDATE, DELETE). Вы не пишете DECLARE, OPEN, CLOSE — всё делает СУБД.

Пример неявного курсора

SELECT order_id INTO v_order_id FROM orders WHERE order_id = 1001;

Если запрос возвращает 0 строк — исключение NO_DATA_FOUND. Если больше одной — TOO_MANY_ROWS. Это ограничение: неявный курсор рассчитан ровно на одну строку.

Атрибуты неявного курсора

человек читает книгу с курсором на странице

SQL%FOUND, SQL%NOTFOUND, SQL%ROWCOUNT — работают так же, как у явного, но относятся к последней выполненной DML-операции.

Частая ошибка: использовать неявный курсор в цикле для массовой обработки. Каждый вызов SELECT INTO создаёт новый неявный курсор, что приводит к огромным накладным расходам. В таких случаях нужен явный курсор или set-based подход.

Курсоры в различных СУБД: сравнение синтаксиса

Хотя концепция курсора универсальна, синтаксис и возможности сильно различаются. Покажу на примерах для четырёх популярных СУБД.

СУБД Особенности Пример объявления
Oracle REF CURSOR, BULK COLLECT, FORALL CURSOR cur IS SELECT ...
PostgreSQL SCROLL, WITH HOLD, курсоры в функциях DECLARE cur CURSOR FOR SELECT ...
MySQL Только в хранимых процедурах, нет SCROLL DECLARE cur CURSOR FOR SELECT ...
SQL Server STATIC, DYNAMIC, KEYSET, FAST_FORWARD DECLARE cur CURSOR FOR SELECT ...

Важно: синтаксис курсора может сильно отличаться в разных СУБД, всегда обращайтесь к документации. Например, в MySQL нельзя использовать курсор вне хранимой процедуры, а в PostgreSQL можно объявить курсор в любом блоке.

Курсоры в Oracle: REF CURSOR и BULK COLLECT

Oracle — пожалуй, самая развитая платформа для курсоров. Здесь есть REF CURSOR — мощный инструмент для динамических запросов и передачи курсоров между процедурами.

REF CURSOR

Позволяет объявить переменную-курсор и открыть её для любого запроса, даже сформированного динамически.

TYPE refcur IS REF CURSOR; v_cur refcur; v_id NUMBER; BEGIN OPEN v_cur FOR 'SELECT employee_id FROM employees WHERE department_id = :1' USING 50; LOOP FETCH v_cur INTO v_id; EXIT WHEN v_cur%NOTFOUND; -- обработка END LOOP; CLOSE v_cur; END;

BULK COLLECT

Массовая выборка — альтернатива построчному FETCH. Загружает все строки в коллекцию (массив) за один проход.

SELECT employee_id BULK COLLECT INTO v_ids FROM employees WHERE department_id = 50;

Пример с FORALL

механическая рука с курсором перемещается по строкам базы

FORALL — массовое выполнение DML для всех элементов коллекции. В связке с BULK COLLECT даёт огромный прирост производительности.

FORALL i IN 1..v_ids.COUNT UPDATE employees SET salary = salary * 1.1 WHERE employee_id = v_ids(i);

Курсоры в PostgreSQL: WITH HOLD и SCROLL

PostgreSQL предлагает два интересных расширения: SCROLL (возможность перемещаться назад) и WITH HOLD (курсор живёт после завершения транзакции).

SCROLL cursor

Позволяет использовать FETCH PRIOR, FETCH ABSOLUTE n и другие команды навигации.

DECLARE cur SCROLL CURSOR FOR SELECT * FROM orders; FETCH PRIOR FROM cur;

WITH HOLD

Обычно курсор автоматически закрывается при завершении транзакции. WITH HOLD отменяет это поведение — курсор остаётся открытым до явного CLOSE.

DECLARE cur CURSOR WITH HOLD FOR SELECT * FROM orders;

Пример

BEGIN; DECLARE cur CURSOR WITH HOLD FOR SELECT * FROM orders WHERE status = 'PENDING'; COMMIT; -- курсор всё ещё открыт! FETCH cur INTO ... CLOSE cur;

Курсоры в MySQL: хранимые процедуры

развилка дорог курсор и наборный подход

В MySQL курсоры доступны только внутри хранимых процедур (не в функциях). Нет поддержки SCROLL — только forward-only.

Объявление курсора в MySQL

DECLARE cur CURSOR FOR SELECT id, name FROM users WHERE active = 1; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;

Пример хранимой процедуры

CREATE PROCEDURE process_users() BEGIN DECLARE done INT DEFAULT FALSE; DECLARE v_id INT; DECLARE v_name VARCHAR(100); DECLARE cur CURSOR FOR SELECT id, name FROM users WHERE active = 1; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; OPEN cur; read_loop: LOOP FETCH cur INTO v_id, v_name; IF done THEN LEAVE read_loop; END IF; -- обработка строки END LOOP; CLOSE cur; END;

Курсоры в SQL Server: статические и динамические

SQL Server предлагает четыре основных типа курсоров, которые отличаются поведением при изменении данных.

STATIC cursor

Создаёт временную копию данных в tempdb. Не видит изменений, сделанных после открытия. Самый безопасный, но и самый ресурсоёмкий.

DECLARE cur CURSOR STATIC FOR SELECT * FROM orders;

DYNAMIC cursor

склад данных с погрузчиком-курсором

Отражает все изменения (вставки, обновления, удаления) в реальном времени. Чувствителен к блокировкам.

DECLARE cur CURSOR DYNAMIC FOR SELECT * FROM orders;

FAST_FORWARD cursor

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

DECLARE cur CURSOR FAST_FORWARD FOR SELECT * FROM orders;

Курсоры в языках программирования: Python, Java, C#

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

  • Python — через библиотеки: psycopg2 (PostgreSQL), sqlite3, mysql-connector-python.
  • Java — через JDBC: Statement.executeQuery() возвращает ResultSet, который работает как курсор.
  • C# — через SqlDataReader в ADO.NET.

Курсоры в Python: пример с psycopg2

Библиотека psycopg2 — стандарт для работы с PostgreSQL из Python. Курсор здесь — объект, который получается из соединения.

Получение курсора

import psycopg2 conn = psycopg2.connect("dbname=test user=postgres") cur = conn.cursor()

Выполнение запроса

разорванная цепь с курсором внутри
cur.execute("SELECT id, name FROM users WHERE active = %s", (True,))

Итерация по результатам

for row in cur: print(row[0], row[1])

Использование контекстного менеджера with гарантирует закрытие курсора и соединения.

with psycopg2.connect(...) as conn: with conn.cursor() as cur: cur.execute(...)

Курсоры в Java: JDBC Statement и ResultSet

В JDBC ResultSet — это курсор, который по умолчанию движется только вперёд. Можно создать прокручиваемый ResultSet, но это дорого.

Создание Statement

Statement stmt = conn.createStatement();

Получение ResultSet

ResultSet rs = stmt.executeQuery("SELECT id, name FROM users");

Навигация по ResultSet

человек срывает яблоко палкой-курсором
while (rs.next()) { int id = rs.getInt("id"); String name = rs.getString("name"); }

Для прокрутки назад нужно явно указать тип: conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_READ_ONLY).

Курсоры в C#: SqlDataReader

SqlDataReader — потоковый курсор, который читает данные по одной строке, не загружая всё в память.

Создание SqlDataReader

using (SqlConnection conn = new SqlConnection(connString)) { conn.Open(); SqlCommand cmd = new SqlCommand("SELECT id, name FROM users", conn); SqlDataReader reader = cmd.ExecuteReader(); while (reader.Read()) { int id = reader.GetInt32(0); string name = reader.GetString(1); } reader.Close(); }

Закрытие reader

Обязательно закрывайте reader (или используйте using). Иначе соединение останется занятым.

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

Главный миф о курсорах: они всегда медленные. На самом деле, медленным может быть неправильное использование. Но в 90% случаев set-based операции работают быстрее. Почему? Потому что СУБД оптимизирована для работы с наборами, а не с одной строкой за раз.

Операция Курсор (построчно) Set-based
Обновление 1 млн строк ~10-30 секунд (в зависимости от СУБД) ~0.5-2 секунды
Потребление памяти Минимальное (одна строка) Может быть высоким (весь набор)
Гибкость Максимальная (можно прервать, изменить логику) Ограниченная (всё в одном запросе)

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

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

пульт управления с курсором-прожектором на мониторах

Сценарии для курсоров

  • Сложная бизнес-логика, которую нельзя выразить одним SQL-запросом (например, расчёт накопительных итогов с условиями).
  • Каскадные обновления связанных таблиц, где каждое обновление зависит от предыдущего.
  • Динамические запросы, где структура данных меняется на лету.

Сценарии против курсоров

  • Массовые обновления или вставки (лучше UPDATE ... WHERE или INSERT ... SELECT).
  • Агрегации и отчёты (оконные функции справятся быстрее).
  • Любая операция, которую можно выразить одним MERGE или JOIN.

Set-based операции: лучший выбор

Set-based подход — это когда одна команда SQL обрабатывает множество строк. Вместо курсора:

-- Вместо курсора: UPDATE employees SET salary = salary * 1.1 WHERE department_id = 50;

Преимущества: скорость, атомарность, меньше кода, легче тестировать.

BULK COLLECT и FORALL: массовая обработка

Если без курсора не обойтись, используйте BULK COLLECT (в Oracle) или аналоги в других СУБД. Это компромисс: вы загружаете строки пачками, а не по одной.

BULK COLLECT

рука зачеркивает строки списка курсором
SELECT employee_id BULK COLLECT INTO v_ids FROM employees WHERE department_id = 50;

FORALL

FORALL i IN 1..v_ids.COUNT UPDATE employees SET salary = salary * 1.1 WHERE employee_id = v_ids(i);

Производительность может быть в 10-100 раз выше, чем построчный FETCH.

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

Теперь перейду к реальным сценариям, с которыми сталкивался сам.

Пример 1: Генерация отчёта с курсором

Задача: сформировать текстовый отчёт по заказам за день, где для каждого клиента нужно вывести список его заказов и общую сумму.

Код примера (Oracle PL/SQL)

DECLARE CURSOR cur_clients IS SELECT client_id, name FROM clients; v_client_id clients.client_id%TYPE; v_name clients.name%TYPE; v_total NUMBER; v_report CLOB; BEGIN OPEN cur_clients; LOOP FETCH cur_clients INTO v_client_id, v_name; EXIT WHEN cur_clients%NOTFOUND; SELECT SUM(total) INTO v_total FROM orders WHERE client_id = v_client_id; v_report := v_report || 'Клиент: ' || v_name || ', сумма: ' || v_total || CHR(10); END LOOP; CLOSE cur_clients; -- сохраняем отчёт DBMS_OUTPUT.PUT_LINE(v_report); END;

Пояснение

робот-курсор в лабиринте труб центра данных

Здесь курсор нужен, потому что для каждого клиента выполняется подзапрос к заказам. Set-based альтернатива — SELECT c.name, SUM(o.total) FROM clients c LEFT JOIN orders o ON c.client_id = o.client_id GROUP BY c.name — была бы быстрее, но если логика сложнее (например, форматирование строки), курсор оправдан.

Пример 2: Каскадное обновление данных

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

Код примера (T-SQL)

DECLARE @order_id INT, @new_status VARCHAR(20); DECLARE cur CURSOR FOR SELECT order_id, new_status FROM order_updates WHERE processed = 0; OPEN cur; FETCH NEXT FROM cur INTO @order_id, @new_status; WHILE @@FETCH_STATUS = 0 BEGIN UPDATE payments SET status = @new_status WHERE order_id = @order_id; UPDATE deliveries SET status = @new_status WHERE order_id = @order_id; UPDATE order_updates SET processed = 1 WHERE order_id = @order_id; FETCH NEXT FROM cur INTO @order_id, @new_status; END; CLOSE cur; DEALLOCATE cur;

Пояснение

Курсор здесь — вынужденная мера, если логика обновления сложная и зависит от внешних условий. Но если можно сделать UPDATE payments SET status = (SELECT new_status FROM order_updates WHERE order_id = payments.order_id) — это будет быстрее.

Пример 3: Миграция данных с трансформацией

Задача: перенести данные из старой таблицы в новую с изменением формата (например, объединить поля).

Код примера (PostgreSQL)

программисты спорят о курсоре и молнии
DO $$ DECLARE cur CURSOR FOR SELECT id, first_name, last_name, phone FROM old_users; v_id INT; v_first_name VARCHAR; v_last_name VARCHAR; v_phone VARCHAR; BEGIN OPEN cur; LOOP FETCH cur INTO v_id, v_first_name, v_last_name, v_phone; EXIT WHEN NOT FOUND; INSERT INTO new_users (id, full_name, contact) VALUES (v_id, v_first_name || ' ' || v_last_name, v_phone); END LOOP; CLOSE cur; END; $$;

Пояснение

Здесь курсор позволяет выполнить трансформацию на лету. Set-based вариант — INSERT INTO new_users SELECT id, first_name || ' ' || last_name, phone FROM old_users — был бы проще, но если трансформация включает сложную логику (например, проверку дубликатов), курсор даёт больше контроля.

Ошибки и лучшие практики при работе с курсорами

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

Ошибка Последствия Решение
Незакрытый курсор Утечка ресурсов, блокировки таблиц Всегда использовать CLOSE (или using в C#/Python)
Отсутствие обработки исключений Аварийное завершение, потеря данных Оборачивать в BEGIN…EXCEPTION…END
Неправильный тип курсора Лишние блокировки или неверные данные Выбирать STATIC для отчётов, FAST_FORWARD для простого чтения
Курсор в триггере Каскадные сбои, падение производительности Избегать курсоров в триггерах — использовать set-based

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

Типичные ошибки

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

В PL/SQL, если курсор не закрыт, он занимает память и может блокировать другие сессии. В C# забытый SqlDataReader блокирует соединение.

Отсутствие обработки исключений

свеча в форме курсора сжигает строки базы

Если внутри цикла возникает ошибка, курсор остаётся открытым, а транзакция — незавершённой. Всегда используйте BEGIN ... EXCEPTION ... WHEN OTHERS THEN CLOSE cur; RAISE; END;.

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

В SQL Server выбор DYNAMIC вместо STATIC может привести к блокировкам, если данные изменяются параллельно.

Рекомендации по оптимизации

FOR LOOP

Вместо явного OPEN/FETCH/CLOSE используйте FOR rec IN cur LOOP ... END LOOP; — это короче и автоматически закрывает курсор.

FOR rec IN (SELECT * FROM orders) LOOP -- обработка END LOOP;

Ограничение строк

Если можно обработать только часть данных, используйте WHERE ROWNUM <= 1000 (Oracle) или LIMIT (PostgreSQL).

BULK COLLECT

удочка с курсором вылавливает рыбу из данных

Для массовой выборки используйте BULK COLLECT с ограничением размера пачки (например, LIMIT 1000). Это снижает нагрузку на память.

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

Заключение: резюме и дальнейшее изучение

Курсоры — мощный, но ресурсоёмкий инструмент. Они незаменимы для сложной построчной логики, но в 90% случаев set-based операции быстрее и надёжнее. Главные выводы:

  • Всегда оценивайте, можно ли заменить курсор одним SQL-запросом.
  • Если курсор неизбежен, используйте массовые операции (BULK COLLECT, FORALL).
  • Не забывайте закрывать курсоры и обрабатывать исключения.
  • Изучайте документацию вашей СУБД — возможности сильно различаются.

Для углублённого изучения рекомендую:

Если вы хотите глубже разобраться в том, как современные инструменты автоматизации кода меняют подход к написанию программ, рекомендую прочитать статью «Как Cursor меняет подход к написанию программ». А для тех, кто интересуется ИИ-инструментами, будет полезна статья «Cursor: ИИ-инструмент для автоматизации кода в 2026».

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

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

Это указатель на текущую строку в результате запроса. Позволяет обрабатывать строки по одной, как если бы вы читали книгу страница за страницей.

Когда использовать курсор, а когда — set-based операцию?

Курсор — когда логика обработки строки зависит от предыдущих строк или требует вызова внешних сервисов. Set-based — для массовых обновлений, агрегаций, фильтрации.

Почему курсоры считаются медленными?

часы со стрелками-курсорами отсчитывают время

Потому что они обрабатывают строки по одной, вызывая множество переключений контекста между СУБД и приложением. Set-based операции работают с целыми наборами данных за один проход.

Как избежать утечки ресурсов при работе с курсором?

Всегда закрывайте курсор после использования (CLOSE). В языках программирования используйте конструкции with (Python) или using (C#). В PL/SQL — блоки BEGIN…EXCEPTION…END.

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

Технически — да, но крайне не рекомендуется. Это может привести к каскадным проблемам и падению производительности. Лучше использовать set-based операции.

Что такое REF CURSOR в Oracle?

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

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

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

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