PostgreSQL の ANALYZE は pg_class 更新でトランザクション・セマンティクスに従わない
序論
PostgreSQL のメーリングリストでこのようなやり取りがありました。
On 10/18/13 12:28 PM, bobJobS wrote:
> If I analyze our database during a transaction and the transaction fails
> (rollback occurs), with the table statistics rollback to their original
> values?Yes.
ANALYZE isn't really that special. It reads data from some tables, does
PostgreSQL: Re: Analyze during a transaction
some math on it, and writes the results to other tables (pg_statistic).
All of that is subject to transaction semantics.
ANALYZE の pg_statistic への更新がトランザクション・セマンティクスに従うことは以前の記事で確認しました。
PostgreSQL の ANALYZE コマンドをトランザクション内で実行した際の挙動 - ぱと隊長日誌
ですが、ANALYZE は pg_class の一部列(※)も更新します。そして、これはトランザクション・セマンティクスに従いません。
(※)PostgreSQL 15 のマニュアルには relpages, reltuples, relallvisible が更新対象と記載されている。
今回の記事ではこれを実験で検証します。
検証結果
同一トランザクションで更新・ANALYZE をする
トランザクション TX1 でテーブルの作成・更新・ANALYZE を実行します。別のトランザクション TX2 で pg_stats(pg_statistic のビュー), pg_class がどのように見えているかを確認します。
-- TX1 =# CREATE TABLE tab1(c1 INTEGER); =# ANALYZE tab1; =# SELECT tablename, attname, n_distinct, histogram_bounds FROM pg_stats WHERE tablename = 'tab1'; tablename | attname | n_distinct | histogram_bounds -----------+---------+------------+------------------ (0 rows) =# SELECT relpages, reltuples, relallvisible FROM pg_class WHERE relname = 'tab1'; relpages | reltuples | relallvisible ----------+-----------+--------------- 0 | 0 | 0 (1 row) -- TX2 =# SELECT tablename, attname, n_distinct, histogram_bounds FROM pg_stats WHERE tablename = 'tab1'; tablename | attname | n_distinct | histogram_bounds -----------+---------+------------+------------------ (0 rows) =# SELECT relpages, reltuples, relallvisible FROM pg_class WHERE relname = 'tab1'; relpages | reltuples | relallvisible ----------+-----------+--------------- 0 | 0 | 0 (1 row) -- TX1 -- トランザクション内で ANALYZE すれば、同一トランザクションでは即時反映される。 =# BEGIN; =# INSERT INTO tab1 SELECT generate_series(1, 10); =# ANALYZE tab1; =# SELECT tablename, attname, n_distinct, histogram_bounds FROM pg_stats WHERE tablename = 'tab1'; tablename | attname | n_distinct | histogram_bounds -----------+---------+------------+------------------------ tab1 | c1 | -1 | {1,2,3,4,5,6,7,8,9,10} (1 row) =# SELECT relpages, reltuples, relallvisible FROM pg_class WHERE relname = 'tab1'; relpages | reltuples | relallvisible ----------+-----------+--------------- 1 | 10 | 0 (1 row) -- TX2 -- TX1 がコミット前なので、pg_stats は反映されていない。だが、pg_class は反映されている。 =# SELECT tablename, attname, n_distinct, histogram_bounds FROM pg_stats WHERE tablename = 'tab1'; tablename | attname | n_distinct | histogram_bounds -----------+---------+------------+------------------ (0 rows) =# SELECT relpages, reltuples, relallvisible FROM pg_class WHERE relname = 'tab1'; relpages | reltuples | relallvisible ----------+-----------+--------------- 1 | 10 | 0 (1 row) -- TX1 =# COMMIT; -- TX2 -- TX1 がコミットすると TX2 にも pg_stats が反映される。 =# SELECT tablename, attname, n_distinct, histogram_bounds FROM pg_stats WHERE tablename = 'tab1'; tablename | attname | n_distinct | histogram_bounds -----------+---------+------------+------------------------ tab1 | c1 | -1 | {1,2,3,4,5,6,7,8,9,10} (1 row) =# SELECT relpages, reltuples, relallvisible FROM pg_class WHERE relname = 'tab1'; relpages | reltuples | relallvisible ----------+-----------+--------------- 1 | 10 | 0 (1 row)
想定通り、pg_stats はトランザクション・セマンティクスに従っています。また、pg_class はトランザクション・セマンティクスに従わず、更新結果が即時反映されています。
進行中のトランザクションと別のトランザクションで ANALYZE をする
トランザクション TX1 でテーブルの作成・更新を実行しますが、コミットはせずに待ちます。別のトランザクション TX2 で ANALYZE を実行し、結果を確認します。
-- TX1 =# CREATE TABLE tab1(c1 INTEGER); =# ANALYZE tab1; =# SELECT tablename, attname, n_distinct, histogram_bounds FROM pg_stats WHERE tablename = 'tab1'; tablename | attname | n_distinct | histogram_bounds -----------+---------+------------+------------------ (0 rows) =# SELECT relpages, reltuples, relallvisible FROM pg_class WHERE relname = 'tab1'; relpages | reltuples | relallvisible ----------+-----------+--------------- 0 | 0 | 0 (1 row) =# BEGIN; =# INSERT INTO tab1 SELECT generate_series(1, 10); -- TX2 -- TX1 をコミットする前に TX2 で ANALYZE しても、pg_class は反映されている。 =# ANALYZE tab1; =# SELECT tablename, attname, n_distinct, histogram_bounds FROM pg_stats WHERE tablename = 'tab1'; tablename | attname | n_distinct | histogram_bounds -----------+---------+------------+------------------ (0 rows) =# SELECT relpages, reltuples, relallvisible FROM pg_class WHERE relname = 'tab1'; relpages | reltuples | relallvisible ----------+-----------+--------------- 1 | 0 | 0 (1 row) -- TX1 -- TX2 の ANALYZE で pg_class を更新した結果は TX1 でも反映されている。 =# SELECT tablename, attname, n_distinct, histogram_bounds FROM pg_stats WHERE tablename = 'tab1'; tablename | attname | n_distinct | histogram_bounds -----------+---------+------------+------------------ (0 rows) =# SELECT relpages, reltuples, relallvisible FROM pg_class WHERE relname = 'tab1'; relpages | reltuples | relallvisible ----------+-----------+--------------- 1 | 0 | 0 (1 row)
TX2 の ANALYZE では pg_stats に TX1 の更新結果が反映されません。TX1 がコミットされていないので、妥当といえます。ただし、pg_class は即時反映されました。
更新処理中に ANALYZE をする
トランザクション TX1 でテーブルの更新を実行します。この更新実行中に別のトランザクション TX2 で ANALYZE を実行し、結果を確認します。
-- TX1 =# CREATE TABLE tab1(c1 INTEGER); =# ANALYZE tab1; =# SELECT tablename, attname, n_distinct, histogram_bounds FROM pg_stats WHERE tablename = 'tab1'; tablename | attname | n_distinct | histogram_bounds -----------+---------+------------+------------------ (0 rows) =# SELECT relpages, reltuples, relallvisible FROM pg_class WHERE relname = 'tab1'; relpages | reltuples | relallvisible ----------+-----------+--------------- 0 | 0 | 0 (1 row) =# BEGIN; =# INSERT INTO tab1 SELECT generate_series(1, 1e+9); -- TX2(TX1 の INSERT 処理中に実行する) -- TX2 での ANALYZE の実行で TX1 の INSERT 処理による relpages の増大が反映されている。 =# ANALYZE tab1; =# SELECT tablename, attname, n_distinct, histogram_bounds FROM pg_stats WHERE tablename = 'tab1'; tablename | attname | n_distinct | histogram_bounds -----------+---------+------------+------------------ (0 rows) =# SELECT relpages, reltuples, relallvisible FROM pg_class WHERE relname = 'tab1'; relpages | reltuples | relallvisible ----------+-----------+--------------- 10254 | 0 | 0 (1 row)
TX1 の INSERT 処理中でも pg_class は即時反映されていることが分かります。
実装調査
ANALYZE で pg_class を更新する際にトランザクション・セマンティクスに従わない理由を調査したところ、PostgreSQL のソースコードで以下のコメントにたどり着きました。
src/backend/commands/vacuum.c
vac_update_relstats() -- update statistics for one relation
<中略>
We violate transaction semantics here by overwriting the rel's existing pg_class tuple with the new values. This is reasonably safe as long as we're sure that the new values are correct whether or not this transaction commits. The reason for doing this is that if we updated these tuples in the usual way, vacuuming pg_class itself wouldn't work very well --- by the time we got done with a vacuum cycle, most of the tuples in pg_class would've been obsoleted. Of course, this only works for fixed-size not-null columns, but these are.
Another reason for doing it this way is that when we are in a lazy VACUUM and have PROC_IN_VACUUM set, we mustn't do any regular updates. Somebody vacuuming pg_class might think they could delete a tuple marked with xmin = our xid.
トランザクション・セマンティクスに従わないとしても、その値が正しいのであれば問題にならない、と主張しているように読めます。
relpages はテーブルの物理サイズの推測値です。この値はプランナで利用されており、重要です。
TX1 の ANALYZE の pg_class 更新がトランザクション・セマンティクスに従い、コミットするまで反映されなかったときの影響を考えます。TX2 は 0 ページと仮定してコスト計算を行ったとしても、実際には 1 ページあるかもしれません。この程度であれば誤差範囲かもしれませんが、もっと大量のデータ更新があればプランナおよび実行計画への影響が大きいでしょう。
このように考えると、ANALYZE の pg_class 更新でトランザクション・セマンティクスに従わないことによるメリットが PostgreSQL の利用者側にとってもありそうです。
まとめ
ANALYZE は pg_statistic (pg_stats) の更新をトランザクション・セマンティクスに従います。一方で pg_class は更新を即時反映します。
この仕様は適切な実行計画を作成する手助けとなっているかもしれません。