MySQL Advent Calendar 2020 - Qiita の11日目の記事であり、同時に、PostgreSQL Advent Calendar 2020 - Qiita の11日目の記事です。
両データベースについての挙動差のお話です。
MySQLとRAND関数の挙動の整理 その1 - なからなLife
MySQLとRAND関数の挙動の整理 その2 - なからなLife
と、今月に入ってランダム関数について書くのは3回目ですね。
前の2回はMySQLに限った話だったので、そのPostgreSQL版を書いても良かったのですが、焼き増し感が強いので、ちょっと違う方向で。
今度は参照・検索ではなく、生成時の話です。
ランダム値生成関数の解説
MySQLのマニュアル(日本語はMySQL 5.6しか無いのでコチラで)
・RAND(), RAND(N)
0 <= v < 1.0 の範囲内で、ランダムな浮動小数点値 v を返します。定数整数引数 N が指定されている場合は、カラム値の反復可能なシーケンスを生成するシード値として使用されます。次の例では、RAND(3) で生成される値のシーケンスが、発生した両方の場所で同じです。
https://dev.mysql.com/doc/refman/5.6/ja/mathematical-functions.html#function_rand
PostgreSQLのマニュアル
関数 戻り値型 説明 random() dp 0.0 <= x < 1.0の範囲の乱数値 https://www.postgresql.jp/document/12/html/functions-math.html
どちらも、「0.0 <= x < 1.0の範囲の乱数値」を生成します。
データ型が浮動小数点値であることも一緒です。
ランダム関数を使ってテストデータを作ってみる
環境は、MySQL8.0.22、および、PostgreSQL12.5です。
MySQLの「FLOOR(10 + RAND() * 10)」、PostgreSQLの「FLOOR(10 + RANDOM() * 10)」は、「10~19」を生成します。*1
MySQL
mysql> SELECT * FROM test_seq LIMIT 5;
+--------+
| col_id |
+--------+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
+--------+
5 rows in set (0.00 sec)
mysql> SELECT
-> (SELECT FLOOR(10 + RAND() * 10)) AS A,
-> (SELECT FLOOR(10 + RAND() * 10)) AS B,
-> (SELECT FLOOR(10 + RAND() * 10)) AS C,
-> (SELECT FLOOR(10 + RAND() * 10)) AS D,
-> (SELECT FLOOR(10 + RAND() * 10)) AS E,
-> (SELECT FLOOR(10 + RAND() * 10)) AS F,
-> (SELECT FLOOR(10 + RAND() * 10)) AS G,
-> (SELECT FLOOR(10 + RAND() * 10)) AS H,
-> (SELECT FLOOR(10 + RAND() * 10)) AS I,
-> (SELECT FLOOR(10 + RAND() * 10)) AS J
-> FROM test_seq
-> LIMIT 5;
+----+----+----+----+----+----+----+----+----+----+
| A | B | C | D | E | F | G | H | I | J |
+----+----+----+----+----+----+----+----+----+----+
| 17 | 14 | 18 | 18 | 17 | 10 | 19 | 15 | 19 | 10 |
| 13 | 15 | 17 | 10 | 19 | 16 | 15 | 14 | 19 | 13 |
| 19 | 14 | 16 | 19 | 16 | 12 | 14 | 15 | 15 | 19 |
| 10 | 14 | 19 | 16 | 15 | 14 | 15 | 15 | 10 | 17 |
| 16 | 17 | 10 | 17 | 16 | 18 | 14 | 16 | 19 | 16 |
+----+----+----+----+----+----+----+----+----+----+
5 rows in set (0.00 sec)
PostgreSQL
postgres=# SELECT * FROM test_seq LIMIT 5;
col_id
--------
1
2
3
4
5
(5 rows)
postgres=# SELECT
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS A,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS B,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS C,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS D,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS E,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS F,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS G,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS H,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS I,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS J
postgres-# FROM test_seq
postgres-# LIMIT 5;
a | b | c | d | e | f | g | h | i | j
----+----+----+----+----+----+----+----+----+----
17 | 18 | 13 | 11 | 15 | 15 | 16 | 11 | 15 | 14
17 | 18 | 13 | 11 | 15 | 15 | 16 | 11 | 15 | 14
17 | 18 | 13 | 11 | 15 | 15 | 16 | 11 | 15 | 14
17 | 18 | 13 | 11 | 15 | 15 | 16 | 11 | 15 | 14
17 | 18 | 13 | 11 | 15 | 15 | 16 | 11 | 15 | 14
(5 rows)
PostgreSQL側、列方向にはランダムに値が生成されていますけど、行方向にはランダムになってませんね。。。
PostgreSQLでMySQLと同じ状態を作れるか
UPDATEなら、MySQLと同じような乱数表になります。
※後述する追記も読んでください
postgres=# CREATE TABLE test_tbl (
postgres(# test_tbl_id INTEGER NOT NULL,
postgres(# a varchar(100),
postgres(# b varchar(100),
postgres(# c varchar(100),
postgres(# d varchar(100),
postgres(# e varchar(100),
postgres(# f varchar(100),
postgres(# g varchar(100),
postgres(# h varchar(100),
postgres(# i varchar(100),
postgres(# j varchar(100),
postgres(# CONSTRAINT test_tbl_pkey PRIMARY KEY (test_tbl_id)
postgres(# );
CREATE TABLE
postgres=# INSERT INTO test_tbl (test_tbl_id)
postgres-# SELECT col_id FROM test_seq
postgres-# LIMIT 5;
INSERT 0 5
postgres=# SELECT * FROM test_tbl;
test_tbl_id | a | b | c | d | e | f | g | h | i | j
-------------+---+---+---+---+---+---+---+---+---+---
1 | | | | | | | | | |
2 | | | | | | | | | |
3 | | | | | | | | | |
4 | | | | | | | | | |
5 | | | | | | | | | |
(5 rows)
postgres=# UPDATE test_tbl SET
postgres-# a = FLOOR(10 + RANDOM() * 10),
postgres-# b = FLOOR(10 + RANDOM() * 10),
postgres-# c = FLOOR(10 + RANDOM() * 10),
postgres-# d = FLOOR(10 + RANDOM() * 10),
postgres-# e = FLOOR(10 + RANDOM() * 10),
postgres-# f = FLOOR(10 + RANDOM() * 10),
postgres-# g = FLOOR(10 + RANDOM() * 10),
postgres-# h = FLOOR(10 + RANDOM() * 10),
postgres-# i = FLOOR(10 + RANDOM() * 10),
postgres-# j = FLOOR(10 + RANDOM() * 10)
postgres-# ;
UPDATE 5
postgres=# SELECT * FROM test_tbl;
test_tbl_id | a | b | c | d | e | f | g | h | i | j
-------------+----+----+----+----+----+----+----+----+----+----
1 | 15 | 15 | 17 | 10 | 16 | 12 | 10 | 13 | 11 | 10
2 | 17 | 16 | 12 | 15 | 17 | 11 | 18 | 12 | 16 | 13
3 | 16 | 18 | 12 | 15 | 13 | 14 | 13 | 12 | 19 | 14
4 | 17 | 11 | 12 | 10 | 13 | 17 | 19 | 14 | 14 | 18
5 | 16 | 16 | 19 | 17 | 17 | 14 | 10 | 19 | 18 | 11
(5 rows)このとおり、きれいな乱数(?)になりました。
PostgreSQLでは、SELECTを使用しないと、きれいなランダムになるらしい。(追記:2020/12/11)
やを執筆された鈴木啓修さん(id:interdb)から
各columnに(SELECT FLOOR(10+RANDOM()*10))を使うと、クエリ実行時に一度だけ各SELECT文のRANDOM()関数が実行されるので、各columnに同じ値が表示されます。
(略:SELECT版の実行計画)
SELECTを使わなければ、以下のようにColumn, row毎にRANDOM()が呼ばれます。
(略:SELECTなし版の実行結果)
というコメント頂いたのですが、コメント欄だと実行計画や実行結果の表示が崩れてしまう、ということで、こちらで再現したものを貼り付けます。
コメントありがとうございます!!!
SELECT版の実行計画
postgres=# EXPLAIN ANALYZE SELECT
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS A,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS B,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS C,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS D,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS E,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS F,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS G,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS H,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS I,
postgres-# (SELECT FLOOR(10 + RANDOM() * 10)) AS J
postgres-# FROM test_seq
postgres-# LIMIT 5;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------
Limit (cost=0.20..0.27 rows=5 width=80) (actual time=0.030..0.032 rows=5 loops=1)
InitPlan 1 (returns $0)
-> Result (cost=0.00..0.02 rows=1 width=8) (actual time=0.002..0.002 rows=1 loops=1)
InitPlan 2 (returns $1)
-> Result (cost=0.00..0.02 rows=1 width=8) (actual time=0.000..0.000 rows=1 loops=1)
InitPlan 3 (returns $2)
-> Result (cost=0.00..0.02 rows=1 width=8) (actual time=0.000..0.000 rows=1 loops=1)
InitPlan 4 (returns $3)
-> Result (cost=0.00..0.02 rows=1 width=8) (actual time=0.000..0.000 rows=1 loops=1)
InitPlan 5 (returns $4)
-> Result (cost=0.00..0.02 rows=1 width=8) (actual time=0.000..0.000 rows=1 loops=1)
InitPlan 6 (returns $5)
-> Result (cost=0.00..0.02 rows=1 width=8) (actual time=0.000..0.000 rows=1 loops=1)
InitPlan 7 (returns $6)
-> Result (cost=0.00..0.02 rows=1 width=8) (actual time=0.000..0.000 rows=1 loops=1)
InitPlan 8 (returns $7)
-> Result (cost=0.00..0.02 rows=1 width=8) (actual time=0.000..0.000 rows=1 loops=1)
InitPlan 9 (returns $8)
-> Result (cost=0.00..0.02 rows=1 width=8) (actual time=0.000..0.000 rows=1 loops=1)
InitPlan 10 (returns $9)
-> Result (cost=0.00..0.02 rows=1 width=8) (actual time=0.000..0.000 rows=1 loops=1)
-> Seq Scan on test_seq (cost=0.00..242008.16 rows=16777216 width=80) (actual time=0.029..0.030 rows=5 loops=1)
Planning Time: 0.155 ms
Execution Time: 0.072 ms
(24 rows)
SELECTなし版の実行結果
postgres=# SELECT postgres-# FLOOR(10 + RANDOM() * 10) AS A, postgres-# FLOOR(10 + RANDOM() * 10) AS B, postgres-# FLOOR(10 + RANDOM() * 10) AS C, postgres-# FLOOR(10 + RANDOM() * 10) AS D, postgres-# FLOOR(10 + RANDOM() * 10) AS E, postgres-# FLOOR(10 + RANDOM() * 10) AS F, postgres-# FLOOR(10 + RANDOM() * 10) AS G, postgres-# FLOOR(10 + RANDOM() * 10) AS H, postgres-# FLOOR(10 + RANDOM() * 10) AS I, postgres-# FLOOR(10 + RANDOM() * 10) AS J postgres-# FROM test_seq postgres-# LIMIT 5; a | b | c | d | e | f | g | h | i | j ----+----+----+----+----+----+----+----+----+---- 10 | 14 | 11 | 19 | 16 | 15 | 12 | 16 | 19 | 12 19 | 10 | 15 | 17 | 19 | 12 | 13 | 16 | 19 | 17 14 | 10 | 10 | 14 | 17 | 18 | 11 | 11 | 11 | 14 15 | 11 | 16 | 16 | 17 | 12 | 10 | 14 | 11 | 17 19 | 11 | 14 | 12 | 18 | 13 | 12 | 16 | 18 | 16 (5 rows)
ついでに、SELECTなし版の実行計画
postgres=# EXPLAIN ANALYZE SELECT
postgres-# FLOOR(10 + RANDOM() * 10) AS A,
postgres-# FLOOR(10 + RANDOM() * 10) AS B,
postgres-# FLOOR(10 + RANDOM() * 10) AS C,
postgres-# FLOOR(10 + RANDOM() * 10) AS D,
postgres-# FLOOR(10 + RANDOM() * 10) AS E,
postgres-# FLOOR(10 + RANDOM() * 10) AS F,
postgres-# FLOOR(10 + RANDOM() * 10) AS G,
postgres-# FLOOR(10 + RANDOM() * 10) AS H,
postgres-# FLOOR(10 + RANDOM() * 10) AS I,
postgres-# FLOOR(10 + RANDOM() * 10) AS J
postgres-# FROM test_seq
postgres-# LIMIT 5;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
Limit (cost=0.00..0.57 rows=5 width=80) (actual time=0.015..0.018 rows=5 loops=1)
-> Seq Scan on test_seq (cost=0.00..1919729.76 rows=16777216 width=80) (actual time=0.014..0.016 rows=5 loops=1)
Planning Time: 0.085 ms
Execution Time: 0.037 ms
(4 rows)
MySQLではどうか?
SELECTを使っても使わなくても、結果も実行計画も同じでした。
実行計画(MySQL5.7)の比較だけ貼っておきます。
mysql> EXPLAIN SELECT
-> (SELECT FLOOR(10 + RAND() * 10)) AS A,
-> (SELECT FLOOR(10 + RAND() * 10)) AS B,
-> (SELECT FLOOR(10 + RAND() * 10)) AS C,
-> (SELECT FLOOR(10 + RAND() * 10)) AS D,
-> (SELECT FLOOR(10 + RAND() * 10)) AS E,
-> (SELECT FLOOR(10 + RAND() * 10)) AS F,
-> (SELECT FLOOR(10 + RAND() * 10)) AS G,
-> (SELECT FLOOR(10 + RAND() * 10)) AS H,
-> (SELECT FLOOR(10 + RAND() * 10)) AS I,
-> (SELECT FLOOR(10 + RAND() * 10)) AS J
-> FROM test_seq
-> LIMIT 5;
+----+-------------+----------+------------+-------+---------------+---------+---------+------+----------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+---------+---------+------+----------+----------+-------------+
| 1 | SIMPLE | test_seq | NULL | index | NULL | PRIMARY | 4 | NULL | 15547276 | 100.00 | Using index |
+----+-------------+----------+------------+-------+---------------+---------+---------+------+----------+----------+-------------+
1 row in set, 11 warnings (0.00 sec)
mysql> EXPLAIN SELECT
-> FLOOR(10 + RAND() * 10) AS A,
-> FLOOR(10 + RAND() * 10) AS B,
-> FLOOR(10 + RAND() * 10) AS C,
-> FLOOR(10 + RAND() * 10) AS D,
-> FLOOR(10 + RAND() * 10) AS E,
-> FLOOR(10 + RAND() * 10) AS F,
-> FLOOR(10 + RAND() * 10) AS G,
-> FLOOR(10 + RAND() * 10) AS H,
-> FLOOR(10 + RAND() * 10) AS I,
-> FLOOR(10 + RAND() * 10) AS J
-> FROM test_seq
-> LIMIT 5;
+----+-------------+----------+------------+-------+---------------+---------+---------+------+----------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+----------+------------+-------+---------------+---------+---------+------+----------+----------+-------------+
| 1 | SIMPLE | test_seq | NULL | index | NULL | PRIMARY | 4 | NULL | 15547276 | 100.00 | Using index |
+----+-------------+----------+------------+-------+---------------+---------+---------+------+----------+----------+-------------+
1 row in set, 1 warning (0.00 sec)
まとめ
- SELECTによる1行分の評価においては、MySQL、PostgreSQLともに、関数の出現(呼び出し)回数分、新しい乱数を生成させる。
- SELECTによる複数行分の評価においては、MySQLは毎回新しい乱数を生成させるが、PostgreSQLは新しい乱数を生成しない。
ていうか、これ「ランダム関数の」挙動差ではなく、「関数全般」の挙動差として認識しておいたほうが良い?
この件が掲載されていたかどうかは忘れたけど、RDBMS毎に同じような関数がある/ないを調べるには、この本が割と便利。
あとは、それぞれのDBについてしっかり勉強しようね、っていうお話なので、いつもの推しごと。
*1:この検証のランダム値自体はなんでもよかったのですが、なんとなく、他の用事で使ったものをそのまま流用しているだけです。