Курсорная переменная - это понятие PL/pgSQL, с помощью которого организована работа с курсорами.
Создадим таблицу:
=> CREATE TABLE t(id integer, s text);
CREATE TABLE
=> INSERT INTO t VALUES (1, 'Раз'), (2, 'Два'), (3, 'Три');
INSERT 0 3
Несвязанная переменная cur:
=> DO $$ DECLARE -- объявление переменной cur refcursor; BEGIN -- связывание с запросом и открытие курсора OPEN cur FOR SELECT * FROM t; END; $$;
DO
Связанная переменная связывается с запросом уже при объявлении.
=> DO $$ DECLARE -- объявление и связывание переменной cur CURSOR FOR SELECT * FROM t; BEGIN -- открытие курсора OPEN cur; END; $$;
DO
При этом переменная cur имеет тот же тип refcursor.
Связанная переменная может иметь параметры:
=> DO $$ DECLARE -- объявление и связывание переменной cur CURSOR(id integer) FOR SELECT * FROM t WHERE t.id = cur.id; BEGIN -- открытие курсора с указанием фактических параметров OPEN cur(1); END; $$;
DO
Обратите внимание на устранение неоднозначности имен в этом и следующем примерах.
Переменные PL/pgSQL также являются (неявными) параметрами курсора.
=> DO $$ <<local>> DECLARE id integer := 3; -- объявление и связывание переменной cur CURSOR FOR SELECT * FROM t WHERE t.id = local.id; BEGIN id := 1; -- открытие курсора (значение id берется на этот момент) OPEN cur; END; $$;
DO
Чтение выполняется командой FETCH. Если нужно только сдвинуть "окно" курсора, то можно воспользоваться другой командой - MOVE.
=> DO $$ DECLARE cur refcursor; rec record; BEGIN OPEN cur FOR SELECT * FROM t ORDER BY id; MOVE cur; FETCH cur INTO rec; RAISE NOTICE '%', rec; CLOSE cur; END; $$;
Что будет выведено на экран?
NOTICE: (2,Два) DO
Обычный способ организации цикла:
=> DO $$ DECLARE cur refcursor; rec record; BEGIN OPEN cur FOR SELECT * FROM t; LOOP FETCH cur INTO rec; EXIT WHEN NOT FOUND; RAISE NOTICE '%', rec; END LOOP; CLOSE cur; END; $$;
NOTICE: (1,Раз) NOTICE: (2,Два) NOTICE: (3,Три) DO
Чтобы не писать много команд, можно воспользоваться командой FOR, которая делает ровно то же самое:
=> DO $$ DECLARE cur CURSOR FOR SELECT * FROM t; BEGIN FOR rec IN cur LOOP -- cur должна быть связана с запросом RAISE NOTICE '%', rec; END LOOP; END; $$;
NOTICE: (1,Раз) NOTICE: (2,Два) NOTICE: (3,Три) DO
Более того, можно вообще обойтись без явной работы с курсором, если цикл - это все, что требуется. И такой цикл будет работать быстрее (из-за выборки по 10 строк, а не по одной).
=> DO $$ DECLARE rec record; -- надо объявить явно BEGIN FOR rec IN (SELECT * FROM t) LOOP RAISE NOTICE '%', rec; END LOOP; END; $$;
NOTICE: (1,Раз) NOTICE: (2,Два) NOTICE: (3,Три) DO
Скобки вокруг запроса не обязательны, но удобны.
Точно так же, как и цикл LOOP, FOR позволяет указать метку - во вложенных циклах это может оказаться полезным:
=> DO $$ DECLARE rec_outer record; rec_inner record; BEGIN <<OUTER>> FOR rec_outer IN (SELECT * FROM t ORDER BY id) LOOP <<INNER>> FOR rec_inner IN (SELECT * FROM t ORDER BY id) LOOP EXIT OUTER WHEN rec_inner.id = 3; RAISE NOTICE '%, %', rec_outer, rec_inner; END LOOP INNER; END LOOP OUTER; END; $$;
Что будет выведено?
NOTICE: (1,Раз), (1,Раз) NOTICE: (1,Раз), (2,Два) DO
Переменная FOUND позволяет узнать, была ли обработана хотя бы одна строка:
=> DO $$ DECLARE rec record; BEGIN FOR rec IN (SELECT * FROM t WHERE false) LOOP RAISE NOTICE '%', rec; END LOOP; RAISE NOTICE 'Была ли как минимум одна итерация? %', FOUND; END; $$;
NOTICE: Была ли как минимум одна итерация? f DO
Следует заметить, что в большом числе случаев вместо использования циклов можно выполнить задачу одним оператором SQL - и это будет проще и еще быстрее. Часто циклы используют просто потому, что это более привычный, "процедурный" стиль программирования. Но для базы данных этот стиль не подходит.
Например:
=> BEGIN; DO $$ DECLARE rec record; BEGIN FOR rec IN (SELECT * FROM t) LOOP RAISE NOTICE '%', rec; DELETE FROM t WHERE id = rec.id; END LOOP; END; $$; ROLLBACK;
BEGIN NOTICE: (1,Раз) NOTICE: (2,Два) NOTICE: (3,Три) DO ROLLBACK
Такой цикл заменяется одной простой командой:
=> BEGIN; DELETE FROM t RETURNING *; ROLLBACK;
BEGIN id | s ----+----- 1 | Раз 2 | Два 3 | Три (3 rows) DELETE 3 ROLLBACK
Продемонстрируем обработку в цикле с обновлением строки, выбранной курсором. Типичный случай - обработка пакета заданий с изменением статуса задания.
=> DO $$ DECLARE cur refcursor; rec record; BEGIN OPEN cur FOR SELECT * FROM t FOR UPDATE; LOOP FETCH cur INTO rec; EXIT WHEN NOT FOUND; UPDATE t SET s = s || ' (обработано)' WHERE CURRENT OF cur; END LOOP; CLOSE cur; END; $$;
DO
=> SELECT * FROM t;
id | s ----+------------------ 1 | Раз (обработано) 2 | Два (обработано) 3 | Три (обработано) (3 rows)
Аналогичный результат можно получить, явно указав в команде UPDATE уникальный ключ таблицы (WHERE id = rec.id). CURRENT OF работает быстрее и не использует индекс, но применимость такой конструкции ограниченная.
Откроем курсор и посмотрим значение курсорной переменной:
=> DO $$ DECLARE cur refcursor; BEGIN OPEN cur FOR SELECT * FROM t; RAISE NOTICE '%', cur; END; $$;
NOTICE: <unnamed portal 10> DO
Это имя курсора (портала), который был открыт на сервере. Имя было сгенерировано автоматически.
При желании имя можно задать явно (оно должно быть уникальным):
=> DO $$ DECLARE cur refcursor := 'cursor12345'; BEGIN OPEN cur FOR SELECT * FROM t; RAISE NOTICE '%', cur; END; $$;
NOTICE: cursor12345 DO
Пользуясь этим, можно написать функцию, которая откроет курсор и вернет его имя:
=> CREATE FUNCTION t_cur() RETURNS refcursor AS $$ DECLARE cur refcursor; BEGIN OPEN cur FOR SELECT * FROM t; RETURN cur; END; $$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION
Начнем транзакцию, вызовем функцию, узнаем имя курсора и получим возможность читать из него данные. Как это сделать - зависит от языка программирования. На psql это можно сделать так:
=> BEGIN;
BEGIN
=> SELECT t_cur() AS curname \gset
=> \echo :curname
<unnamed portal 11>
=> FETCH :"curname"; -- кавычки нужны из-за угловых скобок в имени
id | s ----+------------------ 1 | Раз (обработано) (1 row)
=> COMMIT;
COMMIT
Чтобы клиенту было проще, можно позволить ему самому устанавливать имя курсора:
=> DROP FUNCTION t_cur();
DROP FUNCTION
=> CREATE FUNCTION t_cur(cur refcursor) RETURNS void AS $$ BEGIN OPEN cur FOR SELECT * FROM t; END; $$ VOLATILE LANGUAGE plpgsql;
CREATE FUNCTION
=> BEGIN;
BEGIN
=> SELECT t_cur('cursor12345');
t_cur ------- (1 row)
=> FETCH cursor12345;
id | s ----+------------------ 1 | Раз (обработано) (1 row)
=> COMMIT;
COMMIT
Функция может вернуть и несколько открытых курсоров, используя OUT-параметры.
Таким образом можно за один вызов функции обеспечить клиента информацией из разных таблиц, если это необходимо.
Конец демонстрации.