Skip to content

Циклы в PL/SQL

Циклы используются для повторения какого-либо действия несколько раз.

Loop

Простейший цикл в PL/SQL выглядит так:

sql
loop
 -- какое-либо действие
end loop;
loop
 -- какое-либо действие
end loop;

Данный цикл будет выполнять код между loop и end loop бесконечно. Для того, чтобы завершить цикл, можно использовать команду exit:

sql
declare
    i number := 5;
begin
    loop
        if i = 0 then
            exit;
        end if;
        
        dbms_output.put_line(i);
        i := i - 1;
    end loop;
end;
declare
    i number := 5;
begin
    loop
        if i = 0 then
            exit;
        end if;
        
        dbms_output.put_line(i);
        i := i - 1;
    end loop;
end;

Результат:

5
4
3
2
1
5
4
3
2
1

Переменная i отвечает за оставшееся количество повторений. Такие переменные в программировании ещё называют счётчиками цикла.

Для более удобного выхода из цикла можно использовать конструкцию exit when:

sql
declare
    i number := 5;
begin
    -- Такой же цикл, как и в предыдущем примере, только
    -- читается гораздо лучше
    loop
        exit when i = 0;
        
        dbms_output.put_line(i);
        i := i - 1;
    end loop;
end;
declare
    i number := 5;
begin
    -- Такой же цикл, как и в предыдущем примере, только
    -- читается гораздо лучше
    loop
        exit when i = 0;
        
        dbms_output.put_line(i);
        i := i - 1;
    end loop;
end;

Exit можно использовать только внутри цикла, внутри обычного блока его использование приведёт к ошибке:

-- Ошибка!
begin
    exit;
end;
-- Ошибка!
begin
    exit;
end;

For

Цикл for удобно использовать для автоматического изменения счётчика цикла:

sql
begin
    for i in 1..5
    loop
        dbms_output.put_line(i);
    end loop;
end;
begin
    for i in 1..5
    loop
        dbms_output.put_line(i);
    end loop;
end;

Результат:

1
2
3
4
5
1
2
3
4
5

Здесь не использовались ни проверки на необходимость выйти из цикла, ни изменение счётчика цикла - конструкция for всё сделала за нас. Мы обращаемся к переменной i - она видима только внутри тела цикла, и недоступна вне его.

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

sql
-- Выведет:
-- -5
-- -4
-- -3
-- -2
-- -1
-- 0
-- 1
begin
    for i in -5..1
    loop
        dbms_output.put_line(i);
    end loop;
end;
-- Выведет:
-- -5
-- -4
-- -3
-- -2
-- -1
-- 0
-- 1
begin
    for i in -5..1
    loop
        dbms_output.put_line(i);
    end loop;
end;

Если верхняя граница равна нижней, цикл выполнится ровно один раз:

sql
-- Выведет одну строку:
-- 1
begin
    for i in 1..1
    loop
        dbms_output.put_line(i);
    end loop;
end;
-- Выведет одну строку:
-- 1
begin
    for i in 1..1
    loop
        dbms_output.put_line(i);
    end loop;
end;

В сторону уменьшения работать не будет - тело цикла for i in 3..1 не выполнится ни разу.

Стоит также обратить внимание, что мы не объявляли переменную i в секции declare - она создаётся автоматически. Более того, если мы объявим переменную вне цикла for, то после завершения цикла внешняя переменная останется без изменений:

sql
declare
    i number := 10;
begin
    for i in 1..4
    loop
        null;
    end loop;
    
    -- Выведет 10, а не 4, как можно было бы подумать
    dbms_output.put_line(i);
end;
declare
    i number := 10;
begin
    for i in 1..4
    loop
        null;
    end loop;
    
    -- Выведет 10, а не 4, как можно было бы подумать
    dbms_output.put_line(i);
end;

While

Цикл while будет работать до тех пор, пока условие, указанное после него, истинно:

sql
declare
    i number := 5;
begin
    while i > 0
    loop
        dbms_output.put_line(i);
        i := i -1;
    end loop;
end;
declare
    i number := 5;
begin
    while i > 0
    loop
        dbms_output.put_line(i);
        i := i -1;
    end loop;
end;

Вложенные циклы

Циклы могут быть вложенными. В целом, здесь нет ничего сложного, обратим мнимание лишь на несколько моментов.

Вложенные циклы for могут иметь счётчики с одним и тем же именем:

sql
begin
    for i in 1..2
    loop
        dbms_output.put_line('i1= ' || i);
        for i in 1..2
        loop
            dbms_output.put_line('i2= ' || i);
        end loop;
    end loop;
end;
begin
    for i in 1..2
    loop
        dbms_output.put_line('i1= ' || i);
        for i in 1..2
        loop
            dbms_output.put_line('i2= ' || i);
        end loop;
    end loop;
end;

Выведет:

i1= 1
i2= 1
i2= 2
i1= 2
i2= 1
i2= 2
i1= 1
i2= 1
i2= 2
i1= 2
i2= 1
i2= 2

Как видно, значения счётчика из внешнего цикла не перепуталось со значением счётчика из внутреннего. Это произошло потому, что мы обращались к счётчикам внутри тела цикла, к которому они относятся. Если мы перенесём вывод счётчика внешнего цикла внутрь тела внутреннего, работать не будет:

sql
begin
    for i in 1..2
    loop
        for i in 1..2
        loop
            dbms_output.put_line('i1= ' || i);
            dbms_output.put_line('i2= ' || i);
        end loop;
    end loop;
end;
begin
    for i in 1..2
    loop
        for i in 1..2
        loop
            dbms_output.put_line('i1= ' || i);
            dbms_output.put_line('i2= ' || i);
        end loop;
    end loop;
end;
i1= 1
i2= 1
i1= 2 -- А должно быть 1
i2= 2
i1= 1
i2= 1
i1= 2
i2= 2
i1= 1
i2= 1
i1= 2 -- А должно быть 1
i2= 2
i1= 1
i2= 1
i1= 2
i2= 2

Решить эту проблему помогают метки блоков:

sql
begin
    <<main>>
    for i in 1..2
    loop
        <<child>>
        for i in 1..2
        loop
            
            dbms_output.put_line('i1= ' || main.i);
            dbms_output.put_line('i2= ' || child.i);
        end loop;
    end loop;            
end;
begin
    <<main>>
    for i in 1..2
    loop
        <<child>>
        for i in 1..2
        loop
            
            dbms_output.put_line('i1= ' || main.i);
            dbms_output.put_line('i2= ' || child.i);
        end loop;
    end loop;            
end;

Результат:

i1= 1
i2= 1
i1= 1
i2= 2
i1= 2
i2= 1
i1= 2
i2= 2
i1= 1
i2= 1
i1= 1
i2= 2
i1= 2
i2= 1
i1= 2
i2= 2

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

sql
begin
    <<main>>
    for i in 1..2
    loop
        <<child>>
        for i in 1..2
        loop
            dbms_output.put_line('i1= ' || main.i);
            dbms_output.put_line('i2= ' || child.i);
        -- указываем имя идентификатора блока
        end loop child;
    end loop main;            
end;
begin
    <<main>>
    for i in 1..2
    loop
        <<child>>
        for i in 1..2
        loop
            dbms_output.put_line('i1= ' || main.i);
            dbms_output.put_line('i2= ' || child.i);
        -- указываем имя идентификатора блока
        end loop child;
    end loop main;            
end;

Continue. Переход на следующую итерацию цикла

Команда continue используется для перехода к следующей итерации цикла. Часть тела цикла после данной команды не будет выполнена.

sql
-- Выведет пять строк с числами от 1 до 5.
-- Числа с приплюсованной двойкой не будут выведены,
-- так как их вывод находится после continue.
begin
    for i in 1..5
    loop
        dbms_output.put_line(i);
        continue;
        dbms_output.put_line(i + 2);
    end loop;
end;
-- Выведет пять строк с числами от 1 до 5.
-- Числа с приплюсованной двойкой не будут выведены,
-- так как их вывод находится после continue.
begin
    for i in 1..5
    loop
        dbms_output.put_line(i);
        continue;
        dbms_output.put_line(i + 2);
    end loop;
end;

Как и с командой exit, есть и более удобный вариант этой команды - continue when:

sql
begin
    for i in 1..10
    loop
        continue when i mod 2 = 0;
        dbms_output.put_line(i);
    end loop;
end;
begin
    for i in 1..10
    loop
        continue when i mod 2 = 0;
        dbms_output.put_line(i);
    end loop;
end;

Результат:

1
3
5
7
9
1
3
5
7
9

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