postgres stored procedure pl pgSQL cursor refcursor - ghdrako/doc_snipets GitHub Wiki

Podczas wykonywania operatorów SQL w środowisku PL/pgSQL system zarządzania bazą danych przydziela obszar roboczej pamięci, który zawiera informacje niezbędne do wykonania operatorów SQL oraz zestaw danych zwracanych lub przetwarzanych przez te operatory.

Kursor to wskaźnik na obszar roboczy operatora SQL, a za jego pomocą program PL/pgSQL może zarządzać tym obszarem.

Proces korzystania z jawnych kursorów składa się z następujących kroków:

  • Deklaracja kursora.
  • Otwarcie kursora.
  • Pobieranie danych z kursora.
  • Zamknięcie kursora.
{cursor name} [[NO] SCROLL] CURSOR [({параметры})] 
FOR {оператор SELECT};

Kursory są używane do przechodzenia między wierszami zestawu danych znajdującego się w obszarze roboczym.

Jeśli przy deklarowaniu kursora zostanie podane słowo kluczowe SCROLL, możliwe będzie przemieszczanie się zarówno w przód, jak i w tył. Jeśli zostanie podane NO SCROLL, możliwe będzie przemieszczanie się tylko w przód (możliwe jest tylko zwiększanie numeru wiersza)

DECLARE
Cur_Orders_Date CURSOR FOR
SELECT *
FROM Orders
WHERE order_date ='2019-11-02';

Kursor może być zadeklarowany z parametrami. Pozwala to kursorowi generować różne zestawy danych dla różnych wartości parametrów. Przy definiowaniu parametru należy podać jego nazwę i typ

DECLARE
Cur_Orders_Date_P CURSOR (p_date_ord Orders.order_date%TYPE) FOR 
SELECT *
FROM Orders
WHERE order_date = p_date_ord;

Cursors can be used for row-by-row processing, although that is an inefficient use of cursors within PostgreSQL and should be avoided in favor of using SQL to perform set operations. However, in addition to row-by-row processing, cursors are a mechanism of memory management within PostgreSQL. When you have very large data sets, you can avoid memory bottlenecks by using a cursor to process smaller batches of rows. A very common way to use this is in combination with a function.

DO $$ 
DECLARE  
  counter int; 
  filmcrew CURSOR FOR SELECT person_id, job FROM film_crew; 
  rec record; 
BEGIN 
  OPEN filmcrew; 
  counter = 0;
  WHILE counter < 2 LOOP 
    FETCH NEXT FROM filmcrew INTO rec; 
    counter = counter + 1; 
    RAISE NOTICE 'Job: %', rec.job; 
 END LOOP; 
END; $$ 

Z aktywnego zbioru danych pobierany jest wiersz, na który ustawiony jest wskaźnik, a wartości poszczególnych elementów tego wiersza przypisywane są do zmiennych, podanych po słowie kluczowym INTO. Następnie wskaźnik aktywnego zbioru przesuwa się do następnego wiersza.

FETCH {kierunek} FROM {nazwa kursora} INTO {lista zmiennych}I{rekord};
  • {kierunek} może mieć następujące wartości: NEXT, PRIOR, FIRST, LAST, ABSOLUTE N, RELATIVE N, FORWARD, BACKWARD.
  • Jeśli kierunek nie jest podany, zostanie użyty NEXT.
  • Zazwyczaj pobieranie danych z kursora odbywa się wewnątrz pętli, aż do momentu wykrycia końca zestawu danych. Do wykrywania końca aktywnego zestawu używana jest specjalna zmienna FOUND, która ma wartość TRUE, jeśli wiersz został pomyślnie pobrany, i FALSE, jeśli osiągnięto koniec aktywnego zestawu danych.

Podczas pracy z kursorami można używać polecenia MOVE, które przesuwa kursor, ale nie pobiera wiersza.

MOVE {kierunek} {nazwa kursora};

Kursory do zmiany danych nie powinny zawierać operacji grupowania i sortowania i powinny kończyć się frazą FOR UPDATE. Do aktualizacji wiersza, na który wskazuje kursor, używa się operatora UPDATE, który ma następującą składnię

UPDATE {nazwa tabeli} SET {operator aktualizacji} WHERE CURRENT OF {nazwa kursora}

Aby usunąć wiersz, na którym ustawiony jest wskaźnik kursora, używa się operatora DELETE, który ma następującą składnię

DELETE FROM {nazwa tabeli}
WHERE CURRENT OF {nazwa kursora}
DO $$ 
DECLARE
Cur_Emp cursor IS SELECT * FROM employees_copy 
FOR UPDATE;
v_add_salary employees.salary%TYPE;
v_sum_salary numeric(10,2):=0; 
BEGIN
FOR v_cur_emp IN Cur_Emp
LOOP ” 
v_add_salary := 
CASE
WHEN v_cur_emp.salary > 10000 THEN 1000
WHEN v_cur_emp.salary > 5000 THEN 500 
ELSE 200
END;
v_sum_salary := v_sum_salary +v_add_salary;
UPDATE Employees_copy
SET salary = salary + v_add_salary 
WHERE CURRENT OF cur_emp;
END LOOP;
RAISE NOTICE 'Результат: ' ;
RAISE notice ' v_sum_salary = %', v_sum_salary; 
END $$;
DO $$
DECLARE
Cur_Orders CURSOR (p_status Orders_copy.status%TYPE)
IS SELECT * FROM Orders_copy
WHERE status = p_status;
FOR UPDATE;
v_credit_limit Customers.credit_limit%TYPE; 
v_orders_sum numeric(10,2);
BEGIN
RAISE notice 'Удалены заказы';
FOR v_cur IN Cur_Orders('Pending') 
LOOP
SELECT credit_limit into v_credit_limit 
FROM Customers
WHERE customer_id = v_cur.customer_id;
SELECT SUM(quantity*unit_price) into v_orders_sum
FROM Orders_copy JOIN order_items_copy USING (order_id) 
WHERE customer_id = v_cur.customer_id
AND status = 'Pending';
IF v_orders_sum > v_credit_limit
THEN DELETE FROM Orders_copy
WHERE CURRENT OF cur_orders;
RAISE notice 'customer_id =%%%', v_cur.customer_id, 
'order_id = ', v_cur.order_id;
END IF;
END LOOP;
END $$;
CREATE OR REPLACE FUNCTION film_crew_info() 
RETURNS TABLE (personname TEXT, title TEXT, 
               department TEXT, job TEXT) 
AS $$ 
BEGIN  
RETURN QUERY 
   SELECT 
 p.name AS personname, 
 f.title, 
 fc.department, 
 fc.job 
   FROM 
 film_crew AS fc   JOIN person AS p ON 
 fc.person_id = p.person_id 
   JOIN film AS f ON 
 fc.film_id = f.film_id; 
END  
$$ LANGUAGE plpgsql;

BEGIN; 
DECLARE 
  filmcrew CURSOR FOR  SELECT * FROM film_crew_info(); 
  FETCH 15 
  FROM 
  filmcrew; 
COMMIT; 

This will pull 15 rows in a batch out of the cursor.

Cursors within PostgreSQL and PL/pgSQL have some utility but are infrequently used.

Petla kursorowa

FOR {zmienna petli} IN 
{nazwa kursora}|{instrukcja SELECT} 
LOOP
{cialo petli} 
END LOOP;

Operacje otwierania, pobierania i zamykania kursora w takich pętlach wykonywane są niejawnie. Zmniejsza to ilość kodu i ułatwia jego zrozumienie. Należy jednak pamiętać, że nie wszystkie zadania przetwarzania danych z użyciem kursorów mogą być realizowane przy użyciu pętli dla kursorów.

DO $$
DECLARE
Cur_Orders_Date_P CURSOR (p_date_ord Orders.order_date%TYPE) 
FOR SELECT * FROM Orders
WHERE order_date = p_date_ord;
BEGIN
RAISE NOTICE 'Результат: ';
RAISE NOTICE '%%%%% ' ,
LPAD('order_id',8),LPAD('customer_id',13), 
LPAD('status' , 10),LPAD('salesman_id’, 12) , 
LPAD('order_date’,12);
FOR v_cur_orders IN cur_orders_date_P('2019-11-02') 
LOOP
RAISE notice '%%%%% ',
LPAD(v_cur_orders.order_id::text,8),
LPAD(v_cur_orders.customer_id::text,13),
LPAD(v_cur_orders.status,10),
LPAD(v_cur_orders.salesman_id::text,12), 
LPAD(v_cur_orders.order_date::text,12);
END LOOP;
END $$;

Kursory i pętle dla kursorów mogą być zagnieżdżone. W takim przypadku wewnętrzny kursor wykonuje się dla każdego wiersza zewnętrznego kursora. Zewnętrzna pętla kolejno wybiera dane o klientach, a w wewnętrznej pętli dla każdego klienta tworzone są dane o łącznej sumie zamówień tego klienta, znajdujących się w stanie oczekiwania ('Pending').

DO $$
DECLARE
Cur_Customers CURSOR
FOR SELECT * FROM Customers;
cur_orders CURSOR (p_id_customer orders.customer_id%type, 
                   p_status Orders.status%type) FOR 
           SELECT customer_id, status, 
                  SUM(quantity*unit_price) AS order_sum 
             FROM Orders JOIN Order_items USING (order_id) 
            WHERE customer_id = p_id_customer 
              AND status = p_status
        GROUP BY customer_id, status;
BEGIN
  RAISE NOTICE 'Результат: ';
  RAISE notice '% % %', LPAD('customer_id',10),
  LPAD('status' , 10), LPAD('order_sum',12);
  FOR v cur cuct IN cur customers
  LOOP
    FOR v_cur_order IN cur_orders(v_cur_cuct.customer_id,'Pending') 
    LOOP
      RAISE notice '% % %' ,
      LPAD(v_cur_order.customer_id::text,10),
      LPAD(v_cur_order.status,10),
      LPAD(v_cur_order.order_sum::text,12);
   END LOOP;
  END LOOP;
END $$;

W wewnętrznej pętli kursorowej można używać wartości pól zmiennej kursorowej (zmiennej pętli) zewnętrznej pętli. Ponizej przedstawiono przykład użycia wartości pól zmiennej kursorowej zewnętrznej pętli. W pętli zewnętrznej z tabeli Customers pobierane są dane o klientach, a w pętli wewnętrznej obliczana jest całkowita suma zamówień klienta znajdujących się w stanie oczekiwania ('Pending') i wyświetlane są dane o klientach, których całkowita suma zamówień w stanie Pending przekracza ich limit kredytowy.

DO $$
DECLARE
  Cur_Customers CURSOR
  FOR SELECT * FROM Customers;
  Cur_Orders CURSOR (p_id_customer orders.eustomer_id%type, 
                     p_status Orders.status%type)
                FOR SELECT customer_id, status, 
                           SUM(quantity*unit_price) AS order_sum 
                      FROM Orders JOIN Order_items USING (order_id) 
                     WHERE customer_id = p_id_customer
                       AND status = p_status
                    GROUP BY customer_id, status;
BEGIN
  RAISE NOTICE 'Результат: ';
  RAISE NOTICE '% % %', LPAD('customer_id',10), 
  LPAD('credit_limit',12),LPAD('order_sum',12);
  FOR v cur cuct IN Cur Customers 
  LOOP
    FOR v_cur_order IN
      Cur_Orders(v_cur_cuct.customer_id,'Pending')
    LOOP
      IF v_cur_order.order_sum > v_cur_cuct.credit_limit 
      THEN
        RAISE NOTICE '% % %',
        LPAD(v_cur_order.customer_id::text,10),
        LPAD(v_cur_cuct.credit_limit::text,12), 
        LPAD(v_cur_order.order_sum::text,12);
      END IF;
    END LOOP;
  END LOOP;
END $$;

Kursor do modyfikacji danych

Kursory do zmiany danych nie powinny zawierać operacji grupowania i sortowania i powinny kończyć się frazą FOR UPDATE

Do aktualizacji wiersza, na którym ustawiony jest wskaźnik kursora, używa się operatora UPDATE, który ma następującą składnię:

UPDATE {nazwa tabeli} SET {operator aktualizacji} WHERE CURRENT OF {nazwa kursora}

Aby usunąć wiersz, na którym ustawiony jest wskaźnik kursora, używa się operatora DELETE, który ma następującą składnię:

DELETE FROM {nazwa tabeli}
WHERE CURRENT OF {nazwa kursora}

Przyklad modyfikacji:

DO $$ 
DECLARE
Cur_Emp cursor IS SELECT * FROM employees_copy 
FOR UPDATE;
v_add_salary employees.salary%TYPE;
v_sum_salary numeric(10,2):=0; 
BEGIN
FOR v_cur_emp IN Cur_Emp
LOOP ” 
v_add_salary := 
CASE
WHEN v_cur_emp.salary > 10000 THEN 1000
WHEN v_cur_emp.salary > 5000 THEN 500 
ELSE 200
END;
v_sum_salary := v_sum_salary +v_add_salary;
UPDATE Employees_copy
SET salary = salary + v_add_salary 
WHERE CURRENT OF cur_emp;
END LOOP;
RAISE NOTICE 'Результат: ' ;
RAISE notice ' v_sum_salary = %', v_sum_salary; 
END $$;

Przyklad usuwania:

DO $$
DECLARE
Cur_Orders CURSOR (p_status Orders_copy.status%TYPE)
IS SELECT * FROM Orders_copy
WHERE status = p_status;
FOR UPDATE;
v_credit_limit Customers.credit_limit%TYPE; 
v_orders_sum numeric(10,2);
BEGIN
RAISE notice 'Удалены заказы';
FOR v_cur IN Cur_Orders('Pending') 
LOOP
SELECT credit_limit into v_credit_limit 
FROM Customers
WHERE customer_id = v_cur.customer_id;
SELECT SUM(quantity*unit_price) into v_orders_sum
FROM Orders_copy JOIN order_items_copy USING (order_id) 
WHERE customer_id = v_cur.customer_id
AND status = 'Pending';
IF v_orders_sum > v_credit_limit
THEN DELETE FROM Orders_copy
WHERE CURRENT OF cur_orders;
RAISE notice 'customer_id =%%%', v_cur.customer_id, 
'order_id = ', v_cur.order_id;
END IF;
END LOOP;
END $$;

Zmienne kursorowe

W programach PL/pgSQL można używać zmiennych o typie kursora (zmienne kursorowe). Takie zmienne deklaruje się w sekcji deklaracji, a wartość (zestaw danych) przypisuje się w sekcji wykonywalnej. Po zamknięciu kursora powiązanego z zmienną kursorową można przypisać jej nową wartość. Używając zmiennych kursorowych, można przekazywać wyniki wykonania zapytań z jednego programu do drugiego.

Deklaracja zmiennej kursora:

{nazwa zmiennej kursora} REFCURSOR;

Przypisanie wartości zmiennej kursorowej odbywa się w wykonywalnej części przy użyciu operatora:

OPEN {zazwa zmiennej kursora} FOR {instrukcja SELECT};

Zamknięcie zmiennej kursora wykonuje się za pomocą operatora:

CLOSE {nazwa zmiennej kursora};

Podczas wykonywania tego operatora zwalniane są zasoby używane przez operator SELECT, ale aktywny zestaw danych powiązany z zmienną kursora pozostaje w zakresie zmiennej.

Przykład deklaracji i użycia zmiennej wskaźnikowej kursora v_refcur. W sekcji wykonywalnej można jej przypisać różne wartości (wyniki wykonania różnych instrukcji SELECT), które zależą od wartości zmiennej v_t.

DO $$ 
DECLARE 
  v_refcur REFCURSOR; 
  v_id integer;
  v_name varchar(40); 
  v_wh integer;
  v_t integer;
BEGIN
  v_t :=2;
  RAISE NOTICE 'Результат: ' ;
  RAISE notice ' v_t = %',v_t;
  CASE v_t
    WHEN 1 THEN OPEN v_refcur FOR
           SELECT employee_id, 
                  first_name | | ' ' | J last_name, 
                  department_id
            FROM Employees WHERE employee_id <110
           ORDER BY employee_id;
   WHEN 2 THEN OPEN v_refcur FOR
          SELECT department_id, 
                 department_name, 
                 location_id
           FROM Departments WHERE department_id <70 
          ORDER BY department_id;
  END CASE; 
LOOP
  FETCH v_refcur INTO v_id, v_name, v_wh;
  EXIT WHEN NOT FOUND;
  RAISE NOTICE 'v_id= %%%%%', LPAD(v_id::text,5), 
               'v_name= ', RPAD(v_name,20), 
               'v_wh= ',LPAD(v_wh::text,5);
END LOOP;
CLOSE v_refcur;
END $$;