結合方式とインデックスの関係
ネスト化ループ結合/マージ結合/ハッシュ結合は、それぞれインデックスをどのように使うのか、を整理する。
マスタのxxコードとトランザクションの同名の列を等結合にするSQLについて、
- インデックスが片側(マスタ側のみ)
- インデックスが両側
- インデックスなし
の3条件で、各結合方式の実行速度を計測してみる。
テスト内容
PostgreSQL8.3.5で以下のSQLを実行する。
explain analyze select * from tellers t -- 1000件のマスタ inner join history h on h.tid=t.tid -- 1000万件のトランザクション
結合方式は以下の手順で制御する。
- ふつうに実行すると、プランナはハッシュ結合を選択する。
- set enable_hashjoin to off を実行すると、プランナはマージ結合を選択する。
- さらに set enable_mergejoin to off を実行すると、プランナはネスト化ループ結合を選択する。
結論
- ハッシュ結合はインデックスの有無に関係なく、安定的に高速で動作する。
- これはあらゆるSQL・DBMSに通用する話ではないと予想する
- もし'select count(*) from ...'だったらインデックスだけ見て結合処理できるので、インデックスがあるとハッシュ結合が高速になるRDBMSもあるだろう
- PostgreSQLは「インデックスだけ見てデータは見ない」ということができないらしいので*1上記の結論になった
- マージ結合は両側にインデックスがあるときに高速で動作する。
- 実DBでは「インデックスがマスタ側のみ」というケースが多いので、マージ結合はあまり選択されない
- ネスト化ループ結合にはインデックスが必要。
- 両側にインデックスがある場合、片側にインデックスがある時よりも速くなることがある。件数の少ないマスタを外部表にしてIndex Scanの回数を減らせるから(これ知っててもあまり役に立たないと思うが)
1. 片側(マスタ側)だけインデックスがある場合
これは普通によくある状態。プランナはハッシュ結合を選択する。
ハッシュ結合
31秒ぐらいで完了。
QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------- Hash Join (cost=27.50..330985.64 rows=10000006 width=399) (actual time=1.608..24852.165 rows=10000000 loops=1) Hash Cond: (h.tid = t.tid) -> Seq Scan on history h (cost=0.00..193458.06 rows=10000006 width=47) (actual time=0.010..7285.277 rows=10000000 loops=1) -> Hash (cost=15.00..15.00 rows=1000 width=352) (actual time=1.586..1.586 rows=1000 loops=1) -> Seq Scan on tellers t (cost=0.00..15.00 rows=1000 width=352) (actual time=0.007..0.720 rows=1000 loops=1) Total runtime: 30923.980 ms (6 rows)
マージ結合
インデックスがないトランザクションをソートする処理が重く、80秒近くかかる。
QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------------------- Merge Join (cost=2586608.05..2736653.47 rows=10000006 width=399) (actual time=24793.299..72566.423 rows=10000000 loops=1) Merge Cond: (t.tid = h.tid) -> Index Scan using tellers_pkey on tellers t (cost=0.00..43.25 rows=1000 width=352) (actual time=0.023..1.467 rows=1000 loops=1) -> Materialize (cost=2586607.63..2711607.71 rows=10000006 width=47) (actual time=24793.246..57446.134 rows=10000000 loops=1) -> Sort (cost=2586607.63..2611607.65 rows=10000006 width=47) (actual time=24793.241..41378.094 rows=10000000 loops=1) Sort Key: h.tid Sort Method: external merge Disk: 576448kB -> Seq Scan on history h (cost=0.00..193458.06 rows=10000006 width=47) (actual time=0.007..13119.637 rows=10000000 loops=1) Total runtime: 78964.182 ms (9 rows)
ネスト化ループ結合
80秒ぐらいかかる。外部表のトランザクションは1000万件あるから、1000万回のIndex Scanが発生している。
QUERY PLAN --------------------------------------------------------------------------------------------------------------------------------------- Nested Loop (cost=0.00..66995499.74 rows=10000006 width=399) (actual time=0.033..73155.158 rows=10000000 loops=1) -> Seq Scan on history h (cost=0.00..193458.06 rows=10000006 width=47) (actual time=0.010..7444.253 rows=10000000 loops=1) -> Index Scan using tellers_pkey on tellers t (cost=0.00..6.67 rows=1 width=352) (actual time=0.003..0.004 rows=1 loops=10000000) Index Cond: (t.tid = h.tid) Total runtime: 79494.501 ms
2. 両側にインデックスがある場合
トランザクションの結合カラムにインデックスを追加して、SQLを各結合方式で再実行してみる。
ハッシュ結合
基本的にハッシュ結合にはインデックスは関係ない。速さは変わらない。
QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------- Hash Join (cost=27.50..330985.50 rows=10000000 width=399) (actual time=1.616..25016.110 rows=10000000 loops=1) Hash Cond: (h.tid = t.tid) -> Seq Scan on history h (cost=0.00..193458.00 rows=10000000 width=47) (actual time=0.032..7252.089 rows=10000000 loops=1) -> Hash (cost=15.00..15.00 rows=1000 width=352) (actual time=1.571..1.571 rows=1000 loops=1) -> Seq Scan on tellers t (cost=0.00..15.00 rows=1000 width=352) (actual time=0.007..0.709 rows=1000 loops=1) Total runtime: 31078.684 ms (6 rows)
マージ結合
両側にインデックスがある場合、ソートが不要なので速い。
速さはハッシュ結合と同等。31秒。
QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------------------- Merge Join (cost=0.41..456291.88 rows=10000000 width=399) (actual time=3.505..24941.755 rows=10000000 loops=1) Merge Cond: (t.tid = h.tid) -> Index Scan using tellers_pkey on tellers t (cost=0.00..43.25 rows=1000 width=352) (actual time=3.432..4.556 rows=1000 loops=1) -> Index Scan using history_tid_idx on history h (cost=0.00..331246.13 rows=10000000 width=47) (actual time=0.050..9568.710 rows=10000000 loops=1) Total runtime: 31077.265 ms (5 rows)
ネスト化ループ結合
意外なことにこれが一番速い。30秒。
両側にインデックスがある場合、外部表を件数の少ないマスタにできる。
この結果 Index Scan の回数が1000万回から1000回になり、速くなった模様。
QUERY PLAN ------------------------------------------------------------------------------------------------------------------------------------------------- Nested Loop (cost=0.00..6970250.84 rows=10000000 width=399) (actual time=0.153..23995.164 rows=10000000 loops=1) -> Seq Scan on tellers t (cost=0.00..15.00 rows=1000 width=352) (actual time=0.030..0.995 rows=1000 loops=1) -> Index Scan using history_tid_idx on history h (cost=0.00..5720.24 rows=100000 width=47) (actual time=0.006..9.600 rows=10000 loops=1000) Index Cond: (h.tid = t.tid) Total runtime: 30116.411 ms (5 rows)
3. インデックスがない場合
マスタ/トランザクションのインデックスを削除して、SQLを各結合方式で再実行する。
ハッシュ結合
実行計画も速さも変わらない。31秒。
QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------- Hash Join (cost=27.50..330985.50 rows=10000000 width=399) (actual time=1.657..25184.198 rows=10000000 loops=1) Hash Cond: (h.tid = t.tid) -> Seq Scan on history h (cost=0.00..193458.00 rows=10000000 width=47) (actual time=0.040..7314.014 rows=10000000 loops=1) -> Hash (cost=15.00..15.00 rows=1000 width=352) (actual time=1.603..1.603 rows=1000 loops=1) -> Seq Scan on tellers_no_keys t (cost=0.00..15.00 rows=1000 width=352) (actual time=0.007..0.728 rows=1000 loops=1) Total runtime: 31312.917 ms (6 rows)
マージ結合
1000万件のトランザクションをソートするコストが重い。80秒ぐらい。
QUERY PLAN -------------------------------------------------------------------------------------------------------------------------------------------- Merge Join (cost=2586671.71..2736676.66 rows=10000000 width=399) (actual time=21971.475..72353.744 rows=10000000 loops=1) Merge Cond: (t.tid = h.tid) -> Sort (cost=64.83..67.33 rows=1000 width=352) (actual time=1.420..2.094 rows=1000 loops=1) Sort Key: t.tid Sort Method: quicksort Memory: 56kB -> Seq Scan on tellers_no_keys t (cost=0.00..15.00 rows=1000 width=352) (actual time=0.012..0.673 rows=1000 loops=1) -> Materialize (cost=2586606.83..2711606.83 rows=10000000 width=47) (actual time=21970.029..56852.698 rows=10000000 loops=1) -> Sort (cost=2586606.83..2611606.83 rows=10000000 width=47) (actual time=21970.025..40503.733 rows=10000000 loops=1) Sort Key: h.tid Sort Method: external merge Disk: 576448kB -> Seq Scan on history h (cost=0.00..193458.00 rows=10000000 width=47) (actual time=0.033..9745.576 rows=10000000 loops=1) Total runtime: 78873.202 ms (12 rows)
ネスト化ループ結合
1000万件×1000件のネスト化ループ結合は、インデックスがなければ使いものにならない。
10分経っても終わらないので、実行計画だけ載せておく。
QUERY PLAN ----------------------------------------------------------------------------------- Nested Loop (cost=16.00..225193474.00 rows=10000000 width=399) Join Filter: (t.tid = h.tid) -> Seq Scan on history h (cost=0.00..193458.00 rows=10000000 width=47) -> Materialize (cost=16.00..26.00 rows=1000 width=352) -> Seq Scan on tellers_no_keys t (cost=0.00..15.00 rows=1000 width=352) (5 rows)