title | tags | author | slide |
---|---|---|---|
PostgreSQLのバルクインサート |
PostgreSQL Docker Linux |
dameyodamedame |
false |
今までOracleが多かったのもあり、バルクインサートの構文はほとんど使っていませんでした。
この手の処理は、典型的には初期構築や大量投入処理時に行われるので、システムを止めてインデックスや制約、必要ならロギングも外してローダーなどを使って大量投入した後、外したものをまた付けて統計情報更新などして整合を確認後にシステムを再開する、みたいな感覚なのです(そうしないとメチャクチャ時間かかるので)。
この感覚でいうと、バルクインサートには出番がありません。
しかし昨今は(個人的にはまだ十分とは思ってないけど)ORMなどが発展してきており、RDBMSの差異は徐々に小さくなりつつあり、RDBMS固有のローダー機能に頼るのはその阻害要因になる可能性があり、またRDBMSの構成も複雑になってきていて、個々のDBインスタンスに対して直接作用するローダーは扱いにくいかもしれません。
そこで遅ればせながら、ローダーの代わりとなる(?)バルクインサートの門戸を叩き、単独のインサートと比較してどの程度の効果があるのか調べてみました。
今回はOracleに代わるデータベースの選択肢として、(ある程度)PL/SQLが使え、RETURNING
句を持つPostgreSQLを使用します。MySQLや他のRDBMSでもバルクインサート自体は広く使えると思いますが、速度測定をしてみたかったのがこれだっただけです。
10列で主キーあり。主キーのみvarchar(100)とし、残りの9列はbigintとし、2列ほどインデックスを付けます。
create table test(
id varchar(100) primary key,
value1 bigint not null,
value2 bigint not null,
value3 bigint not null,
value4 bigint not null,
value5 bigint not null,
value6 bigint not null,
value7 bigint not null,
value8 bigint not null,
value9 bigint not null
);
create index idx_test_value1 on test(value1);
create index idx_test_value2 on test(value2);
全行で100万行とし、1行内の全ての列は単一の整数値、もしくはそれを文字列型にしたもの、とします。各行は0から採番して999999までの値になります。
10万行単位でコミットします。
テーブルの中身が空の状態から、インデックスを付けたまま10万行ずつインサートし、100万行になるまで、各トランザクションの時間を計測していきます。1行ずつインサートする場合と、10万行分バルクインサートする場合の2種類の計測を行います。
なお、HDD環境のため、DBデータ含むファイルシステムをRAMディスク内に置いています。
https://qiita.com/dameyodamedame/items/b6e356c783a529cb7187
コードはシェルスクリプトでSQLを出力し、これをdocker上で動作するpostgresqlに食わせる形です。
コード(クリックすると表示)
set -eu
cat >docker-compose.yml <<EOF
services:
db:
build: postgres
restart: always
volumes:
- ./data:/var/lib/postgresql/data
- ./:/home/user
ports:
- "5432:5432"
environment:
POSTGRES_PASSWORD: example
EOF
cat >setup.sql <<EOF
create database test;
EOF
cat >ddl.sql <<EOF
create table test(
id varchar(100) primary key,
value1 bigint not null,
value2 bigint not null,
value3 bigint not null,
value4 bigint not null,
value5 bigint not null,
value6 bigint not null,
value7 bigint not null,
value8 bigint not null,
value9 bigint not null
);
create index idx_test_value1 on test(value1);
create index idx_test_value2 on test(value2);
EOF
docker compose up -d
docker compose exec -T db bash -eux <<EOF
while ! psql 'postgresql://postgres:example@localhost' </dev/null 2>/dev/null; do
sleep 1
done
EOF
docker compose exec -T db psql 'postgresql://postgres:example@localhost' <setup.sql
docker compose exec -T db psql 'postgresql://postgres:example@localhost/test' <ddl.sql
docker compose exec -T db psql 'postgresql://postgres:example@localhost/test' <<EOF
truncate table test;
EOF
for i in `seq 0 9`; do
echo "begin;" >insert.sql
start=`expr $i \* 100000` || true
end=`expr \( $i + 1 \) \* 100000 - 1`
for v in `seq $start $end`;do
echo "insert into test values('$v',$v,$v,$v,$v,$v,$v,$v,$v,$v);"
done >>insert.sql
echo "commit;" >>insert.sql
/usr/bin/time -f %e docker compose exec -T db psql -q -v ON_ERROR_STOP=1 'postgresql://postgres:example@localhost/test' <insert.sql
done
docker compose exec -T db psql 'postgresql://postgres:example@localhost/test' <<EOF
truncate table test;
EOF
for i in `seq 0 9`; do
echo "begin;" >insert.sql
echo "insert into test values" >>insert.sql
start=`expr $i \* 100000` || true
end=`expr \( $i + 1 \) \* 100000 - 2`
for v in `seq $start $end`;do
echo "('$v',$v,$v,$v,$v,$v,$v,$v,$v,$v),"
done >>insert.sql
v=`expr $end + 1`
echo "('$v',$v,$v,$v,$v,$v,$v,$v,$v,$v);" >>insert.sql
echo "commit;" >>insert.sql
/usr/bin/time -f %e docker compose exec -T db psql -q -v ON_ERROR_STOP=1 'postgresql://postgres:example@localhost/test' <insert.sql
done
docker compose exec -T db psql -v ON_ERROR_STOP=1 'postgresql://postgres:example@localhost/test' <<EOF
\\copy test to '/home/user/test.csv' with csv header;
truncate table test;
EOF
/usr/bin/time -f %e docker compose exec -T db psql -q -v ON_ERROR_STOP=1 'postgresql://postgres:example@localhost/test' <<EOF
\\copy test from '/home/user/test.csv' with csv header;
EOF
docker compose down
※今回グラフはデータをコピペしてofficeで描いています
数値
start | simple | bulk |
---|---|---|
0 | 11.49 | 3.22 |
100000 | 11.48 | 3.42 |
200000 | 11.47 | 3.24 |
300000 | 11.67 | 3.26 |
400000 | 11.65 | 3.38 |
500000 | 11.46 | 3.27 |
600000 | 11.6 | 3.19 |
700000 | 11.5 | 3.21 |
800000 | 11.58 | 3.25 |
900000 | 11.45 | 3.23 |
ついでに測った\COPYコマンド(ローダー)の速度は100万件で8.28秒でした(計測方法はコード参照)。
100万行程度では既存の行数に対して時間の変化は見られません(行数が足りないかもしれないということ)。今回の計測ではバルクインサートの方が3倍以上速いようです。ただし、ローダーの方がさらにその3倍速い結果になったということです。
この結果から、今回のデータであれば、ローダーの使用を検討した上で、使用しない判断の場合はバルクインサートで実施すべきということが言えそうです。
(今回のケースでは)
- 通常インサートよりはバルクインサートの方が速い
- バルクインサートよりローダーの方が速い