負荷テストデータ作成に関するTips -- 指定率行の抽出・更新 --

JPOUG Advent Calendar 2019の13日目のエントリーです。

負荷テスト用データは件数が重要

最近、負荷テストの支援をすることが多いのですが、負荷テスト用のデータ準備にはいつも悩まされます。
経験から述べると、アプリケーション開発者にデータの準備をお願いすると大抵はうまく行かないことが多いです。
開発者はプログラムが仕様通り動くかどうかを確認する必要最小限のデータを準備することに関心はありますが、実際の業務を想定したのデータを準備することはあまり考えていません。

また、本番環境の実データを使うにはセキュリティ上いろいろな制約があるため、機微な情報をマスク化する等の加工に時間がかかりただでさえ厳しいテスト工程を圧迫します。

結論から言うと、負荷テストでは値のリアルさよりも件数の妥当性の方が重要だと思います。
何年か前に参画したプロジェクトでは、アプリケーションの仕様を十分ヒアリングした上で、想定件数のテストデータをほぼゼロから作りそれなりの成果を得ました。しかも、アプリケーション開発工程の影響に左右されず先行的にデータの準備を行ったため、試験工程も余裕を持って実施することができました。

その経験から今回は「カーディナリティを考慮したデータを作成する」というテーマで、当時得たノウハウを共有したいと思います。

カーディナリティとは

「カーディナリティ」という用語をマニュアルでひもとくと「表の行数に対する個別値の数の比率。100万行を持つ表内で個別値が2つしかない列の場合、カーディナリティは低くなります。」とあります。

Oracle® Database データベース概要 19c  用語集

世の中的には「カーディナリティ」を異なる意味で使う場合もあるようですが、今回はOracleのマニュアルにある定義を使います。

参考:「カーディナリティて何ですの

さきほど、負荷テストデータは値のリアルさは重要でないと書きましたが、検索カラムのカーディナリティは実データから値の割合を調査しそれを基にテストデータを作成するのがよいと思います。

例えば、HR.EMPLYEES表から職種別構成比を表示させると以下のようになります。

SQL> set pages 50
SQL> col COMP_RATIO for 990.9
SQL> select
  2     JOB_TITLE
  3    ,COMP_RATIO
  4  from
  5    (
  6       select
  7         JOB_ID
  8        ,count(*) / TOTAL * 100 COMP_RATIO
  9        ,TOTAL
 10      from
 11        (
 12           select
 13             JOB_ID
 14            ,count(*) OVER() TOTAL
 15          from
 16            EMPLOYEES
 17        )
 18      group by
 19         JOB_ID
 20        ,TOTAL
 21    ) R
 22    inner join
 23      JOBS J
 24    on  R.JOB_ID = J.JOB_ID
 25  order by
 26     2 desc
 27    ,1
 28  ;

JOB_TITLE                           COMP_RATIO
----------------------------------- ----------
Sales Representative                      28.0
Shipping Clerk                            18.7
Stock Clerk                               18.7
Accountant                                 4.7
Programmer                                 4.7
Purchasing Clerk                           4.7
Sales Manager                              4.7
Stock Manager                              4.7
Administration Vice President              1.9
Accounting Manager                         0.9
Administration Assistant                   0.9
Finance Manager                            0.9
Human Resources Representative             0.9
Marketing Manager                          0.9
Marketing Representative                   0.9
President                                  0.9
Public Accountant                          0.9
Public Relations Representative            0.9
Purchasing Manager                         0.9

19行が選択されました。

「販売代理人は全社員の28.0%」、「出荷担当者は18.7%」というような実際の構成比を把握しておけば、EMPLYEES表にどんな大きな件数のテストデータを作成したとしても現実的な比率のJOB_IDを作成することができます。

負荷テスト用データ作成要領の概要

負荷テストデータを作成するには、ExcelでCSVを作成してSQL*Loaderでロードする等いろいろな方法が考えられますが、今回はテーブルを作成した後に、PL/SQLによってPK項目を生成しながら、PK項目以外のカラムに初期値(0やnull)をセットして、必要な行数のデータを作成するものとします。

その後、カラムごとに指定した割合で値を更新していくこととします。

検証用テーブルと初期データの作成

それでは、検証用のテーブルを作成し初期データを作成してみます。

PK項目のID列と任意の値が入るVAL1列、VAL2列を定義し、初期データとして100万件のデータをバルク・インサートで作成します。(話を簡単にするためすべてのカラムはNUMBER型とします。)

件数はループ変数の定義により任意に設定できるのであまり悩むことはないと思います。

SQL> create table TEST (
  2   ID   number
  3  ,VAL1 number
  4  ,VAL2 number
  5  );

表が作成されました。

SQL> declare
  2    type TBL_INS is table of TEST%rowtype index by pls_integer;
  3    W_INS TBL_INS;
  4  begin
  5    for i in 1..1000000 loop
  6      W_INS(i).ID   := i;
  7      W_INS(i).VAL1 := 0;
  8      W_INS(i).VAL2 := 0;
  9    end loop;
 10    forall i in 1..1000000 insert into TEST values W_INS(i);
 11      commit;
 12  end;
 13  /

PL/SQLプロシージャが正常に完了しました。

SQL> select count(*) from TEST;

  COUNT(*)
----------
   1000000

ROWID順に100行取り出してみる

念のため、ROWID順に先頭の100件を取り出してみます。ID=1から始まらないのがおもしろいところですが、ループ処理で配列へデータを順に作成しているので、同じブロックの中ではROWIDとIDの並び順は同じです。

VAL1とVAL2列は0で初期化されています。

SQL> select * from (
  2  select ROWID,ID,VAL1,VAL2 from TEST order by ROWID)
  3  where rownum <= 100;

ROWID                    ID       VAL1       VAL2
------------------ -------- ---------- ----------
AAAXhJAAGAAAADjAAA      567          0          0
AAAXhJAAGAAAADjAAB      568          0          0
AAAXhJAAGAAAADjAAC      569          0          0
AAAXhJAAGAAAADjAAD      570          0          0
AAAXhJAAGAAAADjAAE      571          0          0
AAAXhJAAGAAAADjAAF      572          0          0
AAAXhJAAGAAAADjAAG      573          0          0
AAAXhJAAGAAAADjAAH      574          0          0
AAAXhJAAGAAAADjAAI      575          0          0
AAAXhJAAGAAAADjAAJ      576          0          0
.................................................
AAAXhJAAGAAAADjABa      657          0          0
AAAXhJAAGAAAADjABb      658          0          0
AAAXhJAAGAAAADjABc      659          0          0
AAAXhJAAGAAAADjABd      660          0          0
AAAXhJAAGAAAADjABe      661          0          0
AAAXhJAAGAAAADjABf      662          0          0
AAAXhJAAGAAAADjABg      663          0          0
AAAXhJAAGAAAADjABh      664          0          0
AAAXhJAAGAAAADjABi      665          0          0
AAAXhJAAGAAAADjABj      666          0          0

100行が選択されました。

このTEST表のVAL1、VAL2に対し、全体(100万件)のうち指定した割合で値を更新していきます。

SAMPLE句で指定した%の行を取り出す

割合を指定して行を取り出すにはSAMPLE句の使用が考えられます。

以下は、SAMPLE句の説明の抜粋です。

sample_percent サンプルの選択: 例 参照

「sample_percentには、全体の行またはブロック数のうち、サンプルに入れる割合(%)を指定します。
0.000001以上100未満の範囲の値を指定します。この割合は、各行(ブロック・サンプリングの場合は行の各クラスタ)が、サンプルの一部として選択される可能性を示します。
sample_percentに指定した割合の行がtableから正確に取り出されるわけではありません。」

とりあえず0.1%(1000行になるはず)の行を取り出してみます。

SQL> select ROWID,ID from TEST sample(0.1) order by ROWID;

ROWID                      ID
------------------ ----------
AAAXhJAAGAAAADjAAa        593
AAAXhJAAGAAAADmADd       2465
AAAXhJAAGAAAADoAEb       4204
AAAXhJAAGAAAADpABw       4592
AAAXhJAAGAAAADpAGs       4908
AAAXhJAAGAAAADpAIm       5030
AAAXhJAAGAAAADqAC7       5226
AAAXhJAAGAAAADrABB       5663
AAAXhJAAGAAAADrAEv       5901
AAAXhJAAGAAAADsADX       6372
.............................
AAAXhJAAGAAAAirAGU     988784
AAAXhJAAGAAAAiuABe     980689
AAAXhJAAGAAAAiyACM     981254
AAAXhJAAGAAAAi0AEa     998004
AAAXhJAAGAAAAi0AFQ     998058
AAAXhJAAGAAAAi6AAS     982170
AAAXhJAAGAAAAi8AEQ     999032
AAAXhJAAGAAAAi+ABq     982777
AAAXhJAAGAAAAi+AGR     983072
AAAXhJAAGAAAAi/AEd     991260

982行が選択されました。

SAMPLE句は正確な行数を取り出さない

SAMPLE句では内部的に乱数を使ってサンプルを取得しているようなので、以下のように取得の度に行数が異なります。指定する%の精度を高くしても(小数点以下の桁数を大きくしても)この傾向は変わりません。しかも、けっこう誤差が大きいです。

SQL> select count(*) from (
  2  select ROWID,ID from TEST sample(0.100000) order by ROWID
  3  );

  COUNT(*)
----------
       994

経過: 00:00:00.01
SQL> /

  COUNT(*)
----------
       996

経過: 00:00:00.01
SQL> /

  COUNT(*)
----------
       898

経過: 00:00:00.01
SQL> /

  COUNT(*)
----------
      1006

SAMPLE句はSEEDを指定すれば同じ結果を返す(ただし行数は不正確)

ただし、乱数を発生させている(と思われる)ので、以下のようにシードを指定すると同じ結果になります。ただし、行数は所望の1000行とはなりません。

SQL> select count(*) from (
  2  select ROWID,ID from TEST sample(0.100000) seed(1) order by ROWID
  3  );

  COUNT(*)
----------
       980

経過: 00:00:00.01
SQL> /

  COUNT(*)
----------
       980

経過: 00:00:00.00
SQL> select count(*) from (
  2  select ROWID,ID from TEST sample(0.100000) seed(10) order by ROWID
  3  );

  COUNT(*)
----------
      1014

経過: 00:00:00.00
SQL> /

  COUNT(*)
----------
      1014

経過: 00:00:00.00
SQL> select count(*) from (
  2  select ROWID,ID from TEST sample(0.100000) seed(100) order by ROWID
  3  );

  COUNT(*)
----------
      1005

経過: 00:00:00.01
SQL> /

  COUNT(*)
----------
      1005

SAMPLE句で取り出した行(カラム)をUpdateしてみる

正確に1000行ではないですが、取り出したIDを使ってVAL1列を0→1に更新してみます。

ところがどっこい、ORA-30560エラーが発生してしまいます。

SQL> update TEST set VAL1 = 1 where ID in (select ID from TEST sample(0.100000));
update TEST set VAL1 = 1 where ID in (select ID from TEST sample(0.100000))
                                                          *
行1でエラーが発生しました。:
ORA-30560: SAMPLE句は使用できません

SQL> update TEST set VAL1 = 1 where ID in (select ID from TEST sample(0.100000) seed(1));
update TEST set VAL1 = 1 where ID in (select ID from TEST sample(0.100000) seed(1))
                                                          *
行1でエラーが発生しました。:
ORA-30560: SAMPLE句は使用できません

乱数でランダムに行(ROWID)を指定しているので、更新行が特定できず、つまり何度やっても同じ結果にならないので、更新できないのでしょうか?

シードを指定してもエラーになりましたので、SAMPLE句が使えないのはOracleの仕様なんでしょう。

SAMPLE句の制限事項

実は、マニュアルをよく読むと「DMLの副問い合わせの中ではSAMPLE句を指定できない」という記述があります。

  • SAMPLE句は、DML文の副問合せの中では指定できません
  • SAMPLE句を問合せで指定できるのは、問合せの対象が実表、マテリアライズド・ビューのコンテナ表、またはキー保存であるビューである場合です。

正攻法で考えてみる

SAMPLE句のような変化球ではなく、正攻法で考えてみます。(少し、前振りが長すぎました。)

結論から書きますが、全体行のうち更新したい行数の割合を「更新率」とすると(例えば100万行のうち1000行更新したければ「更新率=0.1%」)、以下のUPDATE文1により更新が可能です。

update 更新TBL set 更新列 = 更新値 where PK列 in (
 select PK列 from (
 select PK列,mod(PK列, (1 / 更新率)) from 更新TBL)
 where rownum <= 全体件数 * 更新率
 );

つまり、このUPDATE文を実行するためには、予め全体件数を(SELECT COUNT(*) FROM ~)で調べておく必要があります。

それでは、TEST表のVAL1列を更新率0.1%で0→1に更新してみましょう。

SQL> update TEST set VAL1 = 1 where ID in (
  2  select ID from (
  3  select ID,mod(ID, (100 / 0.1)) from TEST)  -- 更新率(0.1%)の逆数
  4  where rownum <= 1000000 * (0.1 / 100)      -- 全体件数 * 更新率 = 更新件数
  5  );

1000行が更新されました。

SQL> commit;

コミットが完了しました。

SQL> select VAL1,count(*) from TEST group by VAL1 order by VAL1;

      VAL1   COUNT(*)
---------- ----------
         0     999000
         1       1000

別の値の更新も自由自在

次に同じVAL1列を更新率2%で0→2に更新してみましょう。

注意したいのは先ほど1に更新した行は更新してはいけないことです。

SQL> update TEST set VAL1 = 2 where ID in (
  2  select ID from (
  3  select ID,mod(ID, (100 / 2)) from TEST where VAL1 != 1 and VAL1 = 0)  -- 更新率(2%)の逆数
  4  where rownum <= 1000000 * (2 / 100)                      -- 全体件数 * 更新率 = 更新件数
  5  );

20000行が更新されました。
SQL> select VAL1,count(*) from TEST group by VAL1 order by VAL1;

      VAL1   COUNT(*)
---------- ----------
         0     979000
         1       1000
         2      20000

所望の結果となりました。

別カラムの更新

次に、VAL2列も更新してみます。

以下のUPDATE文では、VAL1列の値とは無関係にVAL2を更新率18.7%で更新しています。

SQL> update TEST set VAL2 = 1 where ID in (
  2  select ID from (
  3  select ID,mod(ID, (100 / 18.7)) from TEST)  -- 更新率(18.7%)の逆数
  4  where rownum <= 1000000 * (18.7 / 100)      -- 全体件数 * 更新率 = 更新件数
  5  );

187000行が更新されました。

SQL> commit;

コミットが完了しました。

SQL> select VAL2,count(*) from TEST group by VAL2 order by VAL2;

      VAL2   COUNT(*)
---------- ----------
         0     813000
         1     187000

SQL> select VAL1,VAL2,count(*) from TEST group by VAL1,VAL2 order by VAL1,VAL2;

      VAL1       VAL2   COUNT(*)
---------- ---------- ----------
         0          0     813000
         0          1     166000
         1          1       1000
         2          1      20000

PK列がNUMBER型でなくても更新できる

今までの例ではPK列(ID)がNUMBER型であったので、mod関数の引数としてそのまま指定できました。

ということで、PK列が暗黙的にでも数値に変換できない値の場合であっても使えるように工夫してみたのが以下のUPDATE2文です。

update 更新TBL set 更新列 = 更新値 where PK列 in (
 select PK列 from (
 select PK列,mod(row_number() over(order by PK列), (1 / 更新率)) from 更新TBL)
 where rownum <= 全体件数 * 更新率
 );

それでは、別の100万行のテーブルTEST2に対して、指定した更新率(28%)でのUPDATEを行ってみます。

SQL> select count(*) from TEST2;

  COUNT(*)
----------
   1000000

1行が選択されました。

SQL> select * from TEST2 where rownum<11;

ID               VAL1
---------- ----------
ABC0000404          0
ABC0000405          0
ABC0000406          0
ABC0000407          0
ABC0000408          0
ABC0000409          0
ABC0000410          0
ABC0000411          0
ABC0000412          0
ABC0000413          0

10行が選択されました。
SQL> update TEST2 set VAL1 = 1 where ID in (
  2  select ID from (
  3  select ID,mod(row_number() over(order by ID), (100 / 28.0)) from TEST2)  -- 更新率(28.0%)の逆数
  4  where rownum <= 1000000 * (28.0 / 100)                                   -- 全体件数 * 更新率 = 更新件数
  5  );

280000行が更新されました。

SQL> commit;

コミットが完了しました。

SQL> select VAL1,count(*) from TEST2 group by VAL1 order by VAL1;

      VAL1   COUNT(*)
---------- ----------
         0     720000
         1     280000

2行が選択されました。

PK列がNUMBER型であっても、飛び番が発生していたりすると期待した結果とならない可能性があるので、どんな場合でもUPDATE文2の使用をおすすめします。

それでは