KDOC 652: 明示的なトランザクションの小さいINSERTは時間の大半がCOMMITに数えられる
この文書のステータス
- 作成
- 2026-10-11 貴島
- レビュー
- 2026-10-11 貴島
概要
Aurora PostgreSQL の Performance Insights で、Top SQL の1位が COMMIT だった。SELECTのクエリは集計の項目として出ていたが、DMLのクエリは出てないように見えた。アプリケーションは RLS のために、すべてのクエリを BEGIN〜COMMIT のトランザクションで実行していた。書き込みの時間は、どんな書き方のときに COMMIT の行に出るのかを確かめる。
pgbench で INSERT を流しながら、別のセッションが 10 ms ごとに pg_stat_activity でアクティブなセッションの文を数える。2つのセッションが同時に要るので、この実験は pgpg のプロキシを通さず、PostgreSQL に直接つなぐ。
# 書き込む側と標本を取る側の2つのセッションが同時に要るので、pgpg のプロキシを通さず PostgreSQL に直接(15433)つなぐ
sample() {
psql -h localhost -p 15433 -U postgres -d postgres -qAt -F ' | ' <<'SQL'
CREATE TABLE samples (query text, wait text);
DO $$
BEGIN
FOR i IN 1..400 LOOP
PERFORM pg_stat_clear_snapshot();
INSERT INTO samples
-- 待機イベントがなければ CPU で動いている
SELECT query, coalesce(wait_event_type || ':' || wait_event, 'CPU')
FROM pg_stat_activity
WHERE application_name = 'pgbench' AND state = 'active';
PERFORM pg_sleep(0.01);
END LOOP;
END $$;
SELECT query, wait, count(*) FROM samples GROUP BY 1, 2 ORDER BY 3 DESC;
DROP TABLE samples;
SQL
}
# $1: 説明、$2: pgbench に流す SQL、$3: pgbench のオプション
run() {
echo "== $1"
docker exec pgpg-postgres-1 psql -U postgres -qAt \
-c "DROP TABLE IF EXISTS w" -c "CREATE TABLE w (id int, pad text)" \
-c "CREATE INDEX ON w (id)" -c "CREATE INDEX ON w (pad)" 2>/dev/null
printf "$2" | docker exec -i pgpg-postgres-1 sh -c 'cat > /tmp/bench.sql'
out=$(mktemp)
# 拡張プロトコル(-M extended)で書き込む
docker exec pgpg-postgres-1 pgbench -U postgres -n -M extended $3 \
-f /tmp/bench.sql postgres > "$out" 2>/dev/null &
sleep 0.3
sample
wait
grep 'latency average' "$out"
rm -f "$out"
}
run "小さい INSERT を明示的なトランザクションで(4並列、5秒)" \
'BEGIN;\nINSERT INTO w VALUES (1, md5(random()::text));\nCOMMIT;\n' "-c 4 -T 5"
run "小さい INSERT を自動コミットで(4並列、5秒)" \
'INSERT INTO w VALUES (1, md5(random()::text));\n' "-c 4 -T 5"
run "50万行の INSERT を明示的なトランザクションで1回" \
'BEGIN;\nINSERT INTO w SELECT i, md5(i::text) FROM generate_series(1, 500000) i;\nCOMMIT;\n' "-c 1 -t 1"
docker exec pgpg-postgres-1 psql -U postgres -qAt -c "DROP TABLE w"
== 小さい INSERT を明示的なトランザクションで(4並列、5秒)
COMMIT; | LWLock:WALWrite | 1128
COMMIT; | IO:WalSync | 395
INSERT INTO w VALUES (1, md5(random()::text)); | CPU | 21
COMMIT; | CPU | 8
INSERT INTO w VALUES (1, md5(random()::text)); | Client:ClientRead | 3
BEGIN; | CPU | 3
BEGIN; | Client:ClientRead | 2
COMMIT; | IO:WalWrite | 2
INSERT INTO w VALUES (1, md5(random()::text)); | IO:DataFileExtend | 1
latency average = 1.684 ms
== 小さい INSERT を自動コミットで(4並列、5秒)
INSERT INTO w VALUES (1, md5(random()::text)); | LWLock:WALWrite | 1144
INSERT INTO w VALUES (1, md5(random()::text)); | IO:WalSync | 394
INSERT INTO w VALUES (1, md5(random()::text)); | CPU | 38
INSERT INTO w VALUES (1, md5(random()::text)); | Client:ClientRead | 6
INSERT INTO w VALUES (1, md5(random()::text)); | IO:WalWrite | 3
latency average = 1.678 ms
== 50万行の INSERT を明示的なトランザクションで1回
INSERT INTO w SELECT i, md5(i::text) FROM generate_series(1, 500000) i; | CPU | 148
INSERT INTO w SELECT i, md5(i::text) FROM generate_series(1, 500000) i; | IO:DataFileExtend | 6
INSERT INTO w SELECT i, md5(i::text) FROM generate_series(1, 500000) i; | IO:WalWrite | 4
INSERT INTO w SELECT i, md5(i::text) FROM generate_series(1, 500000) i; | IO:WalSync | 4
INSERT INTO w SELECT i, md5(i::text) FROM generate_series(1, 500000) i; | LWLock:WALWrite | 3
COMMIT; | IO:WalSync | 1
latency average = 1934.744 ms
- 小さい INSERT を明示的なトランザクションで繰り返すと、標本の約98%が COMMIT だった。COMMIT の標本のほとんどは、WAL の確定を待つ IO:WalSync と LWLock:WALWrite だった
- 同じ INSERT を自動コミットにすると、標本はすべて INSERT になった。待機イベントの内訳も、1回あたりの時間も変わらない
- 50万行の INSERT を1回でコミットすると、標本のほとんどは INSERT の CPU で、COMMIT は1回だけだった。INSERT の最中にも IO:WalWrite と IO:WalSync が出ている。
WalSync: WALファイルが永続的ストレージに達するのを待機しています。
WALWrite: WALバッファがディスクに書き込まれるのを待機しています。
関連
抽象。
- 時間がどの項目に数えられるかは、処理のまとめ方によって変わる
関連。
- 類似: KDOC 617: Cloudwatch の dbLoad は負荷ではない。Performance Insights の負荷は待機中のセッションも数えるので、WAL の確定を待つ COMMIT が上位に出る
- 前提: KDOC 649: DML は文の中で記録を作り COMMIT は永続化を待つ。コミット処理で何を待っているか
- 前提: KDOC 650: Performance Insights の SQL 統計は pg_stat_statements を集計している
- 前提: KDOC 651: pg_stat_statements は COMMIT のときに走る処理の時間を記録しない