Help us understand the problem. What is going on with this article?

PL/SQLで配列を扱う(要素数0をうまく扱う)

最近会社で聞かれて答えたことをまとめてみました。

PL/SQLで配列を扱うとき、FIRST~LASTでループするような処理を作っていたら、配列の要素数が0のときにエラーになってしまった。どうすればよいか分からない、と聞かれました。

用語

配列と呼んでいるものは、Oracleの公式用語としてはコレクションです。

コレクション・メソッド

コレクションの先頭データを取得するFIRST、コレクションの末尾データを取得するLASTなどは、コレクション・メソッドと呼ばれます。この用語を知っていないと 公式ドキュメント(11g) を検索することも容易ではありません。

コレクションメソッドを使って、0件でもうまく動くような、コレクションを一巡する処理を作成してみます。

そもそもコレクションには3種類ある

PL/SQLには、コレクションと呼べるものが3種類あります。

  • 結合配列
  • ネストした表
  • 可変長配列(VARRAY)

分類の詳細と特性については、SHIFT the Oracle さんのサイト を見ていただくのが分かりやすいと思います。

サンプルコードの解説

動作を理解するためのサンプルコードを末尾に置いてあります。このサンプルコードの動作は、以前作成した Oracle Database 11g XE を用いて動作確認しています。

T_ARRAY1が結合配列、T_ARRAY2がネストした表、T_ARRAY3が可変長配列(VARRAY)を表すデータ型です。これらのデータ型で宣言した変数が、V_ARRAY1~V_ARRAY3です。

このサンプルコードは、コレクションを一巡する処理に焦点を当てています。そのため、コレクションに値を入れる部分は、さほど凝ったことをしていません。EXTENDなどを使用していない、という意味です。

処理をみると分かりますが、コレクションの情報を出力する PRC_OUTPUT は3つとも同じコードです。引数の型のみが異なります。

言い換えるとこうなります。

コレクションは、宣言、初期化、代入、要素数の拡大の方法が、3種類それぞれ微妙に違います。しかし、コレクションを一巡して参照する処理は、どれも同じ書き方で書くことができます。

ループで回す場合、以下のどちらかの方法を使うことになります。

  • FIRST ~ LAST でループして EXISTS で存在するもののみを処理する
  • FIRST+NEXT で飛び飛びに処理する

前者は添え字が数字の場合に使うことができます。Javaに例えると、ArrayListをインデックス番号で辿るような方法です。

後者は添え字の種類を問わず使うことができます。Javaに例えると、LinkedListをリンク経由で辿るような方法です

なお、FIRST ~ LAST でループする場合、注意する点があります。

COUNT=0の場合、FIRST=LAST=NULLとなります。この場合、FIRST ~ LAST でループしようとすると、エラーになってしまいます(ORA-06502)。そのため、ループを COUNT>0 のIFで包む必要があります。

逆順に処理するときは、ループの REVERSE や、コレクションメソッドの PRIOR を使えばできます。でも、使うことがあるかは微妙です。

結合配列

結合配列は、他の言語で言うところの連想配列です。そのため、添え字は連続しているとは限りません。さらに、添え字は文字の可能性もあります。

飛び飛びの度合いが強いなら、FIRST+NEXTでループするのが効率的です。

結合配列では、"未初期化"="要素数0"です。未初期化の場合、COUNTメソッドは0を返します。

"ネストした表" や "可変長配列" では、NULLを代入することで再度、未初期化の状態にできます。しかし、結合配列には、NULLを代入できません(ORA-06550)。再初期化するには、未初期化の変数を用いて代入するという方法があります。

サンプルコードでは V_EMPTY が未初期化のままとなっている変数です。これをV_ARRAY1に代入することで、再度、未初期化状態に戻す処理としています。

ネストした表

ネストした表は、1オリジンのコレクションです。

単純に作成するうえでは密なコレクションです。しかし、DELETEメソッドを用いて途中の要素を削除することができます。このようなことをすると疎なコレクションになります。

大量にDELETEして、よほど飛び飛びにならない限り、FIRST~LASTでループするのが効率的かつ分かりやすいでしょう。

ネストした表は、"未初期化"≠"要素数0"です。未初期化の場合、COUNTメソッドもエラーとなります(ORA-06531)。
必ず初期化してから使うようにすることで、COUNTのエラーを避けられます。
Effective Java 項目43「nullではなく空配列か空コレクションを返す」に似ているかもしれません。
初期化さえしておけば、結合配列の場合と同様に扱うことができます。

NULLを代入することで再度、未初期化の状態にできます。
しかし、先にも書いたように、NULLを代入するよりは要素数0のコレクションとするほうが使い勝手が良いでしょう。

可変長配列(VARRAY)

可変長配列は、1オリジンで密なコレクションです。

密なコレクションなので、FIRST~LASTでループするのが効率的かつ分かりやすいでしょう。

下のサンプルコードでは、処理を揃えるためにFIRST~LASTループ部分にEXISTSを入れてあります。しかし、密なコレクションなので、EXISTSはなくても動きます。

可変長配列は、"未初期化"≠"要素数0"です。未初期化の場合、COUNTメソッドもエラーとなります(ORA-06531)。このあたりの特性は、ネストした表に似ています。

DELETEメソッドを用いて一部の要素を削除することはできません(ORA-06550)。DELETEメソッドを引数なしで使用し、要素を全削除することは可能です。全削除後は、要素数0となります。

NULLを代入することで再度、未初期化の状態にできます。ここも、ネストした表に似ています。

サンプルコード

DECLARE
    TYPE        T_ARRAY1    IS  TABLE OF VARCHAR2(100) INDEX BY BINARY_INTEGER;     -- 結合配列
    TYPE        T_ARRAY2    IS  TABLE OF VARCHAR2(100);                             -- ネストされた表
    TYPE        T_ARRAY3    IS  VARRAY(10) OF VARCHAR2(100);                        -- 可変長配列
    V_IDX       NUMBER;

    -- 結合配列
    PROCEDURE PRC_OUTPUT ( V_ARRAY T_ARRAY1 ) IS
    BEGIN
        -- COUNT/FIRST/LASTを表示
        BEGIN
            DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY.COUNT = [' || V_ARRAY.COUNT || ']' );
        EXCEPTION
            WHEN OTHERS THEN
                DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY.COUNT = [' || SQLERRM || ']');
                RETURN;
        END;
        DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY.FIRST = [' || V_ARRAY.FIRST || ']' );
        DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY.LAST = [' || V_ARRAY.LAST || ']' );
        -- FIRST/LAST/EXISTSを用いてループ
        --      添え字が数字ならこの方法でもできる
        --      NUMBERでループするため、FIRSTやLASTがNULLの場合に処理できないので、COUNT>0で包む
        IF V_ARRAY.COUNT > 0 THEN
            FOR V_IDX IN V_ARRAY.FIRST .. V_ARRAY.LAST LOOP
                IF V_ARRAY.EXISTS ( V_IDX ) THEN
                    DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY(' || V_IDX || ') = [' || V_ARRAY(V_IDX) || ']' );
                END IF;
            END LOOP;
        END IF;
        -- FIRST/NEXTを用いてループ
        --      添え字が文字でも扱える
        V_IDX := V_ARRAY.FIRST;
        LOOP
            EXIT WHEN V_IDX IS NULL;
            DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY(' || V_IDX || ') = [' || V_ARRAY(V_IDX) || ']' );
            V_IDX := V_ARRAY.NEXT(V_IDX);
        END LOOP;
    END;

    -- ネストした表
    PROCEDURE PRC_OUTPUT ( V_ARRAY T_ARRAY2 ) IS
    BEGIN
        -- COUNT/FIRST/LASTを表示
        BEGIN
            DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY.COUNT = [' || V_ARRAY.COUNT || ']' );
        EXCEPTION
            WHEN OTHERS THEN
                DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY.COUNT = [' || SQLERRM || ']');
                RETURN;
        END;
        DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY.FIRST = [' || V_ARRAY.FIRST || ']' );
        DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY.LAST = [' || V_ARRAY.LAST || ']' );
        -- FIRST/LAST/EXISTSを用いてループ
        --      添え字が数字ならこの方法でもできる
        --      NUMBERでループするため、FIRSTやLASTがNULLの場合に処理できないので、COUNT>0で包む
        IF V_ARRAY.COUNT > 0 THEN
            FOR V_IDX IN V_ARRAY.FIRST .. V_ARRAY.LAST LOOP
                IF V_ARRAY.EXISTS ( V_IDX ) THEN
                    DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY(' || V_IDX || ') = [' || V_ARRAY(V_IDX) || ']' );
                END IF;
            END LOOP;
        END IF;
        -- FIRST/NEXTを用いてループ
        --      添え字が文字でも扱える
        V_IDX := V_ARRAY.FIRST;
        LOOP
            EXIT WHEN V_IDX IS NULL;
            DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY(' || V_IDX || ') = [' || V_ARRAY(V_IDX) || ']' );
            V_IDX := V_ARRAY.NEXT(V_IDX);
        END LOOP;
    END;

    -- 可変長配列
    PROCEDURE PRC_OUTPUT ( V_ARRAY T_ARRAY3 ) IS
    BEGIN
        -- COUNT/FIRST/LASTを表示
        BEGIN
            DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY.COUNT = [' || V_ARRAY.COUNT || ']' );
        EXCEPTION
            WHEN OTHERS THEN
                DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY.COUNT = [' || SQLERRM || ']');
                RETURN;
        END;
        DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY.FIRST = [' || V_ARRAY.FIRST || ']' );
        DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY.LAST = [' || V_ARRAY.LAST || ']' );
        -- FIRST/LAST/EXISTSを用いてループ
        --      添え字が数字ならこの方法でもできる
        --      NUMBERでループするため、FIRSTやLASTがNULLの場合に処理できないので、COUNT>0で包む
        IF V_ARRAY.COUNT > 0 THEN
            FOR V_IDX IN V_ARRAY.FIRST .. V_ARRAY.LAST LOOP
                IF V_ARRAY.EXISTS ( V_IDX ) THEN
                    DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY(' || V_IDX || ') = [' || V_ARRAY(V_IDX) || ']' );
                END IF;
            END LOOP;
        END IF;
        -- FIRST/NEXTを用いてループ
        --      添え字が文字でも扱える
        V_IDX := V_ARRAY.FIRST;
        LOOP
            EXIT WHEN V_IDX IS NULL;
            DBMS_OUTPUT.PUT_LINE ( 'V_ARRAY(' || V_IDX || ') = [' || V_ARRAY(V_IDX) || ']' );
            V_IDX := V_ARRAY.NEXT(V_IDX);
        END LOOP;
    END;

BEGIN

    -- 結合配列
    DECLARE
        V_ARRAY     T_ARRAY1;
        V_EMPTY     T_ARRAY1;
    BEGIN
        DBMS_OUTPUT.PUT_LINE ( '■■■■■■■■■■■■■■■■■■■■' );
        DBMS_OUTPUT.PUT_LINE ( '■■■結合配列' );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■未初期化' );
        PRC_OUTPUT ( V_ARRAY );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■代入' );
        V_ARRAY(1) := '111';
        V_ARRAY(3) := '333';
        V_ARRAY(5) := '555';
        PRC_OUTPUT ( V_ARRAY );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■再度、未初期化' );
        V_ARRAY := V_EMPTY;
        PRC_OUTPUT ( V_ARRAY );
        --
    END;

    -- ネストした表
    DECLARE
        V_ARRAY     T_ARRAY2;
    BEGIN
        DBMS_OUTPUT.PUT_LINE ( '■■■■■■■■■■■■■■■■■■■■' );
        DBMS_OUTPUT.PUT_LINE ( '■■■ネストした表' );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■未初期化' );
        V_ARRAY := NULL;
        PRC_OUTPUT ( V_ARRAY );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■初期化済み(空)' );
        V_ARRAY := T_ARRAY2();
        PRC_OUTPUT ( V_ARRAY );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■初期化済み' );
        V_ARRAY := T_ARRAY2( '111', '333', '555' );
        PRC_OUTPUT ( V_ARRAY );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■初期化済み(途中DELETE)' );
        V_ARRAY := T_ARRAY2( '111', '333', '555' );
        V_ARRAY.DELETE(2);
        PRC_OUTPUT ( V_ARRAY );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■初期化済み(全DELETE)' );
        V_ARRAY := T_ARRAY2( '111', '333', '555' );
        V_ARRAY.DELETE;
        PRC_OUTPUT ( V_ARRAY );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■再度、未初期化' );
        V_ARRAY := T_ARRAY2( '111', '333', '555' );
        V_ARRAY := NULL;
        PRC_OUTPUT ( V_ARRAY );
        --
    END;

    -- 可変長配列(VARRAY)
    DECLARE
        V_ARRAY     T_ARRAY3;
    BEGIN
        DBMS_OUTPUT.PUT_LINE ( '■■■■■■■■■■■■■■■■■■■■' );
        DBMS_OUTPUT.PUT_LINE ( '■■■可変長配列(VARRAY)' );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■未初期化' );
        V_ARRAY := NULL;
        PRC_OUTPUT ( V_ARRAY );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■初期化済み(空)' );
        V_ARRAY := T_ARRAY3();
        PRC_OUTPUT ( V_ARRAY );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■初期化済み' );
        V_ARRAY := T_ARRAY3( '111', '333', '555' );
        PRC_OUTPUT ( V_ARRAY );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■初期化済み(全DELETE)' );
        V_ARRAY := T_ARRAY3( '111', '333', '555' );
        V_ARRAY.DELETE;
        PRC_OUTPUT ( V_ARRAY );
        --
        DBMS_OUTPUT.PUT_LINE ( '■■■■■再度、未初期化' );
        V_ARRAY := T_ARRAY3( '111', '333', '555' );
        V_ARRAY := NULL;
        PRC_OUTPUT ( V_ARRAY );
        --
    END;

END;

実行結果

■■■■■■■■■■■■■■■■■■■■
■■■結合配列
■■■■■未初期化
V_ARRAY.COUNT = [0]
V_ARRAY.FIRST = []
V_ARRAY.LAST = []
■■■■■代入
V_ARRAY.COUNT = [3]
V_ARRAY.FIRST = [1]
V_ARRAY.LAST = [5]
V_ARRAY(1) = [111]
V_ARRAY(3) = [333]
V_ARRAY(5) = [555]
V_ARRAY(1) = [111]
V_ARRAY(3) = [333]
V_ARRAY(5) = [555]
■■■■■再度、未初期化
V_ARRAY.COUNT = [0]
V_ARRAY.FIRST = []
V_ARRAY.LAST = []
■■■■■■■■■■■■■■■■■■■■
■■■ネストした表
■■■■■未初期化
V_ARRAY.COUNT = [ORA-06531: 参照しているコレクションは初期化されていません。]
■■■■■初期化済み(空)
V_ARRAY.COUNT = [0]
V_ARRAY.FIRST = []
V_ARRAY.LAST = []
■■■■■初期化済み
V_ARRAY.COUNT = [3]
V_ARRAY.FIRST = [1]
V_ARRAY.LAST = [3]
V_ARRAY(1) = [111]
V_ARRAY(2) = [333]
V_ARRAY(3) = [555]
V_ARRAY(1) = [111]
V_ARRAY(2) = [333]
V_ARRAY(3) = [555]
■■■■■初期化済み(途中DELETE)
V_ARRAY.COUNT = [2]
V_ARRAY.FIRST = [1]
V_ARRAY.LAST = [3]
V_ARRAY(1) = [111]
V_ARRAY(3) = [555]
V_ARRAY(1) = [111]
V_ARRAY(3) = [555]
■■■■■初期化済み(全DELETE)
V_ARRAY.COUNT = [0]
V_ARRAY.FIRST = []
V_ARRAY.LAST = []
■■■■■再度、未初期化
V_ARRAY.COUNT = [ORA-06531: 参照しているコレクションは初期化されていません。]
■■■■■■■■■■■■■■■■■■■■
■■■可変長配列(VARRAY)
■■■■■未初期化
V_ARRAY.COUNT = [ORA-06531: 参照しているコレクションは初期化されていません。]
■■■■■初期化済み(空)
V_ARRAY.COUNT = [0]
V_ARRAY.FIRST = []
V_ARRAY.LAST = []
■■■■■初期化済み
V_ARRAY.COUNT = [3]
V_ARRAY.FIRST = [1]
V_ARRAY.LAST = [3]
V_ARRAY(1) = [111]
V_ARRAY(2) = [333]
V_ARRAY(3) = [555]
V_ARRAY(1) = [111]
V_ARRAY(2) = [333]
V_ARRAY(3) = [555]
■■■■■初期化済み(全DELETE)
V_ARRAY.COUNT = [0]
V_ARRAY.FIRST = []
V_ARRAY.LAST = []
■■■■■再度、未初期化
V_ARRAY.COUNT = [ORA-06531: 参照しているコレクションは初期化されていません。]
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした