Oracleでパーセンタイルを求める

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

はじめに

今年の後半は「Oracle技術者から見た、SAP HANA」というDB Onlineの記事執筆で忙しかったこともあって、個人ブログの更新ができていませんでしたが、Advent Calendarといういいきっかけをいただいたので久しぶりの投稿です。(去年も同じようなことを言っていたような。。。)

ちなみにSAP HANAの連載はまだまだ続きますので、ご興味のある方は是非見てください!

今回のネタは「パーセンタイル」です。

パーセンタイルは、数学的な定義(Wikipedia)はとりあえず横に置きますが、われわれOracleエンジニアにとってレスポンスタイムの評価などでなじみがあると思います。

簡単に言うと100個の測定値を値の順に並べて、小さい方から90番目の値を「90パーセンタイル」あるいは「90%ile」と表現します。

JMeter等の負荷テストツールでも90%ile値は結果に表示されますが、なぜレスポンスタイムの評価に90%ile値が使われるのでしょうか?

これには諸説あると思いますが、私は以下の記述を参考にしています。

■体感レスポンスタイムとは

「体感レスポンスタイムとは、タスクを実行するのにかかったとユーザが感じる時間のことです。これは、最も長いレスポンスタイムの影響を非常に強く受けます。経験的には、体感レスポンスタイムの平均値はレスポンスタイム分布の90%値近辺と言われています。(後略)」
データベースチューニング256の法則 上 P.49~

蛇足ですが、「キャッシュヒット率が90%を下回ると急激に性能が悪化する。」というのは、これも一因なのではないかと私は解釈しています。

パーセンタイルを求める2つの関数

Oracleにパーセンタイルを求める関数には「PERCENTILE_CONT」、「PERCENTILE_DISC」の2つがあります。(この他に近似値を求める「APPROX_PERCENTILE」がありますがリンクだけ貼っておきます。)
また「MEDIAN」関数も広義にはパーセンタイルを求める関数と言えないこともないですが、これについては後述します。

これらの関数はSQL ServerやPostgresなど他のRDBMSにもあるようですが、「CONT」や「DISC」というのは何の略なのか日本語のマニュアルを見てもよくわかりませんので英語のマニュアルも参照してみましょう。

PERCENTILE_CONT

12cR2マニュアル(英語)
12cR2マニュアル(日本語)

PERCENTILE_CONT(expr) WITHIN GROUP
 (ORDER BY expr [ DESC | ASC ])
 [ OVER (query_partition_clause) ]

Purpose

目的

PERCENTILE_CONT is an inverse distribution function that assumes a continuous distribution model.

PERCENTILE_CONTは、連続分散モデルを想定する逆分散関数です。

It takes a percentile value and a sort specification, and returns an interpolated value that would fall into that percentile value with respect to the sort specification.

このファンクションは、パーセンタイル値およびソート指定を使用し、そのソート指定に従ってそのパーセンタイル値に該当する補間された値を戻します。
(中略)

The first expr must evaluate to a numeric value between 0 and 1, because it is a percentile value.

最初のexprは、パーセンタイル値であるため、0から1の数値で評価します。

This expr must be constant within each aggregation group.

このexprは、各集計グループ内の定数である必要があります。

The ORDER BY clause takes a single expression that must be a numeric or datetime value, as these are the types over which Oracle can perform interpolation.

ORDER BY句には、Oracleが補間を実行できる型である数値または日時値の単一式を指定します

The result of PERCENTILE_CONT is computed by linear interpolation between values after ordering them.

PERCENTILE_CONTの結果は、順序付けされた後の値間の直線補間によって計算されます。

Using the percentile value (P) and the number of rows (N) in the aggregation group, you can compute the row number you are interested in after ordering the rows with respect to the sort specification.

This row number (RN) is computed according to the formula RN = (1+(P*(N-1)).

The final result of the aggregate function is computed by linear interpolation between the values from rows at row numbers CRN = CEILING(RN) and FRN = FLOOR(RN).
The final result will be:

If (CRN = FRN = RN) then the result is
   (value of expression from row at RN)
Otherwise the result is
   (CRN - RN) * (value of expression for row at FRN) +
   (RN - FRN) * (value of expression for row at CRN)

PERCENTILE_DISC

12cR2マニュアル(英語)
12cR2マニュアル(日本語)

PERCENTILE_DISC(expr) WITHIN GROUP
 (ORDER BY expr [ DESC | ASC ])
 [ OVER (query_partition_clause) ]

Purpose

目的

PERCENTILE_DISC is an inverse distribution function that assumes a discrete distribution model.

PERCENTILE_DISCは、不連続分散モデルを想定する逆分散関数です。
(後略)

For a given percentile value P, PERCENTILE_DISC sorts the values of the expression in the ORDER BY clause and returns the value with the smallest CUME_DIST value (with respect to the same sort specification) that is greater than or equal to P.

指定されたパーセンタイル値Pに対して、PERCENTILE_DISCは、ORDER BY句の式の値をソートし、P以上である(同じソート指定に従う)最小CUME_DIST値を持つ値を戻します。

つまり、パーセンタイルが要素の間に存在する場合

    • CONTinuous:連続:補間して算出
    • DISCrete:不連続 :隣り合う要素でソート順で先に来る方

となります。

SQL実行例

それでは、マニュアルに記載されている集計の例をそのまま実行してみます。
この例は50パーセンタイルをPERCENTILE_CONTとPERCENTILE_DISCの両方で算出しています。
SALARY列の降順でソートしていることに注目してください。

SQL> show user
USER is "HR"
SQL> SELECT department_id,
  2         PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY salary DESC) "Median cont",
  3         PERCENTILE_DISC(0.5) WITHIN GROUP (ORDER BY salary DESC) "Median disc"
  4    FROM employees
  5    GROUP BY department_id
  6    ORDER BY department_id;

DEPARTMENT_ID Median cont Median disc
------------- ----------- -----------
           10        4400        4400
           20        9500       13000
           30        2850        2900
           40        6500        6500
           50        3100        3100
           60        4800        4800
           70       10000       10000
           80        8900        9000
           90       17000       17000
          100        8000        8200
          110       10154       12008
                     7000        7000

12 rows selected.

結果から、”PERCENTILE_CONT” =< ”PERCENTILE_DISC”となっていることがわかります。
(ちなみにPERCENTILE_CONTは、集計列のソート順に関わらず同じ結果となります。)

MEDIAN関数は50パーセンタイル

SQL> SELECT department_id, MEDIAN(salary)
  2    FROM employees
  3    GROUP BY department_id
  4    ORDER BY department_id;

DEPARTMENT_ID MEDIAN(SALARY)
------------- --------------
           10           4400
           20           9500
           30           2850
           40           6500
           50           3100
           60           4800
           70          10000
           80           8900
           90          17000
          100           8000
          110          10154
                        7000

12 rows selected.

マニュアルにも記述がありますが「MEDIANは、パーセンタイル値がデフォルトで0.5に指定される特別なPERCENTILE_CONTです。」

実際のデータ分布を見てみる

EMPLOYEES表をDEPARTMENT_IDでグルーピングし、それぞれRANK関数で値の順位を確認してみます。(同じ値は当然同じ順位となります。)

順位数が奇数のDEPARTMENT_IDの場合は中央値(M)が存在しますが、偶数の場合は計算の結果50パーセンタイルが決定されます。(D)

また、D値の横に対応するPERCENTILE_CONT(0.5)の値(C)を表示しています。

SQL> set pages 100
SQL> break on department_id skip page
SQL> SELECT department_id,salary
  2  ,RANK() OVER(PARTITION BY department_id ORDER BY salary DESC) rank
  3  FROM employees
  4  ORDER BY department_id,salary DESC;

DEPARTMENT_ID     SALARY       RANK
------------- ---------- ----------
           10       4400          1  ←M

DEPARTMENT_ID     SALARY       RANK
------------- ---------- ----------
           20      13000          1  ←D(C=9500)
                    6000          2

DEPARTMENT_ID     SALARY       RANK
------------- ---------- ----------
           30      11000          1
                    3100          2
                    2900          3  ←D(C=2850)
                    2800          4
                    2600          5
                    2500          6

DEPARTMENT_ID     SALARY       RANK
------------- ---------- ----------
           40       6500          1  ←M

DEPARTMENT_ID     SALARY       RANK
------------- ---------- ----------
           50       8200          1
                    8000          2
                    7900          3
...................................
                    3200         17
                    3100         21
                    3100         21  ←M
                    3100         21
                    3000         24
...................................
                    2200         43
                    2200         43
                    2100         45

DEPARTMENT_ID     SALARY       RANK
------------- ---------- ----------
           60       9000          1
                    6000          2
                    4800          3  ←M
                    4800          3
                    4200          5

DEPARTMENT_ID     SALARY       RANK
------------- ---------- ----------
           70      10000          1  ←M

DEPARTMENT_ID     SALARY       RANK
------------- ---------- ----------
           80      14000          1
                   13500          2
                   12000          3
...................................
                    9500         13
                    9000         16
                    9000         16  ←M
                    8800         18
                    8600         19
                    8400         20
...................................
                    6200         32
                    6200         32
                    6100         34

DEPARTMENT_ID     SALARY       RANK
------------- ---------- ----------
           90      24000          1
                   17000          2  ←M
                   17000          2

DEPARTMENT_ID     SALARY       RANK
------------- ---------- ----------
          100      12008          1
                    9000          2
                    8200          3  ←D(C=8000)
                    7800          4
                    7700          5
                    6900          6

DEPARTMENT_ID     SALARY       RANK
------------- ---------- ----------
          110      12008          1  ←D(C=10154)
                    8300          2

DEPARTMENT_ID     SALARY       RANK
------------- ---------- ----------
                    7000          1  ←M

107 rows selected.

ここからが本題!

RESPONCE_TIME表の作成

それでは、レスポンスタイムデータを擬似的に作成してパーセンタイルを求めるところまでをやってみます。

まず最初に、RESPONCE_TIME表の作成です。ID列とレスポンスタイムを格納するRT列からなる単純なテーブルです。

SQL> conn test/test
Connected.
SQL> CREATE TABLE responce_time (
  2   id NUMBER
  3  ,rt NUMBER);

Table created.

SQL> desc responce_time
 Name  Null?    Type
 ----- -------- ---------
 ID             NUMBER
 RT             NUMBER

レスポンス時間データを作る

次に、DBMS_RANDOMパッケージのNORMALファンクションを使用して標準正規分布の乱数を発生させ、想定する平均レスポンスタイム3秒前後のデータを10000件作成します。

SQL> BEGIN
  2    FOR i IN 1..10000 LOOP
  3      INSERT INTO responce_time
  4      VALUES (i,3+DBMS_RANDOM.NORMAL);
  5    END LOOP;
  6  END;
  7  /

PL/SQL procedure successfully completed.

SQL> COMMIT;

Commit complete.

データの確認

念のためID列でソートしてデータの作成状況を確認します。
10000件のデータが作成されていることがわかります。

SQL> col rt for 0.999
SQL> SELECT * FROM responce_time
  2  ORDER BY id;

        ID     RT
---------- ------
         1  3.239
         2  3.613
         3  3.419
         4  3.388
         5  4.443
         6  3.775
         7  2.510
         8  4.597
.................
      9992  3.144
      9993  1.849
      9994  4.021
      9995  4.205
      9996  3.045
      9997  2.383
      9998  4.202
      9999  2.183
     10000  3.771

10000 rows selected.

90パーセンタイルの確認

それでは、90パーセンタイルを求めてみましょう。パーセンタイル値は「0.9」となります。
念のためPERCENTILE_DISCとPERCENTILE_CONT、参考に最小値、中央値、平均値、最大値も確認します。

SQL> col 90%ile_cont for 90.99999
SQL> col 90%ile_disc for 90.99999
SQL> col MAX for 90.99999
SQL> col MIN for 90.99999
SQL> col MED for 90.99999
SQL> col AVG for 90.99999
SQL> SELECT
  2   MIN(rt) MIN
  3  ,MEDIAN(rt) MED
  4  ,AVG(rt) AVG
  5  ,PERCENTILE_DISC(0.9) WITHIN GROUP (ORDER BY rt) "90%ile_disc"
  6  ,PERCENTILE_CONT(0.9) WITHIN GROUP (ORDER BY rt) "90%ile_cont"
  7  ,MAX(rt) MAX
  8    FROM responce_time;

      MIN       MED       AVG 90%ile_disc 90%ile_cont       MAX
--------- --------- --------- ----------- ----------- ---------
 -1.00573   2.96475   2.98250     4.24513     4.24515   6.53524

レスポンスタイムの90パーセンタイルは「4.245秒」であることがわかります。

レスポンスタイムの場合は連続分散モデルを想定する方が自然なため「PERCENTILE_CONT」を使用する方が良いと思います。
(RT列の昇順(デフォルト)でソートしているため、
”PERCENTILE_DISC” =< ”PERCENTILE_CONT”となります。)

95パーセンタイルの確認

95パーセンタイルの場合は、引数を「0.95」とするだけです。
4.62秒」となることがわかります。

SQL> SELECT
  2   PERCENTILE_DISC(0.95) WITHIN GROUP (ORDER BY rt) "95%ile_disc"
  3  ,PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY rt) "95%ile_cont"
  4    FROM responce_time;

95%ile_disc 95%ile_cont
----------- -----------
    4.62042     4.62043

99パーセンタイルの確認

同様に99パーセンタイルは「5.294秒」となります。
つまり、99パーセンタイルよりも90パーセンタイルの方がレスポンスタイム目標としては厳しいものとなります。

SQL> SELECT
  2   PERCENTILE_DISC(0.99) WITHIN GROUP (ORDER BY rt) "99%ile_disc"
  3  ,PERCENTILE_CONT(0.99) WITHIN GROUP (ORDER BY rt) "99%ile_cont"
  4    FROM responce_time;

99%ile_disc 99%ile_cont
----------- -----------
    5.29363     5.29364

正規分布を可視化する

それではおまけとして、作成した10000件のデータの分布をExcelで可視化してみます。

Excel計算式

  • レスポンス時間データを昇順にソートしB列に貼り付けます。
  • A列は1~10000の順番を示します。90パーセンタイル即ち9000/10000のデータは「4.24513306」となります。
  • C列にはB列を基にした、確率密度を求める式を記述します。「NORM.DIST」関数を使い、平均=3(sec)、標準偏差=1、関数形式=FALSEを指定します。

標準正規分布曲線

横軸にレスポンス時間、縦軸に確率密度となるグラフを描画すると下の図のようになります。
(赤線で90%tileの補助線を書いています。)


実際にやっている事例を見たことはないのですが、アクセスログをデータベースに取り込んで、PERCENTILE_CONT関数でレスポンス時間90パーセンタイルの確認を定期的に行うような運用をすれば、サービスレベルのチェックに使えるのではないかと思います。

明日は、おおのたかしさんの12cR2ネタです。