PostgreSQL 13 統計情報の更新

スポンサーリンク

PostgreSQLに限らず多くのRDBMSはどのようにデータベースを探索するのかが高速なのかの「実行計画」を立てます。クエリを発行する都度、データベースの大きさ等を計測するのは非現実的な計算量になるので、一定周期で計測する「統計情報」に基づいて「実行計画」を立てます。

このページでは「統計情報」について考察します。

統計情報の挙動確認

初期設定

統計情報は定期的に自動更新され、多くの場合は統計情報による問題は発生しません。まず、統計情報が自動更新されないと、どのような不都合が発生するかを観察してみましょう。

動作確認のため、統計情報の自動更新が発生しづらいパラメタを投入します。

これは仕様理解を目的とした実験的な設定です。実務では使用しないよう注意ください。

# vi /var/lib/pgsql/13/data/postgresql.conf

 <omitted>

autovacuum_analyze_threshold = 2000

再起動で設定を反映させます。

# systemctl restart postgresql-13.service

動作確認用に200件のレコードを挿入します。

dropdb test
createdb test
psql test << EOF
CREATE TABLE staff(
  id      serial      PRIMARY KEY,
  number  integer     default 0,
  f_name  varchar(30) default '',
  l_name  varchar(30) default ''
);
EOF

for i in {1..200}
do
  [ $((${i} % 100)) == 0 ] && echo "INSERT : ${i} records "
  psql test -c "INSERT INTO staff (number) VALUES ('${i}');" > /dev/null
done

統計情報の手動更新

多くの場合、統計情報は自動更新されるため大きな問題には発展しません。自動更新で不都合がある場合は手動の更新も可能です。

このシナリオの場合、自動更新が発生しづらくなっています。統計情報が更新されたか否かはpg_stat_all_tablesテーブルより確認が可能です。analyze_countが統計情報を手動更新した回数で、autoanalyze_countが統計情報を自動更新した回数です。

[postgres@centos10 ~]$ psql test << EOF
> \x
> SELECT * FROM pg_stat_all_tables where relname ='staff';
> EOF
Expanded display is on.
-[ RECORD 1 ]-------+-------
relid               | 17154
schemaname          | public
relname             | staff
seq_scan            | 1
seq_tup_read        | 0
idx_scan            | 0
idx_tup_fetch       | 0
n_tup_ins           | 200
n_tup_upd           | 0
n_tup_del           | 0
n_tup_hot_upd       | 0
n_live_tup          | 200
n_dead_tup          | 0
n_mod_since_analyze | 200
n_ins_since_vacuum  | 200
last_vacuum         | 
last_autovacuum     | 
last_analyze        | 
last_autoanalyze    | 
vacuum_count        | 0
autovacuum_count    | 0
analyze_count       | 0
autoanalyze_count   | 0

それでは現在の統計情報を見てみましょう。pg_classテーブルを見ると、現在300件のレコード(tuples)が挿入されているのにも関わらず、0件と表示されている事がわかります。これは統計情報が1度も更新されていないためです。

test=# SELECT relpages, reltuples FROM pg_class WHERE relname = 'staff';
 relpages | reltuples 
----------+-----------
        0 |         0
(1 row)

統計情報を手動で更新するには、ANALYZEコマンドを使用します。

test=# ANALYZE staff ;
ANALYZE
test=# 

統計情報が更新されreltuplesが300に変わった事が確認できます。

test=# SELECT relpages, reltuples FROM pg_class WHERE relname = 'staff';
 relpages | reltuples 
----------+-----------
        2 |       200
(1 row)

実行計画の観察

さらに100件のレコードを追加し、計300件の状態にします。

for i in {201..300}
do
  [ $((${i} % 100)) == 0 ] && echo "INSERT : ${i} records "
  psql test -c "INSERT INTO staff (number) VALUES ('${i}');" > /dev/null
done

この状態で1件のレコードを取得するSQLの実行計画を見てみます。Seq Scanと呼ばれる探索方法でcostが4.50かかるとの計算結果です。

test=# EXPLAIN SELECT * FROM staff WHERE id = 50;
                      QUERY PLAN                      
------------------------------------------------------
 Seq Scan on staff  (cost=0.00..4.50 rows=1 width=10)
   Filter: (id = 50)
(2 rows)

実行計画はデータベースをどのように探索するのが最短かを計画立てる機能で、Seq Scan, Index Scanなどの様々な方法があります。この様々な方法をcost比較し、最も最短となる方法を探します。

データベースを全走で探索するSeq Scanは以下のようにコストを算出します。

(ディスクページ読み取り x seq_page_cost) + (スキャンした行 x cpu_tuple_cost)

全走探索で読み取りを実施した後、「WHERE id = 50」でidが一致するかどうかの演算処理が発生します。演算に伴うコストは以下の通り算出します。

(演算回数 x cpu_operator_cost)

デフォルト設定ではseq_page_cost=1.0, cpu_tuple_cost=0.01, cpu_operator_cost=0.0025です。統計情報は更新されてないので、ペース数=2, カラム数=200です。

これら値を元に計算すると、コストは以下のようになります。

(2 * 1.0) + (200 * 0.01) + (200 * 0.0025) = 4.50

さて、ここで統計情報を更新し、もう1回、実行計画を見てみましょう。costが4.50から5.75に増えている事が分かります。

test=# ANALYZE staff;
ANALYZE
test=# SELECT relpages, reltuples FROM pg_class WHERE relname = 'staff';
 relpages | reltuples 
----------+-----------
        2 |       300
(1 row)
test=# EXPLAIN SELECT * FROM staff WHERE id = 50;
                      QUERY PLAN                      
------------------------------------------------------
 Seq Scan on staff  (cost=0.00..5.75 rows=1 width=10)
   Filter: (id = 50)
(2 rows)

cost5.75の算出根拠は以下の通りです。

(2 * 1.0) + (300 * 0.01) + (300 * 0.0025) = 5.75

統計情報更新頻度のチューニング

マスタデータならばバッチ処理によるデータ更新の都度、手動VACUUMを実行し統計情報を更新すると良いでしょう。一方、トランザクションデータならば、自動の統計情報更新に任せれば多くの場合はうまくいきます。以下、統計情報の自動更新に関するパラメタをまとめます。

統計情報の自動更新に関するパラメタ

下記の「ANALYZE閾値」を超えた時に、自動バキュームデーモンがANALYZEを開始します。「ANALYZE閾値」は以下の数式によって与えられます。

ANALYZE閾値 = ANALYZE基礎閾値 + ANALYZE規模係数 * タプル数

「ANALYZE基礎閾値」「ANALYZE規模係数」は、それぞれpostgresql.confのautovacuum_analyze_thresholdとautovacuum_analyze_scale_factorでデフォルト設定は50と0.1になります。

[postgres@centos10 ~]$ psql -c "show all " | grep analyze
 autovacuum_analyze_scale_factor        | 0.1                                    | Number of tuple inserts, updates, or deletes prior to analyze as a fraction of reltuples.
 autovacuum_analyze_threshold           | 50                                     | Minimum number of tuple inserts, updates, or deletes prior to analyze.

例えば、10,000行のテーブルの場合ならば、

50 + 10,000 * 0.1 = 1,050

の計算で1,050を超えるレコードを更新すればANALYZEが発生します。充分大きなテーブルならば、約10%が更新されれればANALYZEされる事になります。

統計情報の自動更新の初回実行

さきほど実験目的で投入したautovacuum_analyze_thresholdをコメントアウトしデフォルト設定に戻します。さらにANALYZEを観察しやすいようlog_min_messagesをDEBUG2に変更します。

vi /var/lib/pgsql/13/data/postgresql.conf

 <omitted>

#autovacuum_analyze_threshold = 2000
log_min_messages = 'DEBUG2'

設定を反映させます。

systemctl restart postgresql-13.service

autovacuum_analyze_thresholdのデフォルト設定は50ですので、新規作成されたテーブルに50を超えるレコードが作成されると、統計情報が更新されます。統計情報の初回更新の様子を観察するために51件のテーブルを作成します。

dropdb test
createdb test
psql test << EOF
CREATE TABLE staff(
  id      serial      PRIMARY KEY,
  number  integer     default 0,
  f_name  varchar(30) default '',
  l_name  varchar(30) default ''
);
EOF

for i in {1..51}
do
  [ $((${i} % 100)) == 0 ] && echo "INSERT : ${i} records "
  psql test -c "INSERT INTO staff (number) VALUES ('${i}');" > /dev/null
done

統計情報が更新されるまで待ちます。下記統計情報を表示するSQL文を実行し、レコード件数が51件と表示されれば統計情報が更新された事を意味します。

[postgres@centos10 ~]$ psql test -c "SELECT relpages, reltuples FROM pg_class WHERE relname = 'staff';"
 relpages | reltuples 
----------+-----------
        1 |        51
(1 row)

pg_stat_all_tablesテーブルのlast_autoanalyzeやautoanalyze_countからも統計情報が更新された事を読み取れます。

[postgres@centos10 ~]$ psql test << EOF
> \x
> SELECT * FROM pg_stat_all_tables where relname ='staff';
> EOF
Expanded display is on.
-[ RECORD 1 ]-------+----------------------------
relid               | 17250
schemaname          | public
relname             | staff
seq_scan            | 1
seq_tup_read        | 0
idx_scan            | 0
idx_tup_fetch       | 0
n_tup_ins           | 51
n_tup_upd           | 0
n_tup_del           | 0
n_tup_hot_upd       | 0
n_live_tup          | 51
n_dead_tup          | 0
n_mod_since_analyze | 0
n_ins_since_vacuum  | 51
last_vacuum         | 
last_autovacuum     | 
last_analyze        | 
last_autoanalyze    | 2021-01-23 11:52:05.1378+09
vacuum_count        | 0
autovacuum_count    | 0
analyze_count       | 0
autoanalyze_count   | 1

ログレベルをDEBUG2以上にすれば、ログにも統計情報を更新された旨の出力が表示されます。

$ tail -f /var/lib/pgsql/13/data/log/postgresql-Sat.log

 <omitted>

2021-01-23 11:52:05.133 JST [27038] DEBUG:  "public.staff"を解析しています
2021-01-23 11:52:05.137 JST [27038] DEBUG:  "staff": 1ページの内1をスキャン。51の有効な行と0の不要な行が存在。51行をサンプリング。推定総行数は51

充分大きいテーブルの統計情報の自動更新

9,949件のレコードを追加し計10,000レコードのテーブルを作成します。また、作成後に手動VACUUMを実行します。

for i in {52..10000}
do
  [ $((${i} % 100)) == 0 ] && echo "INSERT : ${i} records "
  psql test -c "INSERT INTO staff (number) VALUES ('${i}');" > /dev/null
done
psql test -c "VACUUM staff;"

10,000レコードのテーブルを作成した状態のpg_stat_all_tablesをメモに残します。後続のシナリオでautoanalyze_countが増えた事から、統計情報の更新がなされた事を確認します。

[postgres@centos10 ~]$ psql test << EOF
> \x
> SELECT * FROM pg_stat_all_tables where relname ='staff';
> EOF
Expanded display is on.
-[ RECORD 1 ]-------+------------------------------
relid               | 17250
schemaname          | public
relname             | staff
seq_scan            | 1
seq_tup_read        | 0
idx_scan            | 0
idx_tup_fetch       | 0
n_tup_ins           | 10000
n_tup_upd           | 0
n_tup_del           | 0
n_tup_hot_upd       | 0
n_live_tup          | 9834
n_dead_tup          | 0
n_mod_since_analyze | 179
n_ins_since_vacuum  | 0
last_vacuum         | 2021-01-23 12:24:51.796116+09
last_autovacuum     | 2021-01-23 12:23:05.741684+09
last_analyze        | 
last_autoanalyze    | 2021-01-23 12:23:05.75281+09
vacuum_count        | 1
autovacuum_count    | 2
analyze_count       | 0
autoanalyze_count   | 3

この10,000行のテーブルは、以下計算式の通り、1,050行を超えるレコードが更新された時に統計情報が自動更新されます。

50 + 10,000 * 0.1 = 1,050

1,051行のレコードを追加します。

for i in {10001..11051}
do
  [ $((${i} % 100)) == 0 ] && echo "INSERT : ${i} records "
  psql test -c "INSERT INTO staff (number) VALUES ('${i}');" > /dev/null
done

pg_stat_all_tablesを見ると、autoanalyze_countが増えている事が分かります。確かに統計情報が更新された事が分かります。

[postgres@centos10 ~]$ psql test << EOF
> \x
> SELECT * FROM pg_stat_all_tables where relname ='staff';
> EOF
Expanded display is on.
-[ RECORD 1 ]-------+------------------------------
relid               | 17250
schemaname          | public
relname             | staff
seq_scan            | 1
seq_tup_read        | 0
idx_scan            | 0
idx_tup_fetch       | 0
n_tup_ins           | 11051
n_tup_upd           | 0
n_tup_del           | 0
n_tup_hot_upd       | 0
n_live_tup          | 11051
n_dead_tup          | 0
n_mod_since_analyze | 0
n_ins_since_vacuum  | 1051
last_vacuum         | 2021-01-23 12:24:51.796116+09
last_autovacuum     | 2021-01-23 12:23:05.741684+09
last_analyze        | 
last_autoanalyze    | 2021-01-23 12:27:05.784366+09
vacuum_count        | 1
autovacuum_count    | 2
analyze_count       | 0
autoanalyze_count   | 4

ログにも統計情報を更新された旨の出力が表示されます。

$ tail -f /var/lib/pgsql/13/data/log/postgresql-Sat.log

2021-01-23 12:27:05.773 JST [49298] DEBUG:  "public.staff"を解析しています
2021-01-23 12:27:05.778 JST [49298] DEBUG:  "staff": 60ページの内60をスキャン。11051の有効な行と0の不要な行が存在。11051行をサンプリング。推定総行数は11051
2021-01-23 12:27:05.786 JST [26919] DEBUG:  サーバプロセス (PID 49298)は終了コード0で終了しました
タイトルとURLをコピーしました