Oracle BINARY_INTEGER型の引数を持つPL/SQLプログラムの挙動

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

以下の動作を[Oracle 11g XE on Linux](https://qiita.com/szly/items/4b610f51a3172c04f5d2)で試しています。Oracle 11g XEも会社と同じ動きでした。

# 事の発端

事の発端は、私が即席で作って渡したPL/SQLプログラムが思ったような動きをしないという質問でした。問題個所を紐解くと、"特定の文字列の中にある一文字をランダムに返す"という処理でした。その処理がたまに空文字列を返すというのです。

以下のような処理になっていました。

```
X := SUBSTR ( 'ABCDE', DBMS_RANDOM.VALUE * 5 + 1, 1 );
```

問題は、SUBSTRの第二引数です。SUBSTRの第二引数には、切り出す文字列の開始位置を1オリジンで指定します。その特性上、整数を指定することになります。しかし、小数値を渡していたのが問題でした。

式の値"DBMS_RANDOM.VALUE * 5 + 1"は1以上6未満のランダム数値です。PL/SQLは整数への暗黙のキャスト時に四捨五入するから、5.5以上の値は6になります。文字列の終端を超えた開始位置を指定しているから、空文字列になるんだといった説明をしました。

試すならこんな感じですね。実行すると、ときどき空文字列が返されることが分かります。

```
BEGIN
	FOR V_IDX IN 1 .. 100 LOOP
		DBMS_OUTPUT.PUT_LINE (
			'[' || SUBSTR ( 'ABCDE', DBMS_RANDOM.VALUE * 5 + 1, 1 ) || ']'
		);
	END LOOP;
END;
```

SQLで試すほうが簡単かな。こんな感じのSQLですね。ほら、ときどき空文字列が...ん？返されない？

```
SELECT	SUBSTR ( 'ABCDE', DBMS_RANDOM.VALUE * 5 + 1, 1 )
FROM	ALL_OBJECTS
WHERE	ROWNUM <= 100
```

PL/SQLからSUBSTRを呼び出すときと、SQLからSUBSTRを呼び出すときで、動きが違う？

"プログラムのどこが誤っているか"というだけの質問が、大ごとになってきました。

# 問題個所の再確認

会社では色々と試行錯誤しました。その経緯は省略します。最終的に、問題事象を直接的に示すものは以下です。

```
BEGIN
	BEGIN
		DBMS_OUTPUT.PUT_LINE ( 'PL/SQLで計算 = [' || SUBSTR ( 'ABCDE', 5.8, 1 ) || ']' );
	END;
	DECLARE
		V_CHR	VARCHAR2(32767);
	BEGIN
		SELECT	SUBSTR ( 'ABCDE', 5.8, 1 )
		INTO	V_CHR
		FROM	DUAL
		;
		DBMS_OUTPUT.PUT_LINE ( 'SQLで計算 = [' || V_CHR || ']' );
	END;
END;
```

これを実行すると、以下の結果となります。

```
PL/SQLで計算 = []
SQLで計算 = [E]
```

SUBSTRの第二引数に、PL/SQLでは四捨五入されて"6"が渡され、SQLのSELECT句では切り捨てられて"5"が渡されているようだ、という結果になりました。

SQLやPL/SQLでは"普通は四捨五入"と説明していたら、C/Java畑の人から"普通は切り捨てじゃないのか？"と質問されたり。以下のようなSQLで四捨五入されることを確認できます。

```
SELECT CAST ( 1.8 AS INTEGER ) FROM DUAL
```

会社ではこのあたりまで確認したところでストップ。整数を指定する場所に小数を指定しない、ちゃんと整数を指定することが大切、という結論になりました。

ここからは、会社で試さなかったことをもう少し掘り下げてみます。

# SUBSTRのラッパーを作ってみる

SUBSTRを呼び出すラッパーのユーザーFUNCTIONを作ってみるとどうなるでしょうか？

```
CREATE OR REPLACE FUNCTION FNC_SUBSTR_TEST01 (
		STR1	IN	VARCHAR2
	,	POS		IN	NUMBER
	,	LEN		IN	NUMBER
)
RETURN VARCHAR2 IS
BEGIN
	RETURN SUBSTR ( STR1, POS, LEN );
END;
```

実行すると、このようになります。

```
WITH
DATA AS (
				SELECT 0 AS VALUE FROM DUAL WHERE 0 = 1
	UNION ALL	SELECT 1.0 AS VALUE FROM DUAL
	UNION ALL	SELECT 1.3 AS VALUE FROM DUAL
	UNION ALL	SELECT 1.8 AS VALUE FROM DUAL
	UNION ALL	SELECT 2.0 AS VALUE FROM DUAL
	UNION ALL	SELECT 4.8 AS VALUE FROM DUAL
	UNION ALL	SELECT 5.0 AS VALUE FROM DUAL
	UNION ALL	SELECT 5.3 AS VALUE FROM DUAL
	UNION ALL	SELECT 5.8 AS VALUE FROM DUAL
	UNION ALL	SELECT 6.0 AS VALUE FROM DUAL
)

SELECT	VALUE
	,	SUBSTR ( 'ABCDE', VALUE, 1 )	AS	"SUBSTR"
	,	FNC_SUBSTR_TEST01 ( 'ABCDE', VALUE, 1 )	AS	"FNC_SUBSTR_TEST01"
FROM	DATA
```

|VALUE|SUBSTR|FNC_SUBSTR_TEST01|
|----:|:-----|:----------------|
|1    |A     |A                |
|1.3  |A     |A                |
|1.8  |A     |B                |
|2    |B     |B                |
|4.8  |D     |E                |
|5    |E     |E                |
|5.3  |E     |E                |
|5.8  |E     |                 |
|6    |      |                 |

両者で動きが異なります。動きが異なるということは"プログラム"か"外部環境"か"外部入力"のどれかが異なるということです。今回の場合、外部環境や外部入力は同じように見えます。すると、プログラムが異なるということになります。

プログラムを比較してみることにします。とはいっても、標準プログラムにソースはありません。比較できるのはI/O部分だけです。

```
SELECT	*
FROM	ALL_PROCEDURES AP
WHERE	AP.OWNER || '.' || AP.OBJECT_NAME || '.' || AP.PROCEDURE_NAME IN ( 'SYSTEM.FNC_SUBSTR_TEST01.', 'SYS.STANDARD.SUBSTR' )
ORDER BY
		AP.OWNER
	,	AP.OBJECT_NAME
	,	AP.PROCEDURE_NAME
	,	AP.OVERLOAD
```

|OWNER |OBJECT_NAME      |PROCEDURE_NAME|...|OVERLOAD|OBJECT_TYPE|...|
|:-----|:----------------|:-------------|:--|-------:|:----------|:--|
|SYS   |STANDARD         |SUBSTR        |   |1       |PACKAGE    |   |
|SYS   |STANDARD         |SUBSTR        |   |2       |PACKAGE    |   |
|SYSTEM|FNC_SUBSTR_TEST01|              |   |        |FUNCTION   |   |

```
SELECT	AA.*
FROM	ALL_ARGUMENTS AA
WHERE	AA.OWNER || '.' || AA.PACKAGE_NAME || '.' || AA.OBJECT_NAME IN ( 'SYSTEM..FNC_SUBSTR_TEST01', 'SYS.STANDARD.SUBSTR' )
ORDER BY
		AA.OWNER
	,	AA.PACKAGE_NAME
	,	AA.OBJECT_NAME
	,	AA.OVERLOAD
	,	AA.POSITION
```

|OWNER |OBJECT_NAME      |PACKAGE_NAME  |...|OVERLOAD|ARGUMENT_NAME|POSITION|DATA_TYPE      |...|
|:-----|:----------------|:-------------|:--|-------:|:------------|-------:|:--------------|:--|
|SYS   |SUBSTR           |STANDARD      |   |1       |             |       0|VARCHAR2       |   |
|SYS   |SUBSTR           |STANDARD      |   |1       |STR1         |       1|VARCHAR2       |   |
|SYS   |SUBSTR           |STANDARD      |   |1       |POS          |       2|BINARY_INTEGER |   |
|SYS   |SUBSTR           |STANDARD      |   |1       |LEN          |       3|BINARY_INTEGER |   |
|SYS   |SUBSTR           |STANDARD      |   |2       |             |       0|CLOB           |   |
|SYS   |SUBSTR           |STANDARD      |   |2       |STR1         |       1|CLOB           |   |
|SYS   |SUBSTR           |STANDARD      |   |2       |POS          |       2|BINARY_INTEGER |   |
|SYS   |SUBSTR           |STANDARD      |   |2       |LEN          |       3|BINARY_INTEGER |   |
|SYSTEM|FNC_SUBSTR_TEST01|              |   |        |             |       0|VARCHAR2       |   |
|SYSTEM|FNC_SUBSTR_TEST01|              |   |        |STR1         |       1|VARCHAR2       |   |
|SYSTEM|FNC_SUBSTR_TEST01|              |   |        |POS          |       2|NUMBER         |   |
|SYSTEM|FNC_SUBSTR_TEST01|              |   |        |LEN          |       3|NUMBER         |   |

引数が異なります。SUBSTRの第2引数はBINARY_INTEGERでした。BINARY_INTEGERは[ハードウェア算術計算で高速に計算してくれる整数型](https://docs.oracle.com/cd/E16338_01/appdev.112/b56260/datatypes.htm#i10726)です。

引数をBINARY_INTEGER型に変えたFUNCTIONも作ってみます。

```
CREATE OR REPLACE FUNCTION FNC_SUBSTR_TEST02 (
		STR1	IN	VARCHAR2
	,	POS		IN	BINARY_INTEGER
	,	LEN		IN	BINARY_INTEGER
)
RETURN VARCHAR2 IS
BEGIN
	RETURN SUBSTR ( STR1, POS, LEN );
END;
```

```
WITH
DATA AS (
				SELECT 0 AS VALUE FROM DUAL WHERE 0 = 1
	UNION ALL	SELECT 1.0 AS VALUE FROM DUAL
	UNION ALL	SELECT 1.3 AS VALUE FROM DUAL
	UNION ALL	SELECT 1.8 AS VALUE FROM DUAL
	UNION ALL	SELECT 2.0 AS VALUE FROM DUAL
	UNION ALL	SELECT 4.8 AS VALUE FROM DUAL
	UNION ALL	SELECT 5.0 AS VALUE FROM DUAL
	UNION ALL	SELECT 5.3 AS VALUE FROM DUAL
	UNION ALL	SELECT 5.8 AS VALUE FROM DUAL
	UNION ALL	SELECT 6.0 AS VALUE FROM DUAL
)

SELECT	VALUE
	,	SUBSTR ( 'ABCDE', VALUE, 1 )			AS	"SUBSTR"
	,	FNC_SUBSTR_TEST01 ( 'ABCDE', VALUE, 1 )	AS	"FNC_SUBSTR_TEST01"
	,	FNC_SUBSTR_TEST02 ( 'ABCDE', VALUE, 1 )	AS	"FNC_SUBSTR_TEST02"
FROM	DATA
```

|VALUE|SUBSTR|FNC_SUBSTR_TEST01|FNC_SUBSTR_TEST02|
|----:|:-----|:----------------|:----------------|
|1    |A     |A                |A                |
|1.3  |A     |A                |A                |
|1.8  |A     |B                |A                |
|2    |B     |B                |B                |
|4.8  |D     |E                |D                |
|5    |E     |E                |E                |
|5.3  |E     |E                |E                |
|5.8  |E     |                 |E                |
|6    |      |                 |                 |

FNC_SUBSTR_TEST02はSUBSTRと同じ動きになりました。そうすると、NUMBER(小数)からBINARY_INTEGERへの型変換時に切り捨てられているようだということになります。

# BINARY_INTEGERへの型変換の挙動を確認する

"BINARY_INTEGER型を引数に持つ"という特徴に特化したFUNCTIONを作成してみます。BINARY_INTEGER型の値を受け取り、その値をそのまま返すFUNCTIONです。

```
CREATE OR REPLACE FUNCTION FNC_ARG_TEST01 (
		VALUE	IN	BINARY_INTEGER
)
RETURN BINARY_INTEGER IS
BEGIN
	RETURN VALUE;
END;
```

これを使って、SQLとPL/SQLの動作を比較してみます。

```
DECLARE
	V_VALUE	BINARY_INTEGER;
BEGIN
	BEGIN
		FOR V_REC IN (
			SELECT	FNC_ARG_TEST01(1.8) AS VALUE
			FROM	DUAL
		) LOOP
			DBMS_OUTPUT.PUT_LINE ( 'SQL = [' || V_REC.VALUE || ']' );
		END LOOP;
	END;
	BEGIN
		DBMS_OUTPUT.PUT_LINE ( 'PL/SQL = [' || FNC_ARG_TEST01(1.8) || ']' );
	END;
END;
```

結果はこうなります。

```
SQL = [1]
PL/SQL = [2]
```

ここまでで分かったことは、以下の内容です。

* BINARY_INTEGER型の引数に小数を渡すと、SQL内で呼び出す場合は切り捨てられた値が渡される。
* BINARY_INTEGER型の引数に小数を渡すと、PL/SQL内で呼び出すときは四捨五入された値が渡される。

なお、単純代入は四捨五入です。以下の結果は2になります。

```
DECLARE
	V_VALUE	BINARY_INTEGER;
BEGIN
	V_VALUE := 18 / 10; -- 1.8;
	DBMS_OUTPUT.PUT_LINE ( '単純代入 = [' || V_VALUE || ']' );
END;
```

# まとめ

以下のようなことが分かりました。

* BINARY_INTEGER型の引数に小数を渡すと、SQL内で呼び出す場合は切り捨てられた値が渡される。
* BINARY_INTEGER型の引数に小数を渡すと、PL/SQL内で呼び出すときは四捨五入された値が渡される。
* PL/SQL内で、BINARY_INTEGER型の変数に小数を入れると、四捨五入される。

SQLでBINARY_INTEGER型の引数に小数を渡すときだけ、切捨てとなるようです。この観点で[公式](http://otndnld.oracle.co.jp/document/products/oracle11g/111/doc_dvd/appdev.111/E05670-03/subprograms.htm#i23202)やネットを調べてみました。しかし、この挙動の理由に辿り着くことはできませんでした。

標準で提供されている文字列操作系のFUNCTIONを中心に、処理の高速化のためにBINARY_INTEGER型の引数を使用している個所は随所にあると思います。普段から引数のデータ型を意識することが大切です。このような教訓を再認識しつつも、もやもや感の残る結果になってしまいました。