PostgreSQLのround()の話

これは、[PostgreSQL Advent Calendar 2019](https://qiita.com/advent-calendar/2019/postgresql)の13日目の記事です。

今回は、PostgreSQLが提供するround関数について取り上げます。round()は、[PostgreSQL日本語ドキュメント](https://www.postgresql.jp/document/current/html/functions-math.html#FUNCTIONS-MATH-FUNC-TABLE)では「最も近い整数への丸め」と説明されていて、数値の四捨五入に使われている方も多いのではないでしょうか。実際に、PostgreSQLバージョン12で、1.0から3.0までのNUMERIC型の値を0.1刻みでround()に渡すと、下記のとおり四捨五入していることを確認できます。

```sql
=# SELECT num, round(num::NUMERIC) FROM generate_series(1.0, 3.0, 0.1) num;
 num | round 
-----+-------
 1.0 |     1
 1.1 |     1
 1.2 |     1
 1.3 |     1
 1.4 |     1
 1.5 |     2
 1.6 |     2
 1.7 |     2
 1.8 |     2
 1.9 |     2
 2.0 |     2
 2.1 |     2
 2.2 |     2
 2.3 |     2
 2.4 |     2
 2.5 |     3
 2.6 |     3
 2.7 |     3
 2.8 |     3
 2.9 |     3
 3.0 |     3
(21 rows)
```

次に、1.0から3.0までのDOUBLE PRECISION型の値を0.1刻みでround()に渡すと、以下のとおりです。

```sql
=# SELECT num, round(num::DOUBLE PRECISION) FROM generate_series(1.0, 3.0, 0.1) num;
 num | round 
-----+-------
 1.0 |     1
 1.1 |     1
 1.2 |     1
 1.3 |     1
 1.4 |     1
 1.5 |     2
 1.6 |     2
 1.7 |     2
 1.8 |     2
 1.9 |     2
 2.0 |     2
 2.1 |     2
 2.2 |     2
 2.3 |     2
 2.4 |     2
 2.5 |     2
 2.6 |     3
 2.7 |     3
 2.8 |     3
 2.9 |     3
 3.0 |     3
(21 rows)
```

さて、これらのround()の実行結果を見て、何か気になる点はなかったでしょうか？


実は、round(2.5)の結果が、引数の2.5がNUMERIC型のときは3、DOUBLE PRECISION型のときは2と異なっています。2.5を3に丸めるのは四捨五入ですが、2に丸めるのは[銀行丸め (偶数への丸め、五捨五入)](https://ja.wikipedia.org/wiki/%E7%AB%AF%E6%95%B0%E5%87%A6%E7%90%86#%E5%81%B6%E6%95%B0%E3%81%B8%E3%81%AE%E4%B8%B8%E3%82%81%EF%BC%88round_to_even%EF%BC%89)と呼ばれるものです。つまり、PostgreSQLでは、**引数のデータ型によりround()の挙動が異なり、round(NUMERIC)は四捨五入、round(DOUBLE PRECISION)は銀行丸めを行います**。

1.5、2.5、3.5、…を、それぞれNUMERIC型とDOUBLE PRECISION型でround()に渡すと、結果の違いがよく分ります。

```sql
=# SELECT num, round(num::NUMERIC) "NUMERIC", round(num::DOUBLE PRECISION) "DOUBLE" FROM generate_series(1.5, 10.5, 1.0) num;
 num  | NUMERIC | DOUBLE 
------+---------+--------
  1.5 |       2 |      2
  2.5 |       3 |      2
  3.5 |       4 |      4
  4.5 |       5 |      4
  5.5 |       6 |      6
  6.5 |       7 |      6
  7.5 |       8 |      8
  8.5 |       9 |      8
  9.5 |      10 |     10
 10.5 |      11 |     10
(10 rows)
```

以上のとおり、PostgreSQLが提供するround関数は、引数のデータ型によって挙動が変わるため、取り扱いには注意が必要です。既存システムでround関数を使っている場合は、期待どおりの実行結果が得られているか確認したほうがよいかもしれません。

# おまけ: round()の実装

round(NUMERIC)は、PostgreSQLの内部関数である[numeric_round](https://doxygen.postgresql.org/backend_2utils_2adt_2numeric_8c.html#a2ed930e04879f0dd4424a5bbbbbd5c25)を呼び出して、PostgreSQLで実装されている四捨五入のロジックを実行します。一方、round(DOUBLE PRECISION)は、内部関数の[dround](https://doxygen.postgresql.org/float_8c.html#a3c7923cced10d99582e20866897cebbc)を呼び出して、その関数内では[rint()](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/rint.3.html)を実行するだけです。

rint()は、値を整数に丸めるライブラリコールです。rint()がどのように丸めるか(丸めモード)は、ライブラリコールの[fesetround()](https://linuxjm.osdn.jp/html/LDP_man-pages/man3/fenv.3.html)で設定変更可能です。PostgreSQLは、fesetround()による丸めモードの変更は行なっていないため、rint()はデフォルトの丸めモード[^1]である銀行丸めに従って値を丸めます。


# おまけ: パラメータ設定時の丸め

整数を設定するPostgreSQLパラメータ (例えば、[log_min_duration_statement](https://www.postgresql.jp/document/current/html/runtime-config-logging.html#RUNTIME-CONFIG-LOGGING-WHEN)) に小数点付きの値を指定すると、値はrint()により丸められます。つまり、値の銀行丸め結果の整数が、パラメータの設定値になります。

```sql
=# SET log_min_duration_statement TO 10.5;

=# SHOW log_min_duration_statement;
 log_min_duration_statement 
----------------------------
 10ms
(1 row)
```

[^1]: OSや環境によっては、デフォルトの丸めモードが銀行丸めでない可能性があります。