OracleDBのV$SESSION.EVENTを勘違いしていた話

プロフィールに「SQLチューニングチョットデキル」と書いておきながら、10年以上に渡り`V$SESSION`の`EVENT`列について基本的な勘違いをしていたことに気付いたので、自分への戒めとして再現方法含め記事に残しておいうと思います。

ちなみに、`V$SESSION.EVENT`列は現在セッションが待機中の待機イベントを指すものではないことが分かっている方には、あまり有用な記事ではないです。

# 1. V$SESSIONのモニタリングとEVENT列
「門外不出のOracle現場ワザ」という書籍（Web抜粋版は[こちら](https://www.oracle.com/technetwork/jp/articles/index-082145-ja.html 門外不出のOracle現場ワザ)）に、`V$SESSION`の情報を定期取得（5秒に1回など）することでOracleDBの性能問題の状況をリアルタイムに把握するという手法が載っています。ざっくり言うと、次のようなSQL文を実行すれば、ある程度状況が把握できるという方法です。

```SQL:V$SESSIONからの情報取得
SELECT
    sid,
    serial#,
    program,
    status,
    sql_id,
    event
FROM
    v$session
WHERE
    username = '（アプリが利用するDBユーザ名）'
```

この方法はAWR/Statspackレポートを取得したり、大げさに情報収集のスクリプトを仕掛けたりせずとも手軽に現時点の以下のような状況が手軽に分かります。

- 接続数はどれぐらいか
- 同時実行数はどれぐらいか
- どのSQL文がたくさん実行されているか（`SQL_ID`列より）
- SQL文実行は何で待たされているか（`EVENT`列より）

この手軽さから、私は今までSQLチューニングの現場でこの手法を多用してきました。

しかし、先週にこの手法の中でも重要な`EVENT`列について大きな勘違いをしていることに気づきました。
具体的には、`EVENT`列にある待機イベントは該当のセッションが今現在待機しているイベントであり、メモリからのデータ読み取りや計算処理などCPUを使う処理の場合（ラッチ除く）はアイドル系の待機イベントが表示されるものと思っていました。

ちなみに、[Oracle Database 18cのDatabase Reference](view-source:https://docs.oracle.com/en/database/oracle/oracle-database/18/refrn/V-SESSION.html#GUID-28E2DC75-E157-4C0A-94AB-117C205789B9)では`EVENT`列には以下のような説明がついています。これ見たら、「上述のような勘違いも仕方なくないですか」と思わなくはないですが。

> （英語）
> Resource or event for which the session is waiting
>
> （日本語）
> セッションが待機しているリソースまたはイベント

以下では、`EVENT`列≠（現在待機中のイベント）となる再現ケースを説明していきたいと思います。

# 2. 再現用データ＆SQL文

## 2-1. データモデル
以下のようなテーブルを使います。

![er.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388675/8723c630-643b-2a35-b0c3-5eea8cbb28df.png)

- 作業者（`WORKER`）への仕事（`TASK`）のアサインを管理します。
    - 作業者と仕事の関係は1対多とします。
    - 仕事は完了したら物理削除されるものとします。
- 削除予定作業者（`DELETED_WORKER`）に登録されている作業者を作業者テーブル（`WORK`）から定期的に削除します。

DDL文は以下になります。

```sql:DDL文
CREATE TABLE worker (
    worker_id   NUMBER(8)    NOT NULL,
    worker_name VARCHAR2(10) NOT NULL,
    CONSTRAINT pk_worker PRIMARY KEY (worker_id)
);

CREATE TABLE task (
    task_id    NUMBER(8)    NOT NULL,
    task_name  VARCHAR2(10) NOT NULL,
    worker_id NUMBER(8)    NOT NULL,
    CONSTRAINT pk_task PRIMARY KEY (task_id),
    CONSTRAINT fk_task_01 FOREIGN KEY (worker_id)
      REFERENCES worker(worker_id)
);

CREATE TABLE deleted_worker (
    worker_id NUMBER(8)    NOT NULL
);
```

これはOracleにおける有名なアンチパターンが含まれているのですが、ポイントになるのは以下の2点です。

- `TASK`テーブルから`WORKER`テーブルに参照制約が作成されている。
- `TASK`テーブルの参照キーには索引が作成されていない。

これで何が起こるかというと、`WORKER`テーブルから1行データを削除する際に、参照制約のため該当`WORKER`レコードに紐付く`TASK`レコードが存在しないかチェックする処理が裏で動きます。そのチェック（`SELECT count(1) FROM TASK WHERE WORKER_ID = ?`みたいな処理）のためにTASKテーブルのフルスキャンが実行されてしまいます。

（`WORKER`レコードの削除時に`TASK`テーブルにテーブルロックがかかるという問題もありますが、今回はあまり関係ありません）

## 2-2. データ生成

以下のスクリプトでデータ生成します。

- `WORKER`テーブル10,000万件（約3MB）
- `TASK`テーブル：8,000件（約3MB）
- `DELETED_WORKER`テーブル：2,000万件（約200KB）

```sql:データ生成
INSERT INTO worker
SELECT
    1000000 + id,
    'W' || to_char(1000000 + id, 'FM00000000')
FROM
    (
    SELECT
        level id
    FROM
        dual
    CONNECT BY LEVEL <= 100000
    );

INSERT INTO task
SELECT
    worker_id,
    'T' || to_char(1000000 + worker_id, 'FM00000000'),
    worker_id
FROM
    worker
WHERE
    mod(worker_id, 10) != 0;

INSERT INTO deleted_worker
SELECT
    worker_id
FROM
    worker
WHERE
    mod(worker_id, 10) = 0;

COMMIT;
```
## 2-3. モニタリング対象処理

以下のDELETE文をモニタリングします。`WORKER`テーブルから2,000件削除する際に、8,000件の`TASK`テーブルフルスキャンが2,000回内部で実行されます。今回利用した環境では15秒程度掛かります。

```sql:モニタリング対象処理
DELETE FROM worker w
WHERE
	  EXISTS (
	      SELECT 1
	      FROM deleted_worker d
	      WHERE d.worker_id = w.worker_id
	  );
```

## 2-4. 測定環境

あまり重要ではないですが、今回の環境は以下です。

- VirtualBox6.0.2上の仮想環境（CPU2コア、メモリ2GB）
- OS：CentOS 7.6.1810 (x86_64)
- Oracle Database 18c 18.3.0.0.0

# 3. モニタリング

上述のDELETE文を以下の`V$SESSION`へのSQL文で1秒ごとにモニタリングしながら実行します。

```sql:V$SESSIONモニタリング
SELECT
    to_char(sysdate, 'HH24:MI:SS') tm, 
    sid,
    sql_id,
    status,
    wait_time,
    state,
    event
FROM v$session 
WHERE username = 'EV_TEST';
```

## 3-1. ケース１（バッファキャッシュ全クリア後）

一度キャッシュデータを`ALTER SYSTEM FLUSH BUFFER_POOL ALL`で全削除した上でDELETE文を実行します。以下がその時の`V$SESSION`モニタリングの結果です。

```
TM         SID SQL_ID        STATUS    WAIT_TIME STATE               EVENT
-------- ----- ------------- -------- ---------- ------------------- -------------------------
13:59:51   295 7hys3h7ysgf9m ACTIVE            0 WAITING             db file sequential read
13:59:52   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
13:59:53   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
13:59:54   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
13:59:55   295 czf8yap5hgng7 ACTIVE            1 WAITED KNOWN TIME   db file scattered read
13:59:56   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
13:59:57   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
13:59:58   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
13:59:59   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
14:00:00   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
14:00:01   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
14:00:02   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
14:00:03   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
14:00:04   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
14:00:05   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
14:00:06   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
14:00:07   295 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   db file scattered read
```

`EVENT`列は最初を除いて`db file scattered read`になっているので、一見テーブルフルスキャンで長い時間待機しているように見えますが、`WAIT_TIME`列が-1であるため、実際は待機中ではなくCPUで処理中になります。

以下、`WAIT_TIME`列のマニュアルによる説明。

> （英語）
> If the session is currently waiting, then the value is 0. If the session is not in a wait, then the value is as follows:
>  \>0 - Value is the duration of the last wait in hundredths of a second
>  -1 - Duration of the last wait was less than a hundredth of a second
>  -2 - Parameter TIMED_STATISTICS was set to false
> This column has been deprecated in favor of the columns WAIT_TIME_MICRO and STATE.
>
> （日本語）
> セッションが現在待機中の場合、値は0。セッションが待機中でない場合、値は次のとおり。
>   \>0 - 値は、前回の待機時間(1/100秒)
> -1 - 前回の待機時間は1/100秒未満だった
> -2 - パラメータTIMED_STATISTICSは、falseに設定された
> WAIT_TIME_MICROおよびSTATE列が設定されている場合、この列は非推奨になる。

（本当は`WAIT_TIME`列ではなく`STATE`列を見た方が良いのかもしれませんが。）

実際、`V$SESSION_EVENT`でどの待機ベントでどれだけ待ったかを確認すると以下のようになり、`db file scattered read`では1.2秒しか待機していないことが分かります。

```
14:00:07 SQL> 
14:00:07 SQL> SELECT
14:00:07   2  	  event,
14:00:07   3  	  time_waited_micro / 1000 "wait time (ms)"
14:00:07   4  FROM
14:00:07   5  	  v$session_event
14:00:07   6  WHERE
14:00:07   7  	  sid = sys_context('USERENV', 'SID');

EVENT                                    wait time (ms)
---------------------------------------- --------------
Disk file operations I/O                             22
log file sync                                        34
db file sequential read                           1,157
db file scattered read                              214
SQL*Net message to client                             0
SQL*Net message from client                          13
events in waitclass Other                             1

7行が選択されました。
```
## 3-2. ケース２（チェックポイント＆ログスイッチが起こる場合）

DELETE文実行中にチェックポイント＆ログスイッチが起きた場合の`V$SESSION`のモニタリング結果は以下になります。

```
TM         SID SQL_ID        STATUS    WAIT_TIME STATE               EVENT
-------- ----- ------------- -------- ---------- ------------------- --------------------------------------------------
22:29:05   252 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   PGA memory operation
22:29:06   252 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   enq: CR - block range reuse ckpt
22:29:07   252 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   enq: CR - block range reuse ckpt
22:29:08   252 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   enq: CR - block range reuse ckpt
22:29:09   252 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   enq: CR - block range reuse ckpt
22:29:10   252 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   enq: CR - block range reuse ckpt
22:29:11   252 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   enq: CR - block range reuse ckpt
22:29:12   252 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   enq: CR - block range reuse ckpt
22:29:13   252 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   enq: CR - block range reuse ckpt
22:29:14   252 czf8yap5hgng7 ACTIVE           -1 WAITED SHORT TIME   enq: CR - block range reuse ckpt
22:29:15   252 czf8yap5hgng7 ACTIVE            7 WAITED KNOWN TIME   log file switch (private strand flush incomplete)
22:29:16   252 czf8yap5hgng7 ACTIVE            7 WAITED KNOWN TIME   log file switch (private strand flush incomplete)
22:29:17   252 czf8yap5hgng7 ACTIVE            7 WAITED KNOWN TIME   log file switch (private strand flush incomplete)
22:29:18   252 czf8yap5hgng7 ACTIVE            7 WAITED KNOWN TIME   log file switch (private strand flush incomplete)
22:29:19   252 czf8yap5hgng7 ACTIVE            7 WAITED KNOWN TIME   log file switch (private strand flush incomplete)
```

`EVENT`列には`enq: CR - block range reuse ckpt`や`log file switch (private strand flush incomplete)`というあまり見慣れない待機イベントが多数ありますが、実際にはそれらのイベントではほとんど待機していません。（後者は74ms待機、前者は`V$SESSION_EVENT`に計上すらされていません）

```
22:29:19 SQL> SELECT
22:29:19   2      event,
22:29:19   3      time_waited_micro / 1000 "wait time (ms)"
22:29:19   4  FROM
22:29:19   5      v$session_event
22:29:19   6  WHERE
22:29:19   7      sid = sys_context('USERENV', 'SID');

EVENT                                              wait time (ms)
-------------------------------------------------- --------------
Disk file operations I/O                                        1
log file switch (private strand flush incomplete)              74
log file sync                                                   1
SQL*Net message to client                                       0
SQL*Net message from client                                    11
events in waitclass Other                                       2
```

## 3-3. その他

何回か実行すると、以下のような待機イベントが`V$SESSION`の`EVENT`列に多数確認できますが、どれも長時間待機するものはありませんでした。

- `Disk file operations I/O`
- `PGA memory operation`

# 4. まとめ

ここまでの結果からの想像ですが、`V$SESSION`の`EVENT`列は以下を指すのではないでしょうか。

- 該当セッションが待機中の場合は、現在待機中のイベント
- 該当セッションが待機中でない場合は、前回の待機イベント

つまり`V$SESSION`をモニタリングする上では、以下のようにするのが好ましいということでしょうか。

```
SELECT
    to_char(sysdate, 'HH24:MI:SS') tm, 
    sid,
    serial#,
    program,
    status,
    sql_id,
    CASE WHEN wait_time = 0 THEN event ELSE 'CPU' END event
FROM v$session 
WHERE ...;
```

言い訳ですが、大体CPUネックになっている場合って、Oracleの`V$SESSION`を見る前にsarやtopで当たりを付けることができるので、あまり意識して見ないんですよね…