SQL Cursor?

A cursor lets you iterate row-by-row over a query result and run procedural logic per row. Useful when set-based SQL can’t express the logic or when you must call side-effects per row. It’s slower than set-based operations—use sparingly.


SQL Server (T-SQL) example

DECLARE @id int, @amount decimal(12,2);

DECLARE cur CURSOR LOCAL FAST_FORWARD READ_ONLY FOR
  SELECT id, amount FROM dbo.Payments WHERE status = 'PENDING';

OPEN cur;
FETCH NEXT FROM cur INTO @id, @amount;

WHILE @@FETCH_STATUS = 0
BEGIN
  -- per-row work
  UPDATE dbo.Payments SET status = 'PAID' WHERE id = @id;
  FETCH NEXT FROM cur INTO @id, @amount;
END

CLOSE cur;
DEALLOCATE cur;

Tips: prefer LOCAL FAST_FORWARD READ_ONLY; keep transactions short; fetch only needed columns.


Oracle (PL/SQL) examples

Implicit cursor FOR loop (clean & safe):

BEGIN
  FOR r IN (SELECT id, amount FROM payments WHERE status='PENDING') LOOP
    UPDATE payments SET status='PAID' WHERE id = r.id;
  END LOOP;
END;
/

Explicit cursor:

DECLARE
  CURSOR c IS SELECT id FROM payments WHERE status='PENDING';
  v_id payments.id%TYPE;
BEGIN
  OPEN c;
  LOOP
    FETCH c INTO v_id; EXIT WHEN c%NOTFOUND;
    UPDATE payments SET status='PAID' WHERE id = v_id;
  END LOOP;
  CLOSE c;
END;
/

MySQL (in a stored procedure)

DELIMITER //
CREATE PROCEDURE mark_paid()
BEGIN
  DECLARE v_id INT; DECLARE done INT DEFAULT 0;
  DECLARE cur CURSOR FOR SELECT id FROM payments WHERE status='PENDING';
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;

  OPEN cur;
  read_loop: LOOP
    FETCH cur INTO v_id;
    IF done = 1 THEN LEAVE read_loop; END IF;
    UPDATE payments SET status='PAID' WHERE id = v_id;
  END LOOP;
  CLOSE cur;
END//
DELIMITER ;

PostgreSQL (transaction-scoped)

BEGIN;
DECLARE pay_cur CURSOR FOR SELECT id FROM payments WHERE status='PENDING';
FETCH NEXT FROM pay_cur; -- repeat fetch/update as needed
CLOSE pay_cur;
COMMIT;

Prefer set-based when possible (faster)

UPDATE payments SET status='PAID' WHERE status='PENDING';

Java note (since you’re using Java)

Often you don’t need DB cursors—use a streaming ResultSet:

stmt.setFetchSize(1000); // PostgreSQL/Oracle; MySQL needs server-side prep
try (ResultSet rs = stmt.executeQuery("SELECT ...")) {
  while (rs.next()) {
    // process row
  }
}

When to use cursors

  • Per-row side effects (external calls, complex validation).
  • Very large result sets where you must stream (server-side cursor).
  • Where procedural control flow is unavoidable.

Avoid / replace with

  • Joins, window functions, MERGE, UPDATE ... FROM, CTEs—nearly always faster and simpler.

Back to blog

Leave a comment