次のようなクエリがあります
_SELECT COUNT(1)
FROM article
JOIN reservation ON a_id = r_article_id
WHERE r_last_modified < now() - '8 weeks'::interval
AND r_group_id = 1
AND r_status = 'OPEN';
_
多くの場合、タイムアウトが発生する(10分後)ため、問題を調査することにしました。
EXPLAIN (ANALYZE, BUFFERS)
出力は次のようになります。
_ Aggregate (cost=264775.48..264775.49 rows=1 width=0) (actual time=238960.290..238960.291 rows=1 loops=1)
Buffers: shared hit=200483 read=64361 dirtied=666 written=8, temp read=3631 written=3617
I/O Timings: read=169806.955 write=0.154
-> Hash Join (cost=52413.67..264647.65 rows=51130 width=0) (actual time=1845.483..238957.588 rows=21644 loops=1)
Hash Cond: (reservation.r_article_id = article.a_id)
Buffers: shared hit=200483 read=64361 dirtied=666 written=8, temp read=3631 written=3617
I/O Timings: read=169806.955 write=0.154
-> Index Scan using reservation_r_article_id_idx1 on reservation (cost=0.42..205458.72 rows=51130 width=4) (actual time=34.035..237000.197 rows=21644 loops=1)
Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
Rows Removed by Filter: 151549
Buffers: shared hit=200193 read=48853 dirtied=450 written=8
I/O Timings: read=168614.105 write=0.154
-> Hash (cost=29662.22..29662.22 rows=1386722 width=4) (actual time=1749.392..1749.392 rows=1386814 loops=1)
Buckets: 32768 Batches: 8 Memory Usage: 6109kB
Buffers: shared hit=287 read=15508 dirtied=216, temp written=3551
I/O Timings: read=1192.850
-> Seq Scan on article (cost=0.00..29662.22 rows=1386722 width=4) (actual time=23.822..1439.310 rows=1386814 loops=1)
Buffers: shared hit=287 read=15508 dirtied=216
I/O Timings: read=1192.850
Total runtime: 238961.812 ms
_
ボトルネックノードは明らかにインデックススキャンです。それでは、インデックスの定義を見てみましょう。
_CREATE INDEX reservation_r_article_id_idx1
ON reservation USING btree (r_article_id)
WHERE (r_status <> ALL (ARRAY['FULFILLED', 'CLOSED', 'CANCELED']));
_
サイズ(_\di+
_または物理ファイルにアクセスして報告される)は36 MBです。予約は通常、上記にリストされていないすべてのステータスで比較的短い時間しか費やしていないため、更新が大量に行われているため、インデックスはかなり肥大化しています(ここでは約24 MBが無駄になっています)。
reservation
テーブルのサイズは約3.8 GBで、約4,000万行が含まれています。まだクローズされていない予約の数は約170,000です(正確な数は上記のインデックススキャンノードで報告されます)。
サプライズ:インデックススキャンは大量のバッファ(つまり、8 kbページ)のフェッチを報告します。
_Buffers: shared hit=200193 read=48853 dirtied=450 written=8
_
キャッシュとディスク(またはOSキャッシュ)から読み取った数値の合計は1.9 GBです!
一方、最悪のシナリオでは、すべてのタプルがテーブルの別のページにある場合、訪問(21644 + 151549)+ 4608ページ(テーブルからフェッチされた行の合計と物理ページからのインデックスページ番号)が考慮されます。サイズ)。これはまだ180,000未満です-観測されているほぼ250,000をはるかに下回っています。
興味深い(そしておそらく重要な)のは、ディスクの読み取り速度が約2.2 MB/sであることです。これはごく普通のことです。
この矛盾がどこから来るのかについて誰かが考えを持っていますか?
注:明確にするために、ここで改善/変更すべき点がありますが、実際に得た数値を理解したいと思います-これは、質問についてです。
jjanesの回答 に基づいて、まったく同じクエリをすぐに再実行するとどうなるかを確認しました。影響を受けるバッファの数は実際には変化しません。 (これを行うために、クエリを最小限に抑えて、問題をまだ示しています。)これは、最初の実行で確認したものです。
_ Aggregate (cost=240541.52..240541.53 rows=1 width=0) (actual time=97703.589..97703.590 rows=1 loops=1)
Buffers: shared hit=413981 read=46977 dirtied=56
I/O Timings: read=96807.444
-> Index Scan using reservation_r_article_id_idx1 on reservation (cost=0.42..240380.54 rows=64392 width=0) (actual time=13.757..97698.461 rows=19236 loops=1)
Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
Rows Removed by Filter: 232481
Buffers: shared hit=413981 read=46977 dirtied=56
I/O Timings: read=96807.444
Total runtime: 97703.694 ms
_
そして2番目の後に:
_ Aggregate (cost=240543.26..240543.27 rows=1 width=0) (actual time=388.123..388.124 rows=1 loops=1)
Buffers: shared hit=460990
-> Index Scan using reservation_r_article_id_idx1 on reservation (cost=0.42..240382.28 rows=64392 width=0) (actual time=0.032..385.900 rows=19236 loops=1)
Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
Rows Removed by Filter: 232584
Buffers: shared hit=460990
Total runtime: 388.187 ms
_
ここで重要なのは、多くの更新とインデックスの膨らみだと思います。
インデックスには、「ライブ」ではなくなったテーブル内の行へのポインタが含まれています。これらは、更新された行の古いバージョンです。古い行のバージョンは、古いスナップショットでクエリを満たすためにしばらく保持され、その後、必要以上に頻繁にそれらを削除する作業を行う必要がないため、しばらく保持されます。
インデックスをスキャンするとき、これらの行を訪問する必要があり、それらが表示されなくなったことに気づくため、それらを無視します。 explain (analyze,buffers)
ステートメントは、これらの行を検査するプロセスでの読み取り/ヒットバッファーのカウントを除いて、このアクティビティを明示的に報告しません。
Btreeにはいくつかの「マイクロバキューム」コードがあり、スキャンが再びインデックスに戻ると、追跡したポインタがもう存在していないことを記憶し、インデックスでデッドとしてマークします。そうすれば、次に実行される同様のクエリで、再度追跡する必要がなくなります。そのため、まったく同じクエリをもう一度実行すると、バッファアクセスが予測したものに近づくことがわかります。
テーブルをより頻繁にVACUUM
することもできます。これにより、部分インデックスだけでなく、テーブル自体から不要なタプルが削除されます。一般に、回転率の高い部分インデックスを持つテーブルは、デフォルトレベルよりも積極的なバキュームの恩恵を受ける可能性があります。