Quantcast
Channel: PostgreSQL Deep Dive
Viewing all 94 articles
Browse latest View live

パフォーマンス統計情報のスナップショットを取得する

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 8です。

今日は、PostgreSQLの統計情報を網羅的に収集・蓄積するPgPerfパッケージの紹介をします。

このパッケージは、本日リリースしたものです。

snaga/pgperf-snapshot
https://github.com/uptimejp/pgperf-snapshot

■PostgreSQLの統計情報の記録と監視


PostgreSQLでは、動作についての内部の情報を取得するために、さまざまなシステムビューやSQL関数があります。


統計情報コレクタ
http://www.postgresql.jp/document/9.0/html/monitoring-stats.html
システム管理関数
http://www.postgresql.jp/document/9.0/html/functions-admin.html

この情報を取得することで、PostgreSQLの内部でどのような処理が行われているのかを知ることができます。

一方で、これらの内部の統計情報は時々刻々と変化していくため、データベースサーバの運用という観点で見る場合には、きちんと記録をしておく必要があります。

また、データベースの監視やパフォーマンスの分析にはさまざまな角度からこれらの内部の統計情報を分析する必要がありますが、いざ何か問題が起こって分析しようとすると、必要な情報が取れていない、といったことが起こりえます。

つまり、データベースの運用を行う上では、後に分析する際に必要になる可能性を考慮して、「最初から、定期的に、網羅的に」情報を収集しておくことが重要になります。

■パフォーマンス統計情報を取得・保存するPgPerfパッケージ


PostgreSQLでは、いままでにもいくつかパフォーマンス関連の統計情報を取得したりレポートするツールが存在していました。

しかし、「動作させるための設定が複雑である」、「ツールが複雑でメンテナンスが難しい」、「特定のレポートを出すのに特化している」といった課題がありました。

今回リリースした「PgPerfパッケージ」は、パフォーマンス関連の統計情報を
  • 内部のシステムビューなどのほぼ同じ形式で、
  • 稼働しているシステムに極力手を加えずに、
  • すべてを網羅的に取得・蓄積し、
  • 後から自由に分析できるようにする
という目的のために開発されたものです。

そのため、実装の特徴としては、
  • スクリプト(SQL、PL/pgSQL)のみで動作するため、PostgreSQLの稼働しているプラットフォームに依存しない。
  • 各種性能情報を容易に取得・保存することができ、蓄積したデータを自由に分析・活用することができる。
  • インストールおよびアンインストールが容易で、稼働しているPostgreSQLの設定を変更する必要がない。
といったことが挙げられます。

pgstattupleやpg_stat_statementsなどのcontribモジュールがインストールされていなくても、デフォルトで取得できる情報はすべて取得しますし、これらのcontribモジュールがインストールされていればそれらの情報も併せて取得します。

PgPerfパッケージの詳細なマニュアルを読みたい方はこちらをご覧ください。

PgPerfパッケージユーザーマニュアル
http://www.uptime.jp/go/pgperf-snapshot

■PgPerfパッケージのインストール


PgPerfパッケージのインストールは非常に簡単です。各メジャーバージョンに対応する pgperf_install.sql スクリプトを、contribモジュールと同じようにデータベースに対してインストールするだけです。

[snaga@devsv02 pgperf-snapshot]$ psql -f pgperf_install91.sql testdb
BEGIN
CREATE SCHEMA
psql:pgperf_install91.sql:28: NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "snapshot_pkey" for table "snapshot"
CREATE TABLE
CREATE FUNCTION
(...snip...)
CREATE FUNCTION
COMMIT
[snaga@devsv02 pgperf-snapshot]$
作成すると、データベース内にpgperfというスキーマが作成され、スナップショット取得用のテーブルやSQL関数が作成されたことが分かります。

[snaga@devsv02 pgperf-snapshot]$ psql testdb
psql (9.0.6, server 9.1.2)
Type "help" for help.

testdb=# SET search_path TO pgperf;
SET
testdb=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------------------------------+-------+----------
pgperf | snapshot | table | postgres
pgperf | snapshot_pg_current_xlog | table | postgres
pgperf | snapshot_pg_locks | table | postgres
pgperf | snapshot_pg_relation_size | table | postgres
pgperf | snapshot_pg_stat_activity | table | postgres
pgperf | snapshot_pg_stat_bgwriter | table | postgres
pgperf | snapshot_pg_stat_database | table | postgres
pgperf | snapshot_pg_stat_database_conflicts | table | postgres
pgperf | snapshot_pg_stat_replication | table | postgres
pgperf | snapshot_pg_stat_statements | table | postgres
pgperf | snapshot_pg_stat_user_indexes | table | postgres
pgperf | snapshot_pg_stat_user_tables | table | postgres
pgperf | snapshot_pg_statio_user_indexes | table | postgres
pgperf | snapshot_pg_statio_user_tables | table | postgres
pgperf | snapshot_pg_statistic | table | postgres
pgperf | snapshot_pgstatindex | table | postgres
pgperf | snapshot_pgstattuple | table | postgres
(17 rows)

testdb=#

■スナップショットの取得


それでは、インストールしたPgPerfパッケージを使って、統計情報のスナップショットを取得してみます。

スナップショットを取得するには、pgpgerf.create_snapshot()というSQL関数を使用します。

testdb=# SELECT pgperf.create_snapshot(1);
create_snapshot
-----------------
0
(1 row)

testdb=#
create_snapshot()関数には、スナップショット取得レベルを指定する必要があります。スナップショット取得レベルは、そのデータの種別やスナップショット取得にかかる負荷に応じて、もっとも基礎的な「1」から可能な限りの情報を取得する「5」までレベル分けがされています。

ここでは、まずはもっとも基本的なスナップショット取得レベル1で取得してみます。

■取得したスナップショットの確認


取得したスナップショットは、pgperfスキーマ内にある「snapshot_」で始まる複数のテーブルに格納されます。

pgperf.snapshotテーブルは、スナップショット取得時刻とスナップショットIDの対応を保存するテーブルです。このテーブルを参照すると、いつ取得したスナップショットなのか、スナップショットIDが何番なのか、といった情報を確認することができます。

testdb=# SELECT * FROM pgperf.snapshot;
sid | ts
-----+----------------------------
0 | 2012-12-08 11:17:02.822046
(1 row)

testdb=#
各種統計情報のデータそのものは、その他の「pgperf.snapshot_*」というテーブルに保存されます。

例えば、バックグラウンドライタの統計情報を取得できるpg_stat_bgwriterシステムビューのスナップショットはpgperf.snapshot_pg_stat_bgwriterテーブルに保存される、といった仕組みです。

testdb=# SELECT * FROM pgperf.snapshot_pg_stat_bgwriter ;
-[ RECORD 1 ]---------+------------------------------
sid | 0
checkpoints_timed | 136
checkpoints_req | 35
buffers_checkpoint | 1746118
buffers_clean | 0
maxwritten_clean | 0
buffers_backend | 1383900
buffers_backend_fsync | 0
buffers_alloc | 451660
stats_reset | 2012-11-30 13:29:50.000083+09

testdb=#
詳細については、ユーザマニュアルを参照してください。

PgPerfパッケージユーザーマニュアル
http://www.uptime.jp/go/pgperf-snapshot

■スナップショットの削除


スナップショットを削除するには、スナップショットIDを指定してpgperf.delete_snapshot()関数を使います。指定したスナップショットIDのスナップショットデータを、すべてのスナップショットテーブルから一括して削除してくれます。

testdb=# SELECT * FROM pgperf.snapshot;
sid | ts
-----+----------------------------
0 | 2012-12-08 11:17:02.822046
(1 row)

testdb=# SELECT pgperf.delete_snapshot(0);
delete_snapshot
-----------------
t
(1 row)

testdb=#

■スナップショットの消し込み


基本的にスナップショットのデータは蓄積していくものですが、ある一定の期間以上経過した古いものは削除をしたい、といった場合もあると思います。そのような場合には、pgperf.purge_snapshots()関数を使います。

pgperf.purge_snapshots()関数は、引数にINTERVAL型の値を取り、指定した期間より古いスナップショットを一括して削除します。

以下の例は、取得してから32日以上経過したスナップショットを削除している例です。

testdb=# SELECT pgperf.purge_snapshots('32 days');
purge_snapshots
-----------------
12
(1 row)

testdb=#

■PgPerfパッケージのアンインストール


PgPerfパッケージは、PL/pgSQLで作成されたSQL関数とテーブル類で構成されており、すべてpgperfスキーマ内に作成されます。

ですので、アンインストールする場合にはpgperfスキーマをCASCADE指定でDROPすることで、きれいにアンインストールすることができます。pgperf_uninstall.sqlスクリプトは、この処理を行ってくれます。

[snaga@devsv02 pgperf-snapshot]$ psql -U postgres -f pgperf_uninstall.sql testdb
psql:pgperf_uninstall.sql:1: NOTICE: drop cascades to 50 other objects
DETAIL: drop cascades to table pgperf.snapshot
drop cascades to function pgperf._get_server_version()
drop cascades to function pgperf._check_function(name)
(...snip...)
drop cascades to function pgperf.create_snapshot_pgstatindex(integer)
drop cascades to function pgperf.delete_snapshot_pgstatindex(integer)
DROP SCHEMA
[snaga@devsv02 pgperf-snapshot]$

■まとめ


今回は、PostgreSQLのパフォーマンス統計情報を取得・蓄積する方法として、PgPerfパッケージの紹介と、その基本的な使い方の解説をしました。

PgPerfパッケージは、インストールするのも使ってみるのも(もちろんアンインストールも)非常に簡単ですので、ぜひ試してみていただければと思います。

明日は、このPgPerfパッケージを使って蓄積したスナップショットデータを、SQLを使って分析する方法を御紹介します。

では、また。

ウィンドウ関数を使ってブロック読み込み量の推移を見る

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 9です。

前回は、PgPerfパッケージを使って、PostgreSQLの各種統計情報のスナップショットを取得・保存する方法を解説しました。

今回は、その保存したデータを分析する方法をご紹介します。

お題は「データベースのブロック読み込み発生の推移を分析する」です。

■使用するスナップショットテーブル


今回使用するスナップショットテーブルは、
  • pgperf.snapshot
  • pgperf.snapshot_pg_stat_database
の2つのテーブルです。前者には、スナップショットIDと取得日時が、後者にはデータベースごとのブロック読み込みの統計情報のスナップショットが保存されています。

SELECT * FROM pgperf.snapshot LIMIT 5;
SELECT * FROM pgperf.snapshot_pg_stat_database LIMIT 5;
これらのテーブルを分析することで、データベースのブロック読み込みの推移を確認してみます。

なお、これらのテーブルの構造の詳細についてはPgPerfパッケージのユーザーマニュアルを参照してください。

PgPerfパッケージユーザーマニュアル
http://www.uptime.jp/go/pgperf-snapshot/

■ブロック読み込みの推移をスナップショットから取得する


まず、スナップショットのテーブルからブロック読み込み(blks_read)の発生数を見てみます。

postgres=# SELECT sid,datname,blks_read FROM pgperf.snapshot_pg_stat_database;
sid | datname | blks_read
-----+---------------------+-----------
2 | template1 | 0
2 | template0 | 0
2 | postgres | 58122
2 | pgbench | 0
2 | snaga | 138
2 | testdb | 0
2 | testdb0 | 172543546
3 | template1 | 0
3 | template0 | 0
3 | postgres | 74454
3 | pgbench | 0
3 | snaga | 138
3 | testdb | 0
3 | testdb0 | 172543546
4 | template1 | 0
4 | template0 | 0
4 | postgres | 90749
4 | pgbench | 0
4 | snaga | 138
4 | testdb | 0
4 | testdb0 | 172543546
5 | template1 | 0
5 | template0 | 0
5 | postgres | 107030
(...snip...)
まず、データベースごとに保存されているブロック読み込み数の値を、すべて合算してインスタンス全体のブロック読み込み数に変換します。スナップショットIDでGROUP BYしてSUMでblks_readを合計します。

postgres=# SELECT sid,sum(blks_read) FROM pgperf.snapshot_pg_stat_database GROUP BY sid ORDER BY sid;
sid | sum
-----+-----------
2 | 537034870
3 | 537051202
4 | 537067497
5 | 537083778
6 | 537100021
(...snip...)

■ウィンドウ関数で差分を取得する


ここまで見てきてお分かりの通り、snapshot_pg_stat_databaseテーブルに保存されているブロック読み込みの数値は、過去の数値を積算した値です。つまり、各スナップショットの間隔で発生したブロック読み込み数は、これらの数値の差分ということになります。

例えば、ここで出ているスナップショットIDの2と3の間で発生したブロック読み込みの数は、

537051202 - 537034870 = 16332ブロック

となります。

差分を計算するためには「直前のスナップショットの値」を取得する必要があります。PostgreSQLではウィンドウ関数が使えますので、ウィンドウ関数である lag() を用いて「直前のスナップショットの値」を取得してみます。

ウィンドウ関数の詳細については、以下の記事を参照ください。

Window関数 - 導入編 - still deeper
http://www.chopl.in/blog/2012/12/01/window-function-tutorial/

Window関数 - Let's Postgres
http://lets.postgresql.jp/documents/technical/window_functions

ウィンドウ関数そのものの詳細については上記に譲るとして、ここでは前のレコードとの差分計算だけをやってみることにします。

SQLとしては以下のようなSQLになります。

SELECT sid,
sum(blks_read),
lag(sum(blks_read)) OVER (ORDER BY sid)
FROM pgperf.snapshot_pg_stat_database
GROUP BY sid
ORDER BY sid;
このSQLを実行すると、以下のように「直前のスナップショットの値」が「lag」というカラムに取り込まれます。

postgres=# SELECT sid, sum(blks_read), lag(sum(blks_read)) OVER (ORDER BY sid)
FROM pgperf.snapshot_pg_stat_database GROUP BY sid ORDER BY sid;
sid | sum | lag
------+-----------+-----------
2 | 537034870 |
3 | 537051202 | 537034870
4 | 537067497 | 537051202
5 | 537083778 | 537067497
6 | 537100021 | 537083778
7 | 537116221 | 537100021
(...snip...)
ひとつ前のスナップショットの数値(sum)が、その次のスナップショットのカラム(lag)として取り込まれていることが分かります。

あとは、この2つのカラムを使って差分を計算するだけです。

SELECT sid,
sum(blks_read) - lag(sum(blks_read)) OVER (ORDER BY sid) as "blks_read"
FROM pgperf.snapshot_pg_stat_database
GROUP BY sid
ORDER BY sid;
上記のSQLを実行すると、以下のような結果を取得できます。

postgres=# SELECT sid,
sum(blks_read) - lag(sum(blks_read)) OVER (ORDER BY sid) as "blks_read"
FROM pgperf.snapshot_pg_stat_database
GROUP BY sid ORDER BY sid;
sid | blks_read
------+-----------
2 |
3 | 16332
4 | 16295
5 | 16281
6 | 16243
7 | 16200
(...snip...)
これを見ると、スナップショットIDが2から3の間では16332ブロックの読み込みが発生し、その後も同じくらいの読み込みが発生していることが分かります。

■秒単位の数値に変換する


ここまでで、各スナップショット間で発生したブロック読み込みの数(の推移)を取得することができるようになりました。しかし、スナップショットの取得間隔は場合によって異なります。

負荷試験や検証などの場合には1分単位で取得しているかもしれませんし、運用中には1時間間隔かもしれません。そのため、最後に秒単位の数値に変換します。「ブロック読み込み/秒」ということです。

PgPerfパッケージでは、スナップショット間の「秒数」を取得するSQL関数 pgperf.get_interval() が提供されています。この「秒数」で先ほどのブロック読み込み数を除算することで、「ブロック読み込み/秒」を取得することができます。

以下の例は、スナップショットIDの2と3の間隔を秒数で取得している例です。

postgres=# SELECT pgperf.get_interval(2, 3);
get_interval
--------------
600
(1 row)

postgres=#
先ほどのウィンドウ関数を使えばスナップショットIDについても「直前の値」を取得することができますので、pgperf.get_interval()関数と併せて使うことによって秒単位の値に変換します。

また、スナップショットIDだけでは時刻が分かりませんので、pgperf.snapshotテーブルのsidを使ってJOINして、スナップショットの取得時刻(ts)を取得します。

上記の追加を行ったクエリが以下のものになります。

SELECT s.ts,
( sum(blks_read) - lag(sum(blks_read)) OVER (ORDER BY s.sid) ) /
pgperf.get_interval(lag(s.sid) OVER (ORDER BY s.sid), s.sid) as "blks_read/sec"
FROM pgperf.snapshot_pg_stat_database d, pgperf.snapshot s
WHERE d.sid = s.sid
GROUP BY s.sid
ORDER BY s.ts;
このクエリを実行すると、以下のような結果が得られます。

postgres=# SELECT s.ts,
( sum(blks_read) - lag(sum(blks_read)) OVER (ORDER BY s.sid) ) /
pgperf.get_interval(lag(s.sid) OVER (ORDER BY s.sid), s.sid) as "blks_read/sec"
FROM pgperf.snapshot_pg_stat_database d, pgperf.snapshot s
WHERE d.sid = s.sid
GROUP BY s.sid
ORDER BY s.ts;
ts | blks_read/sec
----------------------------+-----------------------
2012-11-21 18:20:01.238885 |
2012-11-21 18:30:01.464424 | 27.2200000000000000
2012-11-21 18:40:01.685642 | 27.1583333333333333
2012-11-21 19:21:07.584854 | 6.6021897810218978
2012-11-21 19:30:01.215188 | 30.4176029962546816
2012-11-21 19:40:01.442753 | 27.0000000000000000
2012-11-21 19:50:01.665997 | 26.9500000000000000
(...snip...)
このクエリによって、例えば '2012-11-21 18:30:01' にスナップショットを取得した際のブロック読み込みは「約27.22ブロック/秒」であったことが分かります。

■まとめ


今回は、PgPerfパッケージを使って取得したスナップショットのデータを時系列分析する方法を解説しました。

ウィンドウ関数を使うことによって、直前の値との差分を比較的簡単に取得できることを示し、pgperf.get_interval()関数で取得した秒数で除算することによって、簡単に秒単位の数値に変換できることを示しました。

今回は、もっとも簡単な(と思われる)ブロック読み込みについて解説しましたが、前回も紹介した通り、PgPerfパッケージを使うと、さまざまな統計情報が収集・蓄積されます。これらを分析することで、パフォーマンスについて、より深い知見を得られると思います。

ぜひ、PgPerfパッケージで蓄積したデータを分析して、PostgreSQLの安定運用やパフォーマンス管理に役立てていただければと思います。

では、また。

Rを使ってパフォーマンス統計情報を可視化する

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 10です。

前回までに、PostgreSQLのパフォーマンスデータを取得・蓄積し、それをSQLを用いて解析する方法を紹介してきました。

しかし、増えていくデータを活用して人間が何らかのアクションを取るためには、データを何らかの形で「可視化」して、人間が容易に読解・解釈できる形に変更する必要があります。

今回は解析結果を可視化する方法について簡単に紹介します。ツールとしては、オープンソースの統計処理ソフトウェアである「R」を利用します。

■R、および関連パッケージのインストール


まず、RのサイトからRのバイナリパッケージをダウンロードします。

The R Project for Statistical Computing
http://www.r-project.org/

トップページの「Getting Started」の中の「download R」に進むとミラーの一覧が出てきますので、適当なミラーサイトを選んだら「Download R for Windows」というページから「base」を選んでバイナリパッケージをダウンロードします(Windows版のRの最新版はバージョン2.15.2のようです)。

次に、DBIおよびRPostgreSQLのパッケージをインストールします。Rのパッケージのインストールは「CRAN」という、Perlで言うところのCPANのようなサイトからネットワーク経由で直接ダウンロードしてインストールすることができます。

Rを起動してコンソールを開いたら、以下のコマンドを実行します。

> install.packages("DBI")
> install.packages("RPostgreSQL")
ミラーの一覧が出て、適当なミラーサイトを選択するとインストールが始まります。

■ブロック書き込み量を取得するビューを作る


Rの準備ができたら、次はPostgreSQL側の準備をします。

前回のエントリ「ウィンドウ関数を用いたブロック読み込みの計測」を応用して、今回は「ブロック書き込み」について、バックグラウンドライタ、チェックポイント、バックエンドプロセスによる書き込みをそれぞれ時系列で取得し、「書き込みブロック数/秒」として取得します。

まず、ウィンドウ関数を使って各スナップショット取得時の書き出しブロック数のデータを取得するためのクエリを作成し、それをビューとして作成します。

CREATE VIEW pgperf.rpt_blkwrite AS
SELECT date_trunc('second', s.ts) AS "timestamp",
round( (( buffers_checkpoint - lag(buffers_checkpoint) OVER (ORDER BY s.ts) )::float /
pgperf.get_interval(lag(s.sid) OVER (ORDER BY s.ts), s.sid))::numeric, 2) AS "blks_cp/sec",
round( (( buffers_clean - lag(buffers_clean) OVER (ORDER BY s.ts) )::float /
pgperf.get_interval(lag(s.sid) OVER (ORDER BY s.ts), s.sid))::numeric, 2) AS "blks_bg/sec",
round( (( buffers_backend - lag(buffers_backend) OVER (ORDER BY s.ts) )::float /
pgperf.get_interval(lag(s.sid) OVER (ORDER BY s.ts), s.sid))::numeric, 2) AS "blks_be/sec"
FROM pgperf.snapshot s,
pgperf.snapshot_pg_stat_bgwriter b
WHERE s.sid=b.sid
AND s.ts BETWEEN date_trunc('day', now()) - '32 days'::interval
AND date_trunc('day', now()) + '1 day'::interval
ORDER BY s.ts;
ここで作成したpgperf.rpt_blkwriteビューはブロック書き込みの数値を過去32日分時系列表示してくれるもので、このビューに対してSELECTを実行すると以下のような結果を取得できます。

postgres=# SELECT * FROM pgperf.rpt_blkwrite;
timestamp | blks_cp/sec | blks_bg/sec | blks_be/sec
---------------------+-------------+-------------+-------------
2012-12-09 15:45:48 | | |
2012-12-09 15:50:01 | 0.00 | 0.00 | 0.05
2012-12-09 16:00:01 | 0.00 | 0.00 | 0.00
2012-12-09 16:10:01 | 0.00 | 0.00 | 0.01
2012-12-09 16:20:01 | 0.00 | 0.00 | 0.00
(5 rows)

postgres=#
ここまで準備ができたら、次はRからこのデータを取得して時系列でグラフ表示してみます。

■データベースへ接続してクエリを実行する


RからPostgreSQLへのクエリを実行するには、まず library() 関数を使ってRPostgreSQLパッケージを読み込みます。

> library("RPostgreSQL")
要求されたパッケージ DBI をロード中です
>
次に、データベースへのコネクションを作成します。

> con <- dbConnect(PostgreSQL(), host="10.0.2.12", user= "postgres", password="postgres", dbname="postgres")
> con
<PostgreSQLConnection:(9088,3)>
>
これで con オブジェクトにデータベースへのコネクションオブジェクトが作成されました。

データベースへ接続できたら、クエリを発行して結果セットを取得します。ここでは、先ほど作成したpgperf.rpt_blkwriteビューに対してSELECTを実行します。

> rs <- dbSendQuery(con, "SELECT * FROM pgperf.rpt_blkwrite")
特にエラー無くクエリを実行できたら(プロンプトが返ってきたら)、クエリの発行は成功です。

■結果を取得して時系列のグラフを描画する


クエリを実行したら結果セットからデータをフェッチします。

> out <- fetch(rs)
> out
timestamp blks_cp/sec blks_bg/sec blks_be/sec
1 2012-12-09 15:50:01 NA NA NA
2 2012-12-09 16:00:01 0.00 0 0.00
3 2012-12-09 16:10:01 0.00 0 0.01
4 2012-12-09 16:20:01 0.00 0 0.00
5 2012-12-09 16:30:01 0.00 0 0.42
6 2012-12-09 16:40:01 51.36 0 0.00
(...snip...)
>
配列を表示すると、きちんとデータを取得できていることが分かります。

最後に、取り出したデータ out を使って時系列のグラフを描画します。

> ts.plot (out[2:4], gpars=list( ylab='blocks/sec', lty=c(1,1,1), col=c('green', 'red', 'blue')))
上記のts.plotを実行すると、以下のような時系列のグラフが表示されます。


ここでは、緑のラインがチェックポイントによる書き込み、赤のラインがバックグラウンドライタによる書き込みで、青のラインがバックエンドプロセスによる書き込みです。

最後に凡例を表示して完了です。

> legend('topright', c('Checkpoint','Bgwriter','Backend'), lty=c(1,1,1), col=c('green', 'red', 'blue'))
本来はX軸には時刻をプロットすべきなのですが、うまく表示する方法を見つけられず今回は間に合いませんでした。(どなたかご存じの方がいたら教えてください)。

■まとめ


今回は、前回までに取得したパフォーマンス統計情報を、統計処理ソフトウェアであるRを使って可視化する方法を紹介しました。

パフォーマンスに関するデータを取得・蓄積することももちろん重要なのですが、そのデータをきちんと読み解いてそこから「解釈」することもデータを活用する上では欠かせません。「可視化」というのは、そのための非常に重要な方法のひとつです。

Rだけでなく、データを可視化するツールにはさまざまなものがあります。ぜひ、自分に合ったツールを見つけて、取得・蓄積したパフォーマンスデータを有効に活用していただければと思います。

では、また。

■参考文献


Package 'RPostgreSQL'
http://cran.r-project.org/web/packages/RPostgreSQL/RPostgreSQL.pdf
Rと時系列(1)
http://mjin.doshisha.ac.jp/R/33/33.html
Plotting Time-Series Objects
http://stat.ethz.ch/R-manual/R-devel/library/stats/html/plot.ts.html

GrowthForecastでパフォーマンス情報を可視化する

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 11です。

最近、ちょっとした可視化ブームが訪れております(個人的に)。

運用管理を可視化するツールやプラグインもいろいろと公開されている昨今ですが、個人的にはちょっと前から「GrowthForecast」というツールが気になっていましたので、今回はこれを使ってPostgreSQLを可視化してみようと思います。

■「GrowthForecast」とは何か


「GrowthForecast」は @kazeburo氏の開発したグラフツールです。単独でWebサーバとして動作し、WebAPIを経由して可視化すべきデータの登録を受け付け、HTTPリクエストでグラフ化された画像データを取得することができるソフトウェアです。

GrowthForecast - Lightning fast Graphing / Visualization
http://kazeburo.github.com/GrowthForecast/

一般的な運用監視ツールは、長期的に運用監視する場合には便利でいいのですが、セットアップに手間がかかるし使いこなすのもいろいろ大変だということで、その辺りを簡単に使えるようにしたい、というツールです。

というわけで、今回はこのGrowthForecastを使ってPostgreSQLの統計情報を可視化してみます。

■GrowthForecastのインストール


GrowthForecastのインストールは、cpanmというツールを使ってインストールを行います。

cpanmのインストール方法については以下を参照ください。

cpanmを使ってPerlモジュールを入れる - メールの話題 | bounceHammer
http://bouncehammer.jp/ja/email-topics/2011/05/install-perl-modules-with-cpanm.html

CPANの依存するモジュールをモリモリとインストールするので、ネットにつながっていない環境でインストールするのは難しいと思いますので、その点はご注意ください。

cpanmの準備ができたら、GrowthForecastのインストールを行います。

本来はcpanmコマンドで直接URLを指定してインストールできるようですが、私の環境ではできなかったため、一旦、wgetでGrowthForecastのtar ballをダウンロードしてインストールを行います。

[snaga@devsv02 gf]$ wget --no-check-certificate https://github.com/downloads/kazeburo/GrowthForecast/GrowthForecast-0.32.tar.gz
--2012-11-26 23:41:44-- https://github.com/downloads/kazeburo/GrowthForecast/GrowthForecast-0.32.tar.gz
Resolving github.com... 207.97.227.239
(...snip...)
Saving to: `GrowthForecast-0.32.tar.gz'

100%[=============================================>] 91,375 84.5K/s in 1.1s

2012-11-26 23:41:47 (84.5 KB/s) - `GrowthForecast-0.32.tar.gz' saved [91375/91375]

[snaga@devsv02 gf]$ ls
GrowthForecast-0.32.tar.gz
[snaga@devsv02 gf]$ su
Password:
[root@devsv02 gf]# cpanm -n GrowthForecast-0.32.tar.gz
Building GrowthForecast-0.32 ... OK
Successfully installed GrowthForecast-0.32
111 distributions installed
[root@devsv02 gf]#

■GrowthForecastサーバの起動


インストールが終わったら、GrowthForecastのサーバプロセスを起動します。

[snaga@devsv02 ~]$ /usr/bin/growthforecast.pl --data-dir /tmp/gfdata
23:15:47 1.1 | 2012-11-27T23:15:47 [INFO] [short] first updater start in Tue Nov 27 23:16:00 2012 at /usr/lib/perl5/site_perl/5.8.8/GrowthForecast/Worker.pm line 55
23:15:47 2.1 | 2012-11-27T23:15:47 [INFO] [update] first updater start in Tue Nov 27 23:20:00 2012 at /usr/lib/perl5/site_perl/5.8.8/GrowthForecast/Worker.pm line 55
この時、GrowthForecastのサーバはデフォルトのポート番号5125で待ち受けていますので、ブラウザで接続できるかどうかを確認します。


空のGrowthForecastのページが表示されればGrowthForecastサーバの起動は成功です。

■GrowthForecastへのデータの登録


GrowthForecastへのデータの登録は、GrowthForecastのWebAPIを経由して行います。

以下のようなURLの書式になります。

http://ホスト名:ポート番号/api/サービス名/セクション名/グラフ名
例えば、IPアドレス 10.0.2.12 のGrowthForecastサーバに対して、PostgreSQLの接続セッション数をグラフにするには、以下のようなURLになるでしょう。

http://10.0.2.12:5125/api/postgres/database/session
このURLに対してPOSTメソッドでフォームデータを送信することで、GrowthForecastへのデータ登録を行うことができます。

任意のURLにPOSTメソッドでフォームデータを送信できるツールはいろいろありますが、ここではcURLを使います。

まず、psqlを使ってブロック読み込み数をpg_stat_databaseシステムビューから取得します。そして、その結果を "number" という名前のフォーム変数に設定してPOSTメソッドで送信します。

[snaga@devsv02 gf]$ psql -c 'select count(*) from pg_stat_activity' postgres count
-------
1
(1 row)

[snaga@devsv02 gf]$ psql -A -t -c 'select count(*) from pg_stat_activity' postgres
1
[snaga@devsv02 gf]$ curl -F number=`psql -A -t -c 'select count(*) from pg_stat_activity' postgres` http://10.0.2.12:5125/api/postgres/database/session
{"error":0,"data":{"number":1,"llimit":-1000000000,"mode":"gauge","stype":"AREA","adjustval":"1","meta":"","service_name":"postgres","gmode":"gauge","color":"#3399cc","created_at":"2012/11/29 19:00:14","section_name":"database","ulimit":1000000000,"id":2,"graph_name":"session","description":"","sulimit":100000,"unit":"","sort":0,"updated_at":"2012/11/29 19:00:14","adjust":"*","type":"AREA","sllimit":-100000,"md5":"c81e728d9d4c2f636f067f89cc14862c"}}
[snaga@devsv02 gf]$
このようにデータを登録すると、Webブラウザからグラフを見ることができるようになります。


このような「情報の取得→フォーム経由の送信」を定期的に行うことで、GrowthForecastでは時系列のグラフ生成を可能にします。

■バッファの書き出しをグラフ化する


セッション数のような基本的な数値をグラフ化できたら、次は応用編として共有バッファのブロック書き出しをグラフ化してみます。

バッファのブロックの書き出しは、以下のSQLで取得できますので、これで取得できる数値を用います。

SELECT buffers_checkpoint FROM pg_stat_bgwriter;
SELECT buffers_clean FROM pg_stat_bgwriter;
SELECT buffers_backend FROM pg_stat_bgwriter;
buffers_checkpointはチェックポイント処理で書き出されたブロック数、bufers_cleanはバックグラウンド処理で書き出したブロック数、buffers_backendはバックエンドがバッファの入れ替えに際して書き出したブロック数になります。

これらはいずれも過去の数値に積算されていくもので、かつ「ブロック書き出し」という同じカテゴリの数値になりますので、ブロック書き出しのグラフは
  • 差分を計算する
  • 複数のグラフを積み上げる
という形式にしてみます。

まず、先ほどのセッション数と同じように、GrowthForecastサーバにデータを登録して三種類のグラフが表示されるところまでを行います。

curl -F number=`psql -A -t \
-c 'SELECT buffers_checkpoint FROM pg_stat_bgwriter' postgres` \
http://10.0.2.12:5125/api/postgres/database/buffers_checkpoint

curl -F number=`psql -A -t \
-c 'SELECT buffers_clean FROM pg_stat_bgwriter' postgres` \
http://10.0.2.12:5125/api/postgres/database/buffers_clean

curl -F number=`psql -A -t \
-c 'SELECT buffers_backend FROM pg_stat_bgwriter' postgres` \
http://10.0.2.12:5125/api/postgres/database/buffers_backend
グラフは表示されるようになりましたが、このままだと「過去のブロック書き出し数の総数」が表示されてしまうので、差分を表示するように変更しなければなりません。

まず、各グラフの「設定」のページを開いて、「グラフのタイプ」を「実績」から「差分」に変更します。


3つのグラフを差分表示に変更したら、最後にこれら3つのグラフをひとつの複合グラフにまとめます。


まず、「複合グラフの追加」を開きます。

最初に作成する複合グラフの「パス」を指定します。ここでは「buffers_written」とします。

次にグラフの系列を追加していきます。今回は、系列1にbuffers_clean、系列2にbuffers_checkpoint、系列3にbuffers_backendを追加します。それぞれ、「モード:差分」、「スタック:する」として登録します。

最後に「追加」として登録すると、複合グラフが新しいグラフとして登録されます。

■まとめ


今回は「PostgreSQLの動作を可視化」シリーズとして、GrowthForecastを使った可視化を試してみました。

GrowthForecast自体は非常に簡単で使いやすいツールでしたので、PostgreSQLを使っている方もお手軽に可視化を試せるのではないかと思います。興味を持った方は、これを機会にぜひ試してみてください。

では、また。

HinemosでPostgreSQLの性能を監視する

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 12です。

「最近(個人的に)可視化ブームが訪れている」という話を前回のエントリで書きました。

パフォーマンスに関連するデータを可視化してくれるツールにはさまざまな種類があり、いろいろと目移りしてしまうのですが、今回はオープンソースの運用管理ツール「Hinemos」でPostgreSQLのパフォーマンス情報の可視化を行う方法を紹介します。

■HinemosとPostgreSQL性能監視


Hinemosはオープンソースの統合運用管理ソフトウェアです。システム監視機能やジョブ管理機能の機能等を備えており、システムの運用管理をサポートしてくれるソフトウェアになります。

Hinemos:コンピュータ、システム、ネットワークの運用管理を実現するオープンソースソフトウェア(OSS)
http://www.hinemos.info/

先日、この「Hinemosへのアドオン」という形で、PostgreSQLの性能情報を取得・蓄積・可視化・監視することができるツールを作成しました。

Hinemos PostgreSQL性能監視オプション
http://www.uptime.jp/ja/products-services/hinemos-postgres-addon/

このツールは、簡単に言うと「少し大きめのスクリプト」なのですが、オープンソースで公開されており、このスクリプトとHinemosを組み合わせることによって、比較的簡単にPostgreSQLの性能情報を可視化することができます。

動作の前提条件としては、
  • Hinemosのバージョンが4.0以降であること
  • PostgreSQLサーバでHinemosエージェントが動作していること
  • PostgreSQLサーバがUnix系サーバであり、Perlが動作すること
くらいです。

■インストール


Hinemosそのもの(マネージャやエージェント、クライアント)のインストールから説明すると非常に長くなってしまいますので、ここではHinemos自体のインストール方法は割愛します。Hinemosそのもののインストールについては、公式のマニュアル等を参照してください。

Hinemos:コンピュータ、システム、ネットワークの運用管理を実現するオープンソースソフトウェア(OSS)
http://www.hinemos.info/

本エントリでは、Hinemosマネージャ、Hinemosエージェント、Hinemosクライアントのインストールが完了し、動作してることを前提として解説を進めます。

PostgreSQL監視オプションのソースコードはGitHubで配布されていますので、まずはそちらからダウンロードします。

uptimejp/hinemos-postgres-addon
https://github.com/uptimejp/hinemos-postgres-addon

今回は11月14日のリリース版である hinemos-postgres-addon-r20121114.zip を使います。

[snaga@devsv02 hinemos]$ wget --no-check-certificate https://github.com/downloads/uptimejp/hinemos-postgres-addon/hinemos-postgres-addon-r20121114.zip
--2012-12-04 01:44:39-- https://github.com/downloads/uptimejp/hinemos-postgres-addon/hinemos-postgres-addon-r20121114.zip
(...snip...)
Length: 13016 (13K) [application/zip]
Saving to: `hinemos-postgres-addon-r20121114.zip'

100%[======================================>] 13,016 30.1K/s in 0.4s

2012-12-04 01:44:49 (30.1 KB/s) - `hinemos-postgres-addon-r20121114.zip' saved [13016/13016]

[snaga@devsv02 hinemos]$
ZIPファイルをダウンロードしたら、ZIPを解凍し、make installを実行して mon_pgsql スクリプトをインストールします。スクリプトは /opt/hinemos_agent/bin にインストールされます。

[snaga@devsv02 hinemos]$ unzip hinemos-postgres-addon-r20121114.zip
Archive: hinemos-postgres-addon-r20121114.zip
creating: hinemos-postgres-addon-r20121114/
inflating: hinemos-postgres-addon-r20121114/test_mon_pgsql.sh
inflating: hinemos-postgres-addon-r20121114/LICENSE
inflating: hinemos-postgres-addon-r20121114/Makefile
inflating: hinemos-postgres-addon-r20121114/mon_pgsql
[snaga@devsv02 hinemos]$ cd hinemos-postgres-addon-r20121114
[snaga@devsv02 hinemos-postgres-addon-r20121114]$ ls -l
total 48
-rw-rw-r-- 1 snaga snaga 18092 Nov 14 13:12 LICENSE
-rw-rw-r-- 1 snaga snaga 682 Nov 14 13:12 Makefile
-rwxrwxr-x 1 snaga snaga 19695 Nov 14 13:12 mon_pgsql*
-rwxrwxr-x 1 snaga snaga 441 Nov 14 13:12 test_mon_pgsql.sh*
[snaga@devsv02 hinemos-postgres-addon-r20121114]$ su
Password:
[root@devsv02 hinemos-postgres-addon-r20121114]# make install
install -m 755 mon_pgsql /opt/hinemos_agent/bin
[root@devsv02 hinemos-postgres-addon-r20121114]#
インストールが完了したら、mon_pgsqlスクリプトを実行してみます。

mon_pgsqlスクリプトはHinemosエージェントから呼び出される外部コマンドになっています。そのため、コマンドラインで実行して動作確認することが可能です。

[postgres@devsv02 ~]$ /opt/hinemos_agent/bin/mon_pgsql

Usage: /opt/hinemos_agent/bin/mon_pgsql [<option>...] <search_key>

Options:
-h <host> Host name to connect the database.
-p <port> Port number to connect the database.
-U <user> User name used to connect the database.
-d <dbname> Database name to be connected.
-v Verbose output.

Search keys:
session
cache_hit
tuple_wrtn
tuple_read
blks_read
blks_wrtn
txn
xlog_wrtn
dbsize
locks

[postgres@devsv02 ~]$
上記のように、mon_pgsqlスクリプトはPostgreSQLデータベースへの接続用のオプションを受付、かつ「どのようなデータを取得するか」というキー(search key)をコマンドラインオプションとして受け付けます。

受け付けるキーは以下の通りです。
  • session:セッション数
  • cache_hit:キャッシュヒット率
  • tuple_wrtn:タプル更新(INSERT/UPDATE/DELETE)
  • tuple_read:タプル読み込み(RETURN/FETCH)
  • blks_read:ブロック読み込み
  • blks_wrtn:ブロック書き込み
  • txn:トランザクション数
  • xlog_wrtn:WAL書き込み量
  • dbsize:オブジェクトサイズ
  • locks:ロック
例えば、現在のセッション数を取得したい場合には、キーに「session」を指定して実行すると、以下のように、アクティブ、アイドル、ロック待ちのそれぞれのセッション数を取得することができます。

[postgres@devsv02 ~]$ /opt/hinemos_agent/bin/mon_pgsql -h localhost -p 5432 -U postgres -d postgres session
active,1
idle,1
wait,0
[postgres@devsv02 ~]$
ここまで動作確認できたら、いよいよHinemosへの登録と、Hinemos上でのデータの可視化に進みます。

■Hinemosでのリポジトリへのノード登録


まず、「リポジトリ[ノード]」のビューを開き、「リポジトリ[ノードの作成・変更]」ダイアログからサーバそのものをリポジトリに登録します。この手順は、PostgreSQLサーバに限らず、Hinemosで監視するサーバに共通のステップです。

■Hinemosでのカスタム監視の登録


次に、「性能[一覧]」ビューを開き、監視の追加を行います。

「監視種別」としては「カスタム監視(数値)」を選択します。



そして、「カスタム監視[作成・変更]」のダイアログでPostgreSQL監視用の設定を行います。基本的な設定項目と設定内容は以下の通りです。
  • 監視項目ID:監視の設定を識別する任意のIDを指定します。
  • スコープ:監視対象(スコープ)を指定します。
  • 間隔:監視の間隔を指定します。
  • 実効ユーザ:mon_pgsqlスクリプトを実行するOSユーザを指定します。
  • コマンド:mon_psgqlスクリプトを実行するコマンドラインを指定します。
  • 監視/判定(情報、警告、危険):監視で設定する閾値を入力します。
  • 監視/アプリケーション:アプリケーション名を記入します。
  • 収集/収集値表示名:収集・蓄積するデータの名称を記入します。
  • 収集/収集値単位:収集・蓄積するデータの単位を記入します。
以下はPostgreSQLのセッション数を監視する設定をしている例です。



監視の設定を行うと、「監視設定[一覧]」のビューに設定した監視項目の一覧が表示されます。

■Hinemosでの性能情報の表示


監視設定を行うと、「性能[一覧]」のビューに性能情報を収集する項目の一覧が表示されます。



ここで右上のアイコンから「グラフの追加」を選択すると、「性能」のビューが作成され、グラフの表示が出てきますので、「表示種別」で「デバイス別表示」を選択すれば、グラフの表示は完了です。

mon_pgsqlコマンドで複数種別の項目が出力される場合(セッションであればactive/idle/waitなど)には「デバイス別表示」を選択する必要があります。

「グラフ種別」については、「折れ線グラフ」か「積み上げ面グラフ」かは自由に選べます。

■まとめ


今回は、PostgreSQLの性能情報の収集・可視化・監視ということで、HinemosによるPostgreSQLのモニタリングをご紹介しました。

データベースは、その性質上、性能や可用性についてシビアに見られることが多いと思います。既存のソリューションとうまく組み合わせながら、うまく運用管理をしていただければと思います。

では、また。

AWSでそこそこセキュアにPostgreSQLインスタンスを立ち上げる

$
0
0
(4/5追記)アクセス管理関連の脆弱性が発見されています。
バージョン9.2.3、9.1.8、9.0.12、8.4.16およびそれより前のバージョンをお使いの場合はアップグレードしてください。特にクラウド環境で利用する際にはご注意ください。



PostgreSQL Advent Calendar 2012(全部俺)のDay 13です。

クラウドが大流行中です。(個人的に)

いろいろ試したり、ちょっとデータを突っ込んで分析したり、といった用途に使う場合には、クラウド上に自分専用のPostgreSQLが動いていると何かと便利です。

PaaSサービスを使うのも良いのですが、IaaS上に自分でPostgreSQLをインストールした方が自由度は高くなりますので、今回はAmazon EC2上にPostgreSQLをインストールして、自分専用のサーバとして使う設定をしてみます。

なお、インターネット上をデータが流れる関係上、多少、セキュリティに配慮した設定を行おうと思います。(とは言っても、ポート番号を変えてSSL接続を行うだけですが・・)

前提として、インターネット上のサーバの任意のポートに対してTCP接続を張れることが必要です。ファイヤーウォールなどでネットワーク接続が制限されている場合にはこの方法は使えません。

■Amazon EC2でインスタンスを立ち上げる


まず、Amazon EC2でインスタンスを立ち上げます。

EC2のセットアップから説明していると非常に長くなるため詳細は省きますが、今回は以下の設定で立ち上げます。

リージョンは「APAC -Tokyo」として、「Amazon Linux AMI 2012.09」を起動します。アーキテクチャはとりあえず32ビットとします。
  • Instance Type : T1 Micro
  • Availability Zone : No Preference
  • Key Pair : Create a new Key Pair (または既存のものがあればそれを選ぶ)
セキュリティグループでは、SSHの接続と、PostgreSQLの接続のみを受け付ける設定を行います。PostgreSQLのポート番号は、デフォルトの「5432」ではなく、ここでは「16543」を使います。
  • Security Group : Create a new Security Group
  • Group Name : test-pgsql
  • Group Description : test-pgsql
    • Create a new rule : SSH
    • Source : 0.0.0.0/0 (または制限できるのであれば指定する)
    • Create a new rule : Custom TCP rule
    • Port range : 16543
    • Source : 0.0.0.0/0 (または制限できるのであれば指定する)
インスタンスを起動したら、ec2-userでログインします。

login as: ec2-user
Authenticating with public key "imported-openssh-key"

__| __|_ )
_| ( / Amazon Linux AMI
___|\___|___|

https://aws.amazon.com/amazon-linux-ami/2012.09-release-notes/
There are 4 security update(s) out of 33 total update(s) available
Run "sudo yum update" to apply all updates.
[ec2-user@ip-10-152-167-242 ~]$
ログインしたら、まず yum update を行います。

[ec2-user@ip-10-152-167-242 ~]$ sudo su
[root@ip-10-152-167-242 ec2-user]# yum update
Loaded plugins: priorities, security, update-motd, upgrade-helper
Setting up Update Process
Resolving Dependencies
(...snip...)
ruby-libs.i686 0:1.8.7.371-1.20.amzn1 tzdata.noarch 0:2012f-1.15.amzn1
tzdata-java.noarch 0:2012f-1.15.amzn1 yum.noarch 0:3.2.29-30.24.amzn1

Complete!
[root@ip-10-152-167-242 ec2-user]#
次に、PostgreSQLをインストールする際に依存するパッケージをインストールします。今回は、libxsltとuuidが必要になりますので、これらをyumでインストールします。

[root@ip-10-152-167-242 ~]# yum install libxslt.i686 uuid.i686
Loaded plugins: priorities, security, update-motd, upgrade-helper
Setting up Install Process
Resolving Dependencies
--> Running transaction check
---> Package libxslt.i686 0:1.1.26-2.7.amzn1 will be installed
---> Package uuid.i686 0:1.6.2-11.16.amzn1 will be installed
(...snip...)
Installed:
libxslt.i686 0:1.1.26-2.7.amzn1
uuid.i686 0:1.6.2-11.16.amzn1

Complete!
[root@ip-10-152-167-242 ~]#

■PostgreSQLをインストールする


次にPostgreSQLのRPMをインストールします。

Amazon Linux AMIはRed Hat Enterprise Linux 6系のようですので、RHEL6用のPostgreSQL 9.2のRPMパッケージをダウンロードします。

ダウンロード用のスクリプト dl.sh をGistに置きましたので、これを実行してPostgreSQLのRPMをダウンロードします。

https://gist.github.com/4273726

[root@ip-10-152-167-242 ~]# sh dl.sh
--2012-11-24 09:15:20-- http://yum.postgresql.org/9.2/redhat/rhel-6.3-i386/postgresql92-9.2.1-1PGDG.rhel6.i686.rpm
Resolving yum.postgresql.org... 98.129.198.114
Connecting to yum.postgresql.org|98.129.198.114|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: 959964 (937K) [application/x-redhat-package-manager]
Saving to: “postgresql92-9.2.1-1PGDG.rhel6.i686.rpm”

100%[======================================>] 959,964 634K/s in 1.5s

2012-11-24 09:15:22 (634 KB/s) - “postgresql92-9.2.1-1PGDG.rhel6.i686.rpm” saved [959964/959964]

(...snip...)

2012-11-24 09:15:48 (702 KB/s) - “postgresql92-test-9.2.1-1PGDG.rhel6.i686.rpm” saved [1275524/1275524]

[root@ip-10-152-167-242 ~]# ls *.rpm
postgresql92-9.2.1-1PGDG.rhel6.i686.rpm
postgresql92-contrib-9.2.1-1PGDG.rhel6.i686.rpm
postgresql92-debuginfo-9.2.1-1PGDG.rhel6.i686.rpm
postgresql92-devel-9.2.1-1PGDG.rhel6.i686.rpm
postgresql92-docs-9.2.1-1PGDG.rhel6.i686.rpm
postgresql92-libs-9.2.1-1PGDG.rhel6.i686.rpm
postgresql92-plperl-9.2.1-1PGDG.rhel6.i686.rpm
postgresql92-plpython-9.2.1-1PGDG.rhel6.i686.rpm
postgresql92-pltcl-9.2.1-1PGDG.rhel6.i686.rpm
postgresql92-server-9.2.1-1PGDG.rhel6.i686.rpm
postgresql92-test-9.2.1-1PGDG.rhel6.i686.rpm
[root@ip-10-152-167-242 ~]#
このうち、以下の5つのRPMをインストールします。
  • postgresql92-9.2.1-1PGDG.rhel6.i686.rpm
  • postgresql92-contrib-9.2.1-1PGDG.rhel6.i686.rpm
  • postgresql92-devel-9.2.1-1PGDG.rhel6.i686.rpm
  • postgresql92-libs-9.2.1-1PGDG.rhel6.i686.rpm
  • postgresql92-server-9.2.1-1PGDG.rhel6.i686.rpm

[root@ip-10-152-167-242 ~]# rpm -ivh postgresql92-9.2.1-1PGDG.rhel6.i686.rpm postgresql92-contrib-9.2.1-1PGDG.rhel6.i686.rpm postgresql92-devel-9.2.1-1PGDG.rhel6.i686.rpm postgresql92-libs-9.2.1-1PGDG.rhel6.i686.rpm postgresql92-server-9.2.1-1PGDG.rhel6.i686.rpm
warning: postgresql92-9.2.1-1PGDG.rhel6.i686.rpm: Header V4 DSA/SHA1 Signature, key ID 442df0f8: NOKEY
Preparing... ########################################### [100%]
1:postgresql92-libs ########################################### [ 20%]
2:postgresql92 ########################################### [ 40%]
3:postgresql92-contrib ########################################### [ 60%]
4:postgresql92-devel ########################################### [ 80%]
5:postgresql92-server ########################################### [100%]
[root@ip-10-152-167-242 ~]#

■データベースクラスタを初期化する


PostgreSQLのインストールが終わったら、データベースクラスタを初期化します。おなじみのinitdbコマンドの出番です。

[root@ip-10-152-167-242 ~]# su - postgres
-bash-4.1$ pwd
/var/lib/pgsql
-bash-4.1$ ls -F
9.2/
-bash-4.1$ /usr/pgsql-9.2/bin/initdb -D /var/lib/pgsql/9.2/data --no-locale -E UTF-8
The files belonging to this database system will be owned by user "postgres".
This user must also own the server process.

The database cluster will be initialized with locale "C".
The default text search configuration will be set to "english".

fixing permissions on existing directory /var/lib/pgsql/9.2/data ... ok
(...snip...)

Success. You can now start the database server using:

/usr/pgsql-9.2/bin/postgres -D /var/lib/pgsql/9.2/data
or
/usr/pgsql-9.2/bin/pg_ctl -D /var/lib/pgsql/9.2/data -l logfile start

-bash-4.1$

■サーバの鍵と証明書を作成する


次にSSL接続用のサーバの鍵とサーバ証明書を作成します。

-bash-4.1$ openssl genrsa -out server.key 1024
Generating RSA private key, 1024 bit long modulus
.........................................++++++
....++++++
e is 65537 (0x10001)
-bash-4.1$ openssl req -new -key server.key -x509 -days 365 -out server.crt
(...snip...)
Country Name (2 letter code) [XX]:JP
State or Province Name (full name) []:Tokyo
Locality Name (eg, city) [Default City]:Minato-ku
Organization Name (eg, company) [Default Company Ltd]:Uptime Technologies, LLC
Organizational Unit Name (eg, section) []:
Common Name (eg, your name or your server's hostname) []:ec2-54-248-9-93.ap-northeast-1.compute.amazonaws.com
Email Address []:nobody@uptime.jp
-bash-4.1$ ls -F
9.2/ server.crt server.key
-bash-4.1$
サーバの公開鍵と証明書を作成したら、データベースクラスタのディレクトリに移動させます。

-bash-4.1$ chmod 600 server.*
-bash-4.1$ mv server.* 9.2/data/
-bash-4.1$ cd 9.2/data/
-bash-4.1$ ls -F
base/ pg_ident.conf pg_serial/ pg_tblspc/ postgresql.conf
global/ pg_log/ pg_snapshots/ pg_twophase/ postmaster.opts
pg_clog/ pg_multixact/ pg_stat_tmp/ PG_VERSION server.crt
pg_hba.conf pg_notify/ pg_subtrans/ pg_xlog/ server.key
-bash-4.1$

■PostgreSQLにSSLの設定行い、サービスを起動する


postgresql.confファイルの中で以下の設定を有効にします。

listen_addresses = '*'
port = 16543
ssl = on
ssl_cert_file = 'server.crt'
ssl_key_file = 'server.key'
上記以外の設定(log_line_prefixなど)も必要に応じて行います。

次に、インターネットからの接続に対して、すべてSSL接続を要求するようにpg_hba.confの最後の行に以下を追加します。

hostssl all all 0.0.0.0/0 password
そして、サービスを起動するスクリプトで設定するポート番号を変更するために、/etc/sysconfig/pgsql/postgresql-9.2 に以下を記述します。

PGPORT=16543
最後に、サーバ起動時に自動的に起動されるようにサービススクリプトを設定して、PostgreSQLサービスを起動します。

[root@ip-10-152-167-242 ~]# /sbin/chkconfig postgresql-9.2 on
[root@ip-10-152-167-242 ~]# /sbin/chkconfig --list postgresql-9.2
postgresql-9.2 0:off 1:off 2:on 3:on 4:on 5:on 6:off
[root@ip-10-152-167-242 ~]# /etc/rc.d/init.d/postgresql-9.2 start
Starting postgresql-9.2 service: [ OK ]
[root@ip-10-152-167-242 ~]#

■ユーザを作成し、パスワードを設定する


PostgreSQLを起動したら、ユーザの作成を行います。

ここでは、データベース作成権限のある一般ユーザとして snaga というユーザを作成しています。--pwpromptオプションも指定して、パスワードの設定も行います。

[root@ip-10-152-167-242 ~]# su - postgres
-bash-4.1$ createuser -p 16543 --createdb --encrypted --pwprompt snaga
Enter password for new role:
Enter it again:
-bash-4.1$
ユーザの作成が完了したら、サーバ側のセットアップは完了です。

■pgAdminIIIから接続する


それでは、手元のクライアントからインターネットを経由して、PostgreSQLサーバに接続してみます。

とは言え、特に変わった設定は不要で、接続先のホストにEC2のホスト名を指定して、Portに先ほどの16543を指定するだけです。


接続に成功すれば、赤い「×」アイコンが消えて、サーバの情報を取得できるようになります。ここで、右側のペインに表示されている「暗号」の項目が「SSL暗号化」となっていれば、SSL接続できていることになります。


なお、サーバ設定のダイアログでSSLを「無効」とすると接続できなくなります。「必ずSSL接続がされている」ことを確認するためにも、この確認もしておくと良いでしょう。

■まとめ


今回はAmazonのEC2上でPostgreSQLを立ち上げて、安全に接続する方法を解説してきました。

自分でいろいろと試すためにも、クラウド上で自由にPostgreSQLを利用できると非常に便利です。ぜひ、クラウドの利便性とセキュリティをバランスさせながら、うまく使ってみていただければと思います。

ではでは。

pg_receivexlogでリアルタイムバックアップを取得する

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 14です。

ご存じのように、PostgreSQLは9.0から標準でレプリケーションが実装されました。それに加えて9.2では、pg_receivexlogというコマンドが追加されました。

pg_receivexlog
http://www.postgresql.jp/document/9.2/html/app-pgreceivexlog.html

pg_receivexlogは、ネットワーク経由でトランザクションログを受信、ログのアーカイブを作成・蓄積していくことを可能にするコマンドです。このコマンドを使うことによってスタンバイサーバを稼働していなくても、レプリケーションの機能を使ってリアルタイムバックアップを取得することができるようになります。

pg_receivexlogの基本的な仕組みについては、ストリーミングレプリケーションの開発者のFujii氏の以下のスライドを参照してください。


今回は、このpg_receivexlogコマンドを使ってアーカイブログを別サーバに蓄積し、それを使ってリカバリができるのか、というところを検証してみようと思います。

なお、Fujii氏のエントリと盛大に被ってしまったのですが、ここでは気にしないことにします。氏のエントリも併せて読むと二倍楽しめるかもしれません。

■検証構成

今回は以下のような環境でpg_receivexlogの検証を実施しました。

マスターサーバ、スタンバイサーバのいずれもAmazon EC2でLInux AMIインスタンスを利用し、同一リージョン内でWAL転送する構成としています。

マスターサーバ(PostgreSQLを稼働するサーバ)
  • Amazon EC2 Linux AMI (32bit)
  • PostgreSQL 9.2
  • IPアドレス:10.156.93.165

スタンバイサーバ(pg_receivexlog稼働してWALを受信するサーバ)
  • Amazon EC2 Linux AMI (32bit)
  • PostgreSQL 9.2
  • IPアドレス:10.146.98.58
  • 受信したWALを蓄積するディレクトリ:/var/lib/pgsql/9.2/receivexlog/
  • ベースバックアップを取得するディレクトリ:/var/lib/pgsql/9.2/receivebase/
Amazon EC2におけるPostgreSQLサーバの基本的な構築方法については今回は割愛します。詳細はDay13のエントリを参照してください。

■pg_receivexlogのための設定項目

今回、pg_receivexlogを使うために追加で設定した項目は以下の通りです。

postgresql.confでは設定したのは以下の4つの項目です。

wal_level = archive
archive_command = 'cat /dev/null'
max_wal_senders = 3
wal_keep_segments = 8
アーカイブログとしてWALを生成しますのでwal_levelはarchiveとします。

今回はPostgreSQL稼働サーバ上ではアーカイブログを取得しませんが、archive_commandが設定されていないとベースバックアップの取得ができないため、archive_commandはダミーコマンドを設定します。

PostgreSQL稼働サーバ上にWAL送信プロセスが必要ですので、max_wal_sendersを3とします。

pg_receivexlogでの受信が遅延した場合、WALセグメントファイル8つ分までは追い付くことが可能な設定とします(ここでの8は適当な値です)。

次に、レプリケーションの接続を受け付けるためにpg_hba.confに設定を追加します。ここで指定している "10.146.98.58" はスタンバイサーバ(pg_receivexlogを稼働させるサーバ)のIPアドレスです。

hostssl replication postgres 10.146.98.58/32 trust

■リアルタイムバックアップを開始する

設定が完了したら、まずはPostgreSQLを起動します。

PostgreSQLが稼働し始めたら、スタンバイサーバ上でpg_receivexlogコマンドを起動してWALの受信を開始します。

-bash-4.1$ /usr/pgsql-9.2/bin/pg_receivexlog -h 10.156.93.165 -p 6453 -U postgres -D /var/lib/pgsql/9.2/receivexlog -v
pg_receivexlog: starting log streaming at 0/A000000 (timeline 1)
pg_receivexlogコマンドを実行する際には、マスターとなるPostgreSQLサーバのIPアドレス、ポート番号、接続ユーザ名、および受信したWALを保存するローカルのディレクトリを指定します。

"starting log streaming" というメッセージが出たら、ログの受信の開始は成功です。その時、受信用のディレクトリを見ると、

[root@ip-10-146-98-58 ~]# ls -al /var/lib/pgsql/9.2/receivexlog/
total 16392
drwxr-xr-x 2 postgres postgres 4096 Dec 6 14:40 .
drwx------ 5 postgres postgres 4096 Dec 6 14:33 ..
-rw------- 1 postgres postgres 16777216 Dec 6 14:40 00000001000000000000000A.partial
[root@ip-10-146-98-58 ~]#
という形でファイルができ始めていることが分かります。

ここで作成されているファイルの末尾には「.partial」という suffix が付いています。どうやら、まだ16MBを使い切っていない利用中のWALファイル(セグメントファイル)については末尾に「.partial」という suffix が付加されるようです。

ここで、PostgreSQLに対して(WALを生成するために)データの更新処理を行ってみます。

-bash-4.1$ /usr/pgsql-9.2/bin/pgbench -p 6453 -s 10 -i testdb
この時に受信ディレクトリを見てみると、

[root@ip-10-146-98-58 ~]# ls -al /var/lib/pgsql/9.2/receivexlog/
total 131080
drwxr-xr-x 2 postgres postgres 4096 Dec 6 14:43 .
drwx------ 5 postgres postgres 4096 Dec 6 14:33 ..
-rw------- 1 postgres postgres 16777216 Dec 6 14:42 00000001000000000000000A
-rw------- 1 postgres postgres 16777216 Dec 6 14:42 00000001000000000000000B
-rw------- 1 postgres postgres 16777216 Dec 6 14:42 00000001000000000000000C
-rw------- 1 postgres postgres 16777216 Dec 6 14:42 00000001000000000000000D
-rw------- 1 postgres postgres 16777216 Dec 6 14:42 00000001000000000000000E
-rw------- 1 postgres postgres 16777216 Dec 6 14:42 00000001000000000000000F
-rw------- 1 postgres postgres 16777216 Dec 6 14:43 000000010000000000000010
-rw------- 1 postgres postgres 16777216 Dec 6 14:43 000000010000000000000011.partial
[root@ip-10-146-98-58 ~]#
WALセグメントファイルが作成されており、WALが継続的に保存されていることが分かります。

■バックアップセットを取ってみる

それでは、ここでpg_receivexlogで取得したアーカイブログを使ったリカバリを試してみます。

まず、先ほどと同じようにログの受信を開始します。

bash-4.1$ /usr/pgsql-9.2/bin/pg_receivexlog -h 10.156.93.165 -p 6453 -U postgres -D /var/lib/pgsql/9.2/receivexlog -v
pg_receivexlog: starting log streaming at 0/19000000 (timeline 1)
ログの蓄積が開始されたら、次にベースバックアップを取得します。

今回は、pg_basebackupコマンドを使ってネットワーク経由でスタンバイサーバ上にベースバックアップを取得します。ここでは、一旦 /var/lib/pgsql/9.2/receivebase ディレクトリにベースバックアップを保存します。

-bash-4.1$ /usr/pgsql-9.2/bin/pg_basebackup -h 10.156.93.165 -p 6453 -U postgres -D /var/lib/pgsql/9.2/receivebase -v
NOTICE: pg_stop_backup complete, all required WAL segments have been archived
pg_basebackup: base backup completed
-bash-4.1$
ベースバックアップを取得したら、ベースバック取得以降のWALを生成するために、データを更新処理を行います。

[snaga@db01 ~]$ psql -p 6453 testdb
psql (9.2.1)
Type "help" for help.

testdb=> \d
List of relations
Schema | Name | Type | Owner
--------+--------------------+-------+----------
public | pg_stat_statements | view | postgres
public | pgbench_accounts | table | postgres
public | pgbench_branches | table | postgres
public | pgbench_history | table | postgres
public | pgbench_tellers | table | postgres
public | t1 | table | postgres
public | t2 | table | snaga
(7 rows)

testdb=> create table t3 ( uid integer primary key, uname text not null );
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t3_pkey" for table "t3"
CREATE TABLE
testdb=> insert into t3 values ( 101, 'Jung, Nicole' );
INSERT 0 1
testdb=>
ここで、CTRL-Cでpg_receivexlogコマンドを停止します。

bash-4.1$ /usr/pgsql-9.2/bin/pg_receivexlog -h 10.156.93.165 -p 6453 -U postgres -D /var/lib/pgsql/9.2/receivexlog -v
pg_receivexlog: starting log streaming at 0/19000000 (timeline 1)
pg_receivexlog: finished segment at 0/1A000000 (timeline 1)
^Cpg_receivexlog: received interrupt signal, exiting.
pg_receivexlog: not renaming "00000001000000000000001A", segment is not complete
bash-4.1$
pg_receivexlogコマンドの停止によって、スタンバイからマスターへの接続が完全に切断され、マスターサーバからスタンバイサーバへのログのストリーミングが停止します。

■アーカイブログリカバリを実施

この状態で、先ほどまでに取得してあったバックアップセットを使ってスタンバイノード上にレストア、リカバリを実施してみます。

使用するのは、ベースバックアップと、リアルタイムにバックアップを取ってあったアーカイブログです。

まず、取得してあったベースバックアップをスタンバイサーバに展開します。

-bash-4.1$ pwd
/var/lib/pgsql
-bash-4.1$ ls -F
9.2/
-bash-4.1$ cd 9.2/
-bash-4.1$ ls -F
backups/ data/ receivebase/ receivexlog/
-bash-4.1$ ls data/
-bash-4.1$ cp -r receivebase/* data/
-bash-4.1$ ls -F data
backup_label pg_ident.conf pg_snapshots/ PG_VERSION server.key
base/ pg_log/ pg_stat_tmp/ pg_xlog/
global/ pg_multixact/ pg_subtrans/ postgresql.conf
pg_clog/ pg_notify/ pg_tblspc/ postgresql.conf.orig
pg_hba.conf pg_serial/ pg_twophase/ server.crt
次に蓄積していたアーカイブログをpg_xlog以下に配置します。「.partial」と suffix の付いていたWALセグメントファイルも、通常のWALセグメントのファイル名に変更します。

-bash-4.1$ cp receivexlog/0000000100000000000000* data/pg_xlog/
-bash-4.1$ cd data/pg_xlog/
-bash-4.1$ ls -F
00000001000000000000000A 000000010000000000000013
00000001000000000000000B 000000010000000000000014
00000001000000000000000C 000000010000000000000015
00000001000000000000000D 000000010000000000000016
00000001000000000000000E 000000010000000000000017
00000001000000000000000F 000000010000000000000018
000000010000000000000010 000000010000000000000019
000000010000000000000011 00000001000000000000001A.partial
000000010000000000000012
-bash-4.1$ mv 00000001000000000000001A.partial 00000001000000000000001A
-bash-4.1$ pwd
/var/lib/pgsql/9.2/data/pg_xlog
-bash-4.1$
ここまで準備ができたら、スタンバイサーバのPostgreSQLを起動します。

[root@ip-10-146-98-58 ~]# /etc/rc.d/init.d/postgresql-9.2 start
Starting postgresql-9.2 service: [ OK ]
[root@ip-10-146-98-58 ~]#
スタンバイサーバ上でPostgreSQLサービスが正しく起動したら、データベースインスタンスに接続してレコードを見てみます。

[root@ip-10-146-98-58 ~]# su - postgres
-bash-4.1$ psql testdb
psql (9.2.1)
Type "help" for help.

testdb=# \d
List of relations
Schema | Name | Type | Owner
--------+--------------------+-------+----------
public | pg_stat_statements | view | postgres
public | pgbench_accounts | table | postgres
public | pgbench_branches | table | postgres
public | pgbench_history | table | postgres
public | pgbench_tellers | table | postgres
public | t1 | table | postgres
public | t2 | table | snaga
public | t3 | table | snaga
(8 rows)

testdb=# select * from t3;
uid | uname
-----+--------------
101 | Jung, Nicole
(1 row)

testdb=#
先ほど、ベースバックアップ取得以降に更新を行ったレコード(最後にINSERTしたもの)までリカバリできていることが分かります。

この時のサーバログを見ると、

[2012-12-07 00:10:14 JST] 4197: LOG: database system was interrupted; last known up at 2012-12-07 00:00:16 JST
[2012-12-07 00:10:14 JST] 4197: LOG: creating missing WAL directory "pg_xlog/archive_status"
[2012-12-07 00:10:14 JST] 4197: LOG: database system was not properly shut down; automatic recovery in progress
[2012-12-07 00:10:14 JST] 4197: LOG: redo starts at 0/19000074
[2012-12-07 00:10:14 JST] 4197: LOG: record with zero length at 0/1A023DEC
[2012-12-07 00:10:14 JST] 4197: LOG: redo done at 0/1A023DC4
[2012-12-07 00:10:14 JST] 4197: LOG: last completed transaction was at log time 2012-12-07 00:04:39.920898+09
[2012-12-07 00:10:14 JST] 4194: LOG: database system is ready to accept connections
となっていて、通常のWALと同じようにリカバリすることができていることが分かります。

■まとめ

今回は、バージョン9.2で新たに追加されたpg_receivexlogコマンドを使って、ネットワーク経由でのアーカイブログのリアルタイムバックアップ、そしてそこからリカバリできるかどうかを試してみました。

実際に実運用するためには手順として考えなければならないことはもう少しあるかと思いますが、機能として動作するところまでは今回確認することができました。

データベースの最大の役割のひとつは「データの保護」にあります(少なくとも私はそう思っています)。そういった意味でも、このpg_receivexlogコマンドのような機能は、今後の活躍が期待される領域のひとつのように思います。

では、また。

tablelogでテーブルの更新差分を取得する

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 15です。

PostgreSQLに限らず、RDBMSにおいて「特定のテーブルの更新差分だけを取得したい」というのは、DBAの夢であると言えます(よね?)。

トランザクションログの中には当然ながら「更新情報」が記録されているわけですが、それを再利用可能な形で取り出す方法が無く、涙で枕を濡らした思い出を持つDBAも多いことでしょう。多分。

もう少し分かりやすく申しますと、PostgreSQLのメジャーバージョンアップの際などにはpg_dumpによるexport/importが必要となるわけですが、データ量が多くなって来ている今、データのexportにも時間がかかりますし、24x365のシステムも増えていますので、移行している間にもデータの更新が発生してしまいます。

なので、こういった作業の合間に発生する更新情報(少ないとは言え)を、保存しておいて、後から再利用できると便利ですよね? ね? という話であります。

■tablelogモジュール


というわけで、今回はPostgreSQLにおいて、テーブルの更新差分を取得する方法をご紹介します。

今回はtablelogというモジュールを使います。

PgFoundry: Table Audit: Project Info
http://pgfoundry.org/projects/tablelog/

tablelogモジュールは、テーブルデータに更新が発生した場合に、トリガを使ってその更新データを別のログテーブルに保存するモジュールです。

ですので、特定のテーブルにロギング用のトリガを設定しておくことで、後刻、ログテーブルから更新情報を時系列で取り出すことが可能になる、ということです。

余談ですが、「tablelog」をGoogleで検索すると「次の検索結果を表示しています: tabelog」と言われるのですが、ちょっと違います。惜しいですが。

■tablelogモジュールのインストール


それでは、tablelogモジュールをインストールしてみます。

tar.gzを展開して、環境変数USE_PGXSを1に設定しつつ、make & make install を実行します。

なお、今回はインストールスクリプトやロギングの設定用のSQL関数をいくつか追加していますので、パッチ(table_log-0_4_4_snaga.diff)も適用します。

table_log-0_4_4_snaga.diff
https://gist.github.com/4291369

[snaga@devsv02 pgsql]$ tar zxf table_log-0.4.4.tar.gz
[snaga@devsv02 pgsql]$ cd table_log-0.4.4
[snaga@devsv02 table_log-0.4.4]$ ls
Makefile table_log.c table_log_init.sql tests
README.table_log table_log.h table_log.sql.in
[snaga@devsv02 table_log-0.4.4]$ patch -p1 < ../table_log-0_4_4_snaga.diff
patching file Makefile
patching file table_log.sql.in
patching file table_log_uninstall.sql.in
[snaga@devsv02 table_log-0.4.4]$ env USE_PGXS=1 PATH=/usr/pgsql-9.1/bin:$PATH make
sed 's,MODULE_PATHNAME,$libdir/table_log,g' table_log.sql.in >table_log.sql
sed 's,MODULE_PATHNAME,$libdir/table_log_uninstall,g' table_log_uninstall.sql.in >table_log_uninstall.sql
gcc -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -I/usr/include/et -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wformat-security -fno-strict-aliasing -fwrapv -fpic -I. -I. -I/usr/pgsql-9.1/include/server -I/usr/pgsql-9.1/include/internal -I/usr/include/et -D_GNU_SOURCE -I/usr/include/libxml2 -I/usr/include -c -o table_log.o table_log.c
gcc -O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector --param=ssp-buffer-size=4 -m64 -mtune=generic -I/usr/include/et -Wall -Wmissing-prototypes -Wpointer-arith -Wdeclaration-after-statement -Wendif-labels -Wformat-security -fno-strict-aliasing -fwrapv -fpic -L/usr/pgsql-9.1/lib -L/usr/lib64 -shared -o table_log.so table_log.o
rm table_log.o
[snaga@devsv02 table_log-0.4.4]$ su
Password:
[root@devsv02 table_log-0.4.4]# env USE_PGXS=1 PATH=/usr/pgsql-9.1/bin:$PATH make install
/bin/mkdir -p '/usr/pgsql-9.1/share/contrib'
/bin/mkdir -p '/usr/pgsql-9.1/lib'
/bin/mkdir -p '/usr/share/doc/pgsql/contrib'
/bin/sh /usr/pgsql-9.1/lib/pgxs/src/makefiles/../../config/install-sh -c -m 644 table_log.sql table_log_uninstall.sql '/usr/pgsql-9.1/share/contrib/'
/bin/sh /usr/pgsql-9.1/lib/pgxs/src/makefiles/../../config/install-sh -c -m 755 table_log.so '/usr/pgsql-9.1/lib/'
/bin/sh /usr/pgsql-9.1/lib/pgxs/src/makefiles/../../config/install-sh -c -m 644 ./README.table_log '/usr/share/doc/pgsql/contrib/'
[root@devsv02 table_log-0.4.4]# exit
exit
[snaga@devsv02 table_log-0.4.4]$
次に、tablelogを使用したいデータベースに対してインストールを行います。

[snaga@devsv02 table_log-0.4.4]$ psql -f /usr/pgsql-9.1/share/contrib/table_log.sql testdb2
SET
CREATE FUNCTION
CREATE FUNCTION
(...snip...)
CREATE FUNCTION
CREATE FUNCTION
[snaga@devsv02 table_log-0.4.4]$
これでインストールは完了です。

■ターゲットとなるテーブルにロギングを設定する


まず、ターゲットとなるテーブルのサンプルを作成します。

testdb2=# CREATE table t1 ( uid integer primary key, uname text not null );
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "t1"
CREATE TABLE
testdb2=#
次に、上記で作成したテーブル t1 に対してログ取得を設定します。

ログの設定はSQL関数の table_log_init() を使用します。最初の引数はロギングのレベルで、3~5の値を取ります。

ロギングレベルの4はtrigger_idと呼ばれるIDも含めて保存し、ロギングレベル5は更新したユーザ名も含めて保存します(詳細は後述)。

ここではロギングレベルを3にして設定を行います。

testdb2=# SELECT table_log_init(3, 't1');
table_log_init
----------------

(1 row)

testdb2=# \d+
List of relations
Schema | Name | Type | Owner | Size | Description
--------+--------+-------+-------+---------+-------------
public | t1 | table | snaga | 0 bytes |
public | t1_log | table | snaga | 0 bytes |
(2 rows)

testdb2=#

■テーブルを更新してログを取得する


それでは、実際にテーブルにレコードをINSERTしてみましょう。

testdb2=# SELECT * FROM t1;
uid | uname
-----+-------
(0 rows)

testdb2=# INSERT INTO t1 VALUES ( 101, 'satoshi nagayasu' );
INSERT 0 1
testdb2=# SELECT * FROM t1;
uid | uname
-----+------------------
101 | satoshi nagayasu
(1 row)

testdb2=# SELECT * FROM t1_log;
uid | uname | trigger_mode | trigger_tuple | trigger_changed
-----+------------------+--------------+---------------+-------------------------------
101 | satoshi nagayasu | INSERT | new | 2012-11-26 13:34:01.218549+09
(1 row)

testdb2=#
上記のように、テーブルt1に対して、そのログテーブルであるテーブルt1_logに、カラムのデータに加えて、トリガの種別(INSRET/UPDATE/DELETE)、タプルの種別(更新前のデータoldか、更新後のデータnewか)と、トリガが実行された時刻が保存されています。

次に、データのUPDATEを行ってみます。ここでは、小文字を大文字に変換してみます。

testdb2=# UPDATE t1 SET uname = upper(uname);
UPDATE 1
testdb2=# SELECT * FROM t1;
uid | uname
-----+------------------
101 | SATOSHI NAGAYASU
(1 row)

testdb2=# SELECT * FROM t1_log;
uid | uname | trigger_mode | trigger_tuple | trigger_changed
-----+------------------+--------------+---------------+-------------------------------
101 | satoshi nagayasu | INSERT | new | 2012-11-26 13:34:01.218549+09
101 | satoshi nagayasu | UPDATE | old | 2012-11-26 13:34:38.38462+09
101 | SATOSHI NAGAYASU | UPDATE | new | 2012-11-26 13:34:38.38462+09
(3 rows)

testdb2=#
今度は、UPDATEのログのため、trigger_modeがUPDATEとなり、更新前のレコード(old)と更新後のレコード(new)の両方が記録されています。

■ロギングの停止


なお、ロギングを停止したい場合には、disable_table_log()関数を使うことで停止することができます。(これはオリジナルには無く、今回適用したパッチで追加されるSQL関数です)

testdb2=# SELECT disable_table_log('t1');
disable_table_log
-------------------

(1 row)

testdb2=# \d t1+
Table "public.t1"
Column | Type | Modifiers
--------+---------+-----------
uid | integer | not null
uname | text | not null
Indexes:
"t1_pkey" PRIMARY KEY, btree (uid)

testdb2=#

■ログレベルの違い


ログレベルを4にするとtrigger_idと呼ばれる値が追加され、ログレベルを5にすると更新を実行したユーザも記録されるようになります。

trigger_idというのは、ロギングの対象テーブルにプライマリキーが存在しない場合に、プライマリキーの代替えとしてレコードを一意に識別するために設定される BIGSERIAL 値です。

以下は、テーブルt1, t2, t3について、それぞれログレベルを変えて変更差分を取得したものです。見ていただくと分かりますが、ログレベルによってログテーブルのカラム数が違っています。

testdb2=# \x
Expanded display is on.
testdb2=# SELECT * FROM t1_log;
-[ RECORD 1 ]---+------------------------------
uid | 101
uname | satoshi nagayasu
trigger_mode | INSERT
trigger_tuple | new
trigger_changed | 2012-11-26 13:48:24.141572+09

testdb2=# SELECT * FROM t2_log;
-[ RECORD 1 ]---+------------------------------
uid | 101
uname | satoshi nagayasu
trigger_mode | INSERT
trigger_tuple | new
trigger_changed | 2012-11-26 13:48:24.142149+09
trigger_id | 1

testdb2=# SELECT * FROM t3_log;
-[ RECORD 1 ]---+------------------------------
uid | 101
uname | satoshi nagayasu
trigger_mode | INSERT
trigger_tuple | new
trigger_changed | 2012-11-26 13:48:24.142683+09
trigger_id | 1
trigger_user | snaga

testdb2=#

■まとめ


というわけで、今回はテーブルの更新情報を取得、保存するtablelogモジュールを御紹介しました。

データベースのデータが大きくなり、かつメンテナンス時間を取りづらい昨今、テーブルの更新情報を取得してうまく使えば、運用管理がもっと容易になるケースが多くあると思います。

ぜひ、こういったモジュールもうまく使いこなしていただければと思います。

ではまた。

PostgreSQLのストレージアーキテクチャ(基本編)

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 16です。

PostgreSQLのアーキテクチャやパフォーマンスを議論する際、「ストレージ(ファイル)が追記型のストレージアーキテクチャを採用している」ということは、PostgreSQL特有の大きな特徴として認識している方も多いでしょう。

少し前にも、ネット上でPostgreSQLと他のRDBMSのストレージのアーキテクチャの違いについて話題になったこともありました。

PostgreSQLとMySQLはどちらかに明確な優位性がありますか? - QA@IT
http://qa.atmarkit.co.jp/q/2395

優位性云々の議論はとりあえず置いておくとして、まずはPostgreSQLの実際の仕組みをきちんと理解するために「追記型のストレージ」というものがどのように動いているのかを覗いてみます。

■「追記型のストレージアーキテクチャ」とは


PostgreSQLにおける「追記型のストレージアーキテクチャ」というのは、簡単に言えば、「レコードの更新処理を行う際に、ブロック内の以前のレコードを上書きするのではなく、別のレコードとして作成する」という仕組みのことです。


以降では、この追記型のアーキテクチャについてについて、テーブル内のレコードがどのように変化していくのか、実際の動作を追いながら解説していきます。

■pageinspectモジュールのインストール


今回は、テーブル内のレコードの状態を見るためにpageinspectモジュールを利用します。

pageinspectモジュールは、PostgreSQLのテーブルやインデックスのブロックの、さらにその中にある「タプル(内部的にはitemと呼ばれる)」の状態を取得するための関数群を提供するcontribモジュールです。

F.20. pageinspect
http://www.postgresql.jp/document/9.0/html/pageinspect.html

9.0以前はインストールスクリプトを使ってインストール、9.1以降はEXTENSIONとしてインストールすることになりますので、必要に応じてDay2のエントリも参照にしてインストールしてください。

■ブロック内部における新規レコードの状態


それでは、実際にテーブルの更新処理においてブロック内のレコードがどのように変化していくのかを見てみましょう。

まず、integerとtextのカラムを持つテーブルt1を作成し、レコードを一件INSERTします。

testdb=# CREATE TABLE t1 ( uid INTEGER PRIMARY KEY, uname TEXT NOT NULL );
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "t1"
CREATE TABLE
testdb=#
testdb=# INSERT INTO t1 VALUES ( 101, 'insert 1' );
INSERT 0 1
testdb=# select * from t1;
uid | uname
-----+----------
101 | insert 1
(1 row)

testdb=#
この状態でテーブルのブロック内のレコードの状態を見てみましょう。

レコードの状態を見るには、pageinspectモジュールで提供される二つの関数 get_raw_page() と heap_page_items() を使います。get_raw_page() はテーブルのブロックをbytea形式で取得します。heap_page_items()は、bytea形式のバイナリを受け取って、その内部にあるレコードの状態を表示します。

testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 8152 | 1 | 37 | 1859 | 0
(1 row)

testdb=#
「lp」はブロック内のアイテムのオフセットID、「lp_off」はブロック内におけるレコード本体のオフセット(アドレス)です。「lp_len」はレコードの長さです。


なお、テーブルファイルのブロック内部は上記のような配置になっており、データ本体はブロック内の空き領域を後ろの方から使用します。ですので、lp_offの値が8152と、8kBブロックのギリギリ後ろの方になっています。

また、「t_xmin」はそのレコードを作成したトランザクションのトランザクションIDを示しており、「t_xmax」は逆にそのレコードを削除したトランザクションのトランザクションIDを示しています。

上記の場合、t_xminの値が1859で、t_xmaxの値が0になっていますので、このレコードを作成したトランザクションのトランザクションIDが1859であり、かつまだ削除されていない(生きている)レコードであることが分かります。

■レコードに対する更新処理


次に、このレコードを更新して、ブロックの内部がどのように変化するか見てみます。

testdb=# UPDATE t1 SET uname = 'update 1' WHERE uid = 101;
UPDATE 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 8152 | 1 | 37 | 1859 | 1860
2 | 8112 | 1 | 37 | 1860 | 0
(2 rows)

testdb=#
レコードを更新すると、先ほどのレコード(lp==1)のt_xmaxが1860に設定され、新しいレコード(lp==2)が作成されました。

この時、新しく作成されたレコードのt_xminの値(1860)が古いレコードのt_xmaxの値(1860)と同じになっていることが分かります。つまり、古いレコード(lp==1)を削除するのと同時に新しいレコード(lp==2)を追加しているのです。

このように、PostgreSQLのUPDATEの処理では、古いレコードにt_xmaxを設定することで「削除したことにして」、新しいレコードを作成することによって、あたかも「更新処理」を行っているように動作するのです。

これが、PostgreSQLの「追記型のストレージアーキテクチャ」の基本的な構造です。

この状態で更新処理を行うと、更に新しいレコードが追加され、t_xminとt_xmaxを使ってチェーンのようにつながっていきます。

testdb=# UPDATE t1 SET uname = 'update 2' WHERE uid = 101;
UPDATE 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 8152 | 1 | 37 | 1859 | 1860
2 | 8112 | 1 | 37 | 1860 | 1861
3 | 8072 | 1 | 37 | 1861 | 0
(3 rows)

testdb=#

■不要領域の解放(VACUUM処理)


このように、更新処理を続けていると、PostgreSQLでは削除されたレコードの領域が増えていきます。この「削除されたレコードの領域」のことを俗に「不要領域」と呼んだりします。

この不要領域が増えてくると、「実際に生きているレコード数は少ないのにファイルサイズが大きい」という状況が発生し、パフォーマンスが低下する原因になります。

この不要領域を解放(回収)する仕組みが、PostgreSQLで有名な「VACUUM」です。

VACUUMの基本的なしくみは上記の通りなのですが、実際にブロック内部のレコードがどのように変化するのかを見てみます。

先ほどのテーブルt1に対してVACUUMを行ったの結果が以下のものです。

testdb=# VACUUM t1;
VACUUM
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 3 | 2 | 0 | |
2 | 0 | 0 | 0 | |
3 | 8152 | 1 | 37 | 1861 | 0
(3 rows)

testdb=#
先ほどまで存在していた古いレコード(lp==1とlp==2)のlp_lenがゼロになり、t_xmin/t_xmaxも削除されてlp_flagsが0に設定されています。これが「テーブルがVACUUMされた状態」になります。

なお、lp_flags==0の領域は「未使用領域」となっていて、すぐに再利用できる領域であることを意味しています。

この状態で、さらに更新処理(UPDATE)を行ってみます。

testdb=# UPDATE t1 SET uname = 'update 3' WHERE uid = 101;
UPDATE 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 3 | 2 | 0 | |
2 | 8112 | 1 | 37 | 1862 | 0
3 | 8152 | 1 | 37 | 1861 | 1862
(3 rows)

testdb=#
すると今度は以前のレコードが使っていたlp==2の領域が使われました。

つまり、VACUUM処理で解放(回収)したことで領域が空き、新しいレコードがそこを利用できるようになった、ということです。

■まとめ


今回はPostgreSQLの「追記型のストレージアーキテクチャ」について、実際の動作を追いかけながら、その基本的な仕組みを解説しました。

VACUUM処理は、現在は自動VACUUMプロセスによって自動的に実施されるため、昔ほど気にする必要は無くなってきました。

とは言え、どのような仕組みで動いているのかを理解しておくことは、トラブルシューティングやパフォーマンスチューニングの際には重要になってきますので、PostgreSQLを使いこなしたいという方は、ぜひこの辺りを理解しておいていただければと思います。

明日は、このPostgreSQLの「追記型のストレージアーキテクチャ」の弱点を克服するべく実装されている工夫について紹介します。

では、また。

PostgreSQLのストレージアーキテクチャ(Page Pruning編)

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 17です。

昨日のエントリでは、「ストレージアーキテクチャ(基本編)」ということで、PostgreSQLの内部でディスクがどのように使われているのか、その基本を解説しました。そして、PostgreSQLの更新処理で「新しいレコードを追記し、古いレコードからチェーンのようにつなぐ」という処理をしている様子を実際に観察しました。

今回は、この「追記型のストレージアーキテクチャ」の持つ課題を克服するために、どのような工夫がされているのかを実際の動作を見ながら解説します。

■追記型ストレージにおけるファイルの肥大化とその抑制


PostgreSQLに対する指摘として、「この追記型の更新処理がデメリットである。更新が増えるとレコードがどんどん増えていくのが弱点である」という指摘があります。

原則論としては、確かに更新処理を連続して行うとレコードが追記されてデータのサイズが大きくなっていくのですが、実際の実装はそんなに単純なものではなく、データサイズが増えないように随所に工夫がされています。

今回は、その中でも「Page Pruning」と呼ばれる処理について解説します。

■「Page Pruning」とは


「Page Pruning」とは、ページ内に未使用領域が少なくなった場合に行われる処理です。

ページ内に未使用領域が無い場合、更新処理を行う、つまり新しいレコード(タプル)を追記するためには、他のページブロックを使うしかないと思われがちです。

が、実際には他のブロックを使う前に、今現在タプルが存在しているページが本当に使えないのか、再度整理して確認する、という処理が内部で自動的に行われています。これが「Page Pruning」と呼ばれる処理です。

簡単に言うと、そのページ内だけVACUUMをかけて不要領域を削除している様子をイメージしてもらえれば良いと思います。

このPage Pruning処理があることによって、特定のレコードに対する更新処理が連続的に行われているような場合に、無闇に使用するブロック数が増えるような事態を抑制することができているのです。

それでは実際にページ内部の状態を具体的に見てみましょう。

■更新処理とブロック内の未使用領域の減少


まず前回と同じように更新処理をするためのテーブルを作成し、1件のレコードを作成、そのレコードを連続的に更新していきます。分かりやすくするために、ここでは少し大きめのレコードを使います。

testdb=# CREATE TABLE t1 ( uid INTEGER PRIMARY KEY, uname TEXT NOT NULL );
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "t1_pkey" for table "t1"
testdb=# INSERT INTO t1 VALUES ( 101, 'insert 1-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------' );
INSERT 0 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 309 | 39586 | 0
(1 row)

testdb=# UPDATE t1 SET uname = 'update 0-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------' WHERE uid=101;
UPDATE 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 309 | 39586 | 39587
2 | 7568 | 1 | 309 | 39587 | 0
(2 rows)

testdb=# UPDATE t1 SET uname = 'update 1-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------' WHERE uid=101;
UPDATE 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 309 | 39586 | 39587
2 | 7568 | 1 | 309 | 39587 | 39588
3 | 7256 | 1 | 309 | 39588 | 0
(3 rows)

testdb=# SELECT pg_relation_size('t1');
pg_relation_size
------------------
8192
(1 row)

testdb=#
このように更新処理を行っていくと、前回見た通り、古いレコードのt_xmaxと新しいレコードのt_xminとの間でチェーンの構造ができていきます。

このUPDATEをブロックの空き領域が無くなるまで続けるとどうなるのでしょうか。

以下が、UPDATEを続けたブロック内部の状態です。

lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 309 | 39586 | 39587
2 | 7568 | 1 | 309 | 39587 | 39588
3 | 7256 | 1 | 309 | 39588 | 39589
4 | 6944 | 1 | 309 | 39589 | 39590
5 | 6632 | 1 | 309 | 39590 | 39591
6 | 6320 | 1 | 309 | 39591 | 39592
7 | 6008 | 1 | 309 | 39592 | 39593
8 | 5696 | 1 | 309 | 39593 | 39594
9 | 5384 | 1 | 309 | 39594 | 39595
10 | 5072 | 1 | 309 | 39595 | 39596
11 | 4760 | 1 | 309 | 39596 | 39597
12 | 4448 | 1 | 310 | 39597 | 39598
13 | 4136 | 1 | 310 | 39598 | 39599
14 | 3824 | 1 | 310 | 39599 | 39600
15 | 3512 | 1 | 310 | 39600 | 39601
16 | 3200 | 1 | 310 | 39601 | 39602
17 | 2888 | 1 | 310 | 39602 | 39603
18 | 2576 | 1 | 310 | 39603 | 39604
19 | 2264 | 1 | 310 | 39604 | 39605
20 | 1952 | 1 | 310 | 39605 | 39606
21 | 1640 | 1 | 310 | 39606 | 39607
22 | 1328 | 1 | 310 | 39607 | 39608
23 | 1016 | 1 | 310 | 39608 | 39609
24 | 704 | 1 | 310 | 39609 | 0
(24 rows)
最新のタプルが1件(lp==24)、および更新前の古いタプル(lp==1~23)でブロック内部がほぼ一杯になっています。

また、lp==24のタプルのオフセットを見ると、かなりブロックの前方まで埋まってきていることが分かります。(前回解説した通り、ページブロックは後ろの方から使われます。)

■Page Pruning処理の実施


ここまでの状態で再度UPDATEを行ってみます。UPDATEが繰り返され、ブロック内が古いタプル(レコード)で一杯になっている状態で、再度UPDATEを行うとどうなるのでしょうか。

以下は、次のUPDATE処理を行った直後のページブロック内部の状態です。

lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 24 | 2 | 0 | |
2 | 7568 | 1 | 310 | 39610 | 0
3 | 0 | 0 | 0 | |
4 | 0 | 0 | 0 | |
5 | 0 | 0 | 0 | |
6 | 0 | 0 | 0 | |
7 | 0 | 0 | 0 | |
8 | 0 | 0 | 0 | |
9 | 0 | 0 | 0 | |
10 | 0 | 0 | 0 | |
11 | 0 | 0 | 0 | |
12 | 0 | 0 | 0 | |
13 | 0 | 0 | 0 | |
14 | 0 | 0 | 0 | |
15 | 0 | 0 | 0 | |
16 | 0 | 0 | 0 | |
17 | 0 | 0 | 0 | |
18 | 0 | 0 | 0 | |
19 | 0 | 0 | 0 | |
20 | 0 | 0 | 0 | |
21 | 0 | 0 | 0 | |
22 | 0 | 0 | 0 | |
23 | 0 | 0 | 0 | |
24 | 7880 | 1 | 310 | 39609 | 39610
(24 rows)
前回のエントリで見たように、lp_off==0の領域は未使用領域として解放されたことを表していますので、このブロックには空き領域がたくさんできていることが分かります。これは、PostgreSQLが「もはや必要とされない削除済みタプル」を判別して、自動的にページ内を整理したためです。

ですので、先ほどまで不要領域(削除済みレコード)で一杯だったこのページは、明示的にVACUUMを実施していないにも関わらず、自動的に不要領域が解放されて未使用領域を確保できたことになります。

もう少し詳細に見てみると、最後に残ったタプルは2つあり、lp==24のタプルの次にlp==2のタプルがつながっていて、lp==2のタプルが最新の生きているタプル(t_xmax==0)であることが分かります。

この時、テーブルファイルのサイズを見ると、最初の8kBのままです。

testdb=# SELECT pg_relation_size('t1');
pg_relation_size
------------------
8192
(1 row)

testdb=#
つまり、ページがほぼ一杯になってしまったにも関わらず、自動的にそのページ内部を整理することによって、他のブロックを使わずに当該ページの中だけで更新処理を行うことができたことになります。

このように、PostgreSQLでは更新処理を行う際にページブロック内が一杯になっていると、「他のブロックに書き込みに行く前」に、そのページブロック内を整理して再利用できる領域が無いかどうかを確認します。この処理を行うことによって、やみくもに他のブロックを読み書きしないようにしているのです。この処理を内部的には「Page Pruning」と呼びます。

このPage Pruningの処理によって、更新が多い場合でも更新処理を単一のページブロック内で完結するようになっています。

なお、Page Pruningの処理はソースコード内では src/backend/access/heap/pruneheap.c で実装されています。興味のある方はこの辺りを参照してください。

■まとめ


今回は、追記型のストレージ構造を持つPostgreSQLにおいて、更新処理によるブロック数の増加をどのような方法で抑制しているのか、その工夫のひとつを、実際のページの内部の状態を追いかけながら解説しました。

確かにPostgreSQLは追記型のストレージアーキテクチャを持つため、更新処理を行うことによってファイルサイズが肥大化していくと思われがちですが、実際にはこのようにファイルの肥大化(=ページブロックの増加)を防ぐ工夫が実装されていることがお分かりいただけたかと思います。

次回は、このようなテーブル内部におけるタプルの移動の処理とインデックスとの関係について解説してみようと思います。

では、また。

PostgreSQLのストレージアーキテクチャ(HOT編)

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 18です。

前回のエントリでは、PostgreSQLのストレージアーキテクチャのうち、特にPage Pruningについて実際の動作状況を見ながら、その仕組みを解説しました。

ここまではテーブルのみの解説をしてきましたが、実際には(ほとんどのテーブルには)インデックスもありますので、レコードを更新する場合にはインデックスについてのケアも必要になります。

というわけで、今回はレコードに対する更新が行われている間に、インデックスがどのように動作しているのかを、具体的な動作例を交えて見てみます。

■インデックスの内部構造

B-Treeのリーフノードの内部構造は以下のようになっています。


前々回のストレージアーキテクチャの基本編で、pageinspectというcontribモジュールを紹介しました。

pageinsectモジュールで提供されているheap_page_items()関数でテーブルのブロック内部の状態を見ることができたわけですが、インデックス(B-Treeインデックス)についてもbt_page_items()という同様の関数があり、この関数を使うことによって、B-Treeインデックスのリーフノードのブロック内部の状態を見ることができます。

testdb=# INSERT INTO t1 VALUES ( 101, 'insert 1' );
INSERT 0 1
testdb=#
testdb=# SELECT * FROM bt_page_items('t1_pkey', 1);
itemoffset | ctid | itemlen | nulls | vars | data
------------+-------+---------+-------+------+-------------
1 | (0,1) | 12 | f | f | 65 00 00 00
(1 row)

testdb=#
ここで確認しておいていただきたいのは、ctidカラムとdataカラムです。

dataカラムはインデックスのキーの値で、直前にプライマリキーとして「101」という値をINSERTしていますので、dataカラムのデータが16進数で「65」、10進数で「101」となっています。


上記の図の通り、インデックスは該当するレコードへのポインタを保持しているのですが、具体的にはこのctidカラムがインデックスエントリに対応するポインタ情報になります。

ここでは、ctidが「(0,1)」となっていますが、これは「テーブルファイルの0番目のブロックのラインポインタ1番目に位置している」ということを意味しています。

■インデックスとレコードの関連付け

それでは、本当にブロック0のラインポインタ1番目にレコードが存在しているかを見てみましょう。

testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 309 | 43737 | 0
(1 row)

testdb=#
見てみると、確かに0番ブロックの1番目のラインポインタにレコードが存在していました。

このように、インデックスとテーブルのレコードは、ctidという「ポインタ」を介してつながっているのです。

■レコードに対する更新処理(HOT Update)

それでは、前回と同じように同一のレコードに更新処理を行いながら、「テーブルブロックの内部」と「インデックスブロックの内部」がどのように変化していくかを見てみましょう。

まず、1件目のレコードをINSERTすると以下のようになります。

testdb=# INSERT INTO t1 VALUES ( 101, 'insert 1 ' );
INSERT 0 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 309 | 42731 | 0
(1 row)

testdb=# select * from bt_page_items('t1_pkey', 1);
itemoffset | ctid | itemlen | nulls | vars | data
------------+-------+---------+-------+------+-------------
1 | (0,1) | 12 | f | f | 65 00 00 00
(1 row)

testdb=#
テーブルブロックおよびインデックスブロック双方にアイテムが1件挿入されていることが分かります。

次に、このレコードに対して更新処理を行い、同じようにテーブルブロックとインデックスブロックの内部の状態を見てみます。

testdb=# UPDATE t1 SET uname = 'update 0-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------' WHERE uid=101;
UPDATE 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 309 | 42731 | 42732
2 | 7568 | 1 | 309 | 42732 | 0
(2 rows)

testdb=# select * from bt_page_items('t1_pkey', 1);
itemoffset | ctid | itemlen | nulls | vars | data
------------+-------+---------+-------+------+-------------
1 | (0,1) | 12 | f | f | 65 00 00 00
(1 row)

testdb=#
今度は、テーブルブロックの方は古いレコードが削除され、新しいレコードが追記されていることが分かりますが、一方でインデックスのブロックの方はエントリが増えていません。

これが「HOT: Heap Only Tuple」と呼ばれる仕組みで、インデックスのキーが更新されない限り、テーブルのタプルは増えていってもインデックスのエントリは増加しないことになります。

これが、追記型のストレージアーキテクチャの弱点をカバーするための工夫のひとつであり、このような更新処理を「HOT Update」と呼びます。

上記の状態で、インデックスのキーから(最新の)レコードを取得する場合、
  • インデックスキー(dataカラムの値)からレコードの位置(ctidカラムの値)を取得
  • ctidカラムから読みだしたテーブル内の位置のレコード(lp==1)を取得
  • 当該レコードは削除済(t_xmax==42732)のため、t_xmaxとt_xminのチェーンを辿ってlp==2のレコードを見つける
  • lp==2はまだ削除されていない(t_xmaxが空)レコードなので、これを使う
という流れになります。

なお、HOT Updateが有効になるのは「インデックスの張られていないカラムを更新する場合」であり、インデックスのあるカラムを更新するとHOT Updateとなりませんのでその点は注意が必要です。

■Page Pruning処理とインデックス

それでは、前回見たようなブロックの空き領域が少なくなってPage Pruning処理が行われた場合、インデックスとテーブルレコードの状態はどうなるのでしょうか。実際に動作させて見てみましょう。

lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 309 | 42731 | 42732
2 | 7568 | 1 | 309 | 42732 | 42733
3 | 7256 | 1 | 309 | 42733 | 42734
4 | 6944 | 1 | 309 | 42734 | 42735
5 | 6632 | 1 | 309 | 42735 | 42736
6 | 6320 | 1 | 309 | 42736 | 42737
7 | 6008 | 1 | 309 | 42737 | 42738
8 | 5696 | 1 | 309 | 42738 | 42739
9 | 5384 | 1 | 309 | 42739 | 42740
10 | 5072 | 1 | 309 | 42740 | 42741
11 | 4760 | 1 | 309 | 42741 | 42742
12 | 4448 | 1 | 310 | 42742 | 42743
13 | 4136 | 1 | 310 | 42743 | 42744
14 | 3824 | 1 | 310 | 42744 | 42745
15 | 3512 | 1 | 310 | 42745 | 42746
16 | 3200 | 1 | 310 | 42746 | 42747
17 | 2888 | 1 | 310 | 42747 | 42748
18 | 2576 | 1 | 310 | 42748 | 42749
19 | 2264 | 1 | 310 | 42749 | 42750
20 | 1952 | 1 | 310 | 42750 | 42751
21 | 1640 | 1 | 310 | 42751 | 42752
22 | 1328 | 1 | 310 | 42752 | 42753
23 | 1016 | 1 | 310 | 42753 | 42754
24 | 704 | 1 | 310 | 42754 | 0
(24 rows)
上記が、Page Pruning処理が起こる直前のテーブルブロックの状態です。更新のチェーンが長くなっていることが分かります。

itemoffset | ctid | itemlen | nulls | vars | data
------------+-------+---------+-------+------+-------------
1 | (0,1) | 12 | f | f | 65 00 00 00
(1 row)
一方、上記がインデックスの状態です。テーブルのレコードが何度も更新されているにも関わらず、インデックスエントリは最初に作成したものから変わっていない(更新されていない)ことが分かります。

この状態でレコードのUPDATEを行うと、Page Pruning処理が実行されます。

lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 24 | 2 | 0 | |
2 | 7568 | 1 | 310 | 42755 | 0
3 | 0 | 0 | 0 | |
4 | 0 | 0 | 0 | |
5 | 0 | 0 | 0 | |
6 | 0 | 0 | 0 | |
7 | 0 | 0 | 0 | |
8 | 0 | 0 | 0 | |
9 | 0 | 0 | 0 | |
10 | 0 | 0 | 0 | |
11 | 0 | 0 | 0 | |
12 | 0 | 0 | 0 | |
13 | 0 | 0 | 0 | |
14 | 0 | 0 | 0 | |
15 | 0 | 0 | 0 | |
16 | 0 | 0 | 0 | |
17 | 0 | 0 | 0 | |
18 | 0 | 0 | 0 | |
19 | 0 | 0 | 0 | |
20 | 0 | 0 | 0 | |
21 | 0 | 0 | 0 | |
22 | 0 | 0 | 0 | |
23 | 0 | 0 | 0 | |
24 | 7880 | 1 | 310 | 42754 | 42755
(24 rows)
この時、インデックスエントリは変化せず、相変わらず ctid=(0,1) を指しています。

itemoffset | ctid | itemlen | nulls | vars | data
------------+-------+---------+-------+------+-------------
1 | (0,1) | 12 | f | f | 65 00 00 00
(1 row)
この時、テーブルのlp==1のレコードはlp_flags==2となっていますが、このlp_flags==2というのは「HOT更新後のリダイレクト」を意味し、その時のlp_offはブロック内のオフセットではなく、ラインポインタの番号を意味しています(詳細はソースコードの include/storage/itemid.h を参照してください)。

つまり、レコードを探す順序としては、
  • インデックスの ctid==(0,1) からテーブルのlp==1のレコードを見る
  • lp==1がリダイレクトなので、lp_off==24からラインポインタ24番(lp==24)を見る
  • lp==24は削除済(t_xmax==42755)なので、チェーンをたどって次の新しいレコード(lp==2)を探す
  • lp==2はまだ生きているので、このレコードを使う
という流れになります。

■まとめ

今回は、HOT Updateにおけるテーブルのレコードとインデックスの関係について解説しました。

PostgreSQLは追記型のストレージアーキテクチャを取るために、更新処理によって古いレコードが増えていくことはその通りなのですが、実際には、これまで見てきた通り、レコードやブロックが極力増えないような工夫が随所に凝らされています。

PostgreSQLがどのように動作しているのかを正しく理解し、その特徴をうまく捉えて、設計やパフォーマンスチューニングしていただければと思います。

明日は、ストレージアーキテクチャの最終回として「FILLFACTOR」を取り上げます。

では、また。

PostgreSQLのストレージアーキテクチャ(FILLFACTOR編)

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 19です。

昨日までのエントリで、PostgreSQLの追記型アーキテクチャの基本的な仕組みと、「追記型であるがゆえの課題」にどのように対処しているのかを解説してきました。

ストレージアーキテクチャ小咄シリーズの最終回である今回は、「FILLFACTOR」と呼ばれる仕組みについて、その動作している様子を見ながら解説します。

(他のRDBMSをご存じの方のボキャブラリーに直すと、OracleやDB2で言うところのPCTFREE、SQL ServerのFILLFACTORと似たような役割の機能になります。)

■ブロックに空き領域が無い時の更新処理


ブロック内部が既にいっぱいで空き領域が無い時にレコードを追加または更新しようとした場合、特に前々回に解説したPage Pruningを実施しても空き領域が無かった場合、PostgreSQLではどのように処理されるのでしょうか。

結論から言うと、同一ブロック内に新しいレコードを追記する領域が無いため、他のブロックに新しいレコードを書き込むことになります。

テーブルのブロック構造を思い出しながら、具体的な例で見てみましょう。


以下の例は、テーブルに25件のレコードをINSERTしていった状態のテーブルです。

1つのブロックに25件のレコードが含まれていて、テーブルがその1つのブロックのみでできています。既にブロック全体にレコードが配置されており、これ以上レコードを追記する空き領域が無い状態になっています。

testdb=# INSERT INTO t1 VALUES ( 125, 'insert 125 ' );
INSERT 0 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46798 | 0
2 | 7568 | 1 | 311 | 46799 | 0
3 | 7256 | 1 | 311 | 46800 | 0
4 | 6944 | 1 | 311 | 46801 | 0
5 | 6632 | 1 | 311 | 46802 | 0
6 | 6320 | 1 | 311 | 46803 | 0
7 | 6008 | 1 | 311 | 46804 | 0
8 | 5696 | 1 | 311 | 46805 | 0
9 | 5384 | 1 | 311 | 46806 | 0
10 | 5072 | 1 | 311 | 46807 | 0
11 | 4760 | 1 | 311 | 46808 | 0
12 | 4448 | 1 | 311 | 46809 | 0
13 | 4136 | 1 | 311 | 46810 | 0
14 | 3824 | 1 | 311 | 46811 | 0
15 | 3512 | 1 | 311 | 46812 | 0
16 | 3200 | 1 | 311 | 46813 | 0
17 | 2888 | 1 | 311 | 46814 | 0
18 | 2576 | 1 | 311 | 46815 | 0
19 | 2264 | 1 | 311 | 46816 | 0
20 | 1952 | 1 | 311 | 46817 | 0
21 | 1640 | 1 | 311 | 46818 | 0
22 | 1328 | 1 | 311 | 46819 | 0
23 | 1016 | 1 | 311 | 46820 | 0
24 | 704 | 1 | 311 | 46821 | 0
25 | 392 | 1 | 311 | 46822 | 0
(25 rows)

testdb=# SELECT pg_relation_size('t1');
pg_relation_size
------------------
8192
(1 row)

testdb=#
この状態でレコードを更新すると、どうなるのでしょうか。

testdb=# UPDATE t1 SET uname = 'update 101 ' WHERE uid=101;
UPDATE 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46798 | 46823
2 | 7568 | 1 | 311 | 46799 | 0
3 | 7256 | 1 | 311 | 46800 | 0
4 | 6944 | 1 | 311 | 46801 | 0
5 | 6632 | 1 | 311 | 46802 | 0
6 | 6320 | 1 | 311 | 46803 | 0
7 | 6008 | 1 | 311 | 46804 | 0
8 | 5696 | 1 | 311 | 46805 | 0
9 | 5384 | 1 | 311 | 46806 | 0
10 | 5072 | 1 | 311 | 46807 | 0
11 | 4760 | 1 | 311 | 46808 | 0
12 | 4448 | 1 | 311 | 46809 | 0
13 | 4136 | 1 | 311 | 46810 | 0
14 | 3824 | 1 | 311 | 46811 | 0
15 | 3512 | 1 | 311 | 46812 | 0
16 | 3200 | 1 | 311 | 46813 | 0
17 | 2888 | 1 | 311 | 46814 | 0
18 | 2576 | 1 | 311 | 46815 | 0
19 | 2264 | 1 | 311 | 46816 | 0
20 | 1952 | 1 | 311 | 46817 | 0
21 | 1640 | 1 | 311 | 46818 | 0
22 | 1328 | 1 | 311 | 46819 | 0
23 | 1016 | 1 | 311 | 46820 | 0
24 | 704 | 1 | 311 | 46821 | 0
25 | 392 | 1 | 311 | 46822 | 0
(25 rows)

testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 1));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46823 | 0
(1 row)

testdb=# SELECT pg_relation_size('t1');
pg_relation_size
------------------
16384
(1 row)

testdb=#
空き領域が無い状態でレコードを更新(UPDATE)すると、更新前の古いレコード(lp==1)は削除(t_xmaxが設定される)されますが、同じブロックに空き領域が無い(Page Pruningをしても空き領域を作れない)ため、新しいレコードは異なる新しい2番目のブロックに書き込まれています。新しいブロックが作成されたことで、pg_relation_sizeで取得されるテーブルサイズが8kBから16kBに増加していることが分かります。

これは、もともと25件のレコードが1つのブロックに収まっていたものの、更新する際に(追記するための)空き領域が足りないため新しいブロックを作成した(テーブルファイルを拡張した)ということを示しています。

■「別ブロックへの更新処理」の何が問題か


では、この「別ブロックへの更新処理」の何が問題なのでしょうか。今まで見てきた「同じブロック内での更新」と比べて何が違うのでしょうか。

それは、「更新処理の際に別ブロックへのレコード追記を行うと、余分なI/O処理が発生する」ということです。

同一のブロック内での更新処理であれば、共有バッファ内の(8kBの)読み書きだけで処理が完結することが担保されます。データベースのI/O処理はブロック単位で行われるので、これは当然のことです。

それに対して、異なるブロックへの更新処理になると、「異なるブロックを読み込む」というI/O処理が発生する可能性がある、ということです。もう少し正確に言うと、
  • 空き領域のあるブロックを探す
  • 空き領域のあるブロックが見つからない場合は、ファイルを拡張して新しいブロックを作る
  • ブロックをディスクから共有バッファに読み込む
  • 共有バッファへ読み込んだブロックに対して新しいレコードを書き込む
という処理が行われることになり、(パフォーマンス的に)最悪のケースではディスクをファイルを拡張したり、読み込んだり、といったディスクI/Oが発生することになり、このことがパフォーマンスに大きな悪影響を与えます。なぜなら、メモリへの操作に対してディスクI/Oは、3ケタ近く処理速度が遅いからです。

そもそも、データベースにおいてはストレージが追記型構造であるかどうかに関わらず、ブロックへのアクセスを如何に削減するか、余計なブロックアクセスを減らすか、ということが重要です。

にも関わらずブロックを一杯まで使ってしまうと、異なるブロックへの更新処理が頻発し、ディスクI/Oが増えてしまう可能性が高まります。追記型のストレージアーキテクチャを持つPostgreSQLでは、この可能性が特に高くなります。

また、当然ながら更新の時だけではなく、(前回までに見てきたように)参照の時にもPostgreSQLの内部では古いタプルからチェーンを辿るようにして最新のタプルを探しますので、更新のチェーンが異なるブロックにまたがっているということは、参照の時にも余計なI/O処理を発生させることになり、パフォーマンスに影響を与えます。

■強制的に空き領域を確保して更新(UPDATE)に備える


というわけで、前述のような状況(別ブロックへの追記)が発生すると、「あぁ、同じブロックに空き領域があればなぁ」と考えるのが人情というものでしょう。この「UPDATEに備えて同じブロックに意図的に空き領域を確保しておく」ための仕組みが「FILLFACTOR」と呼ばれるものです。


これは、INSERTをする際には指定した割合の空き領域を確保しておき、UPDATEをする時にその空き領域を使う、という仕組みです。このように領域を予約しておくことによって、「更新処理の際に同じブロックの空き領域を使える」という可能性が飛躍的に高まります。

実際の動作を見てみましょう。

以下は、先の例とまったく同じレコードを25件INSERTしたテーブルの状態です。

先ほどと違うのは、同じ25件のレコードであるにも関わらず、今回は1ブロックに収まらず既に2ブロック使っていること、そして1ブロック目に少し空き領域があることです(最後のlp_offが1000バイト以上あります)。

testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46856 | 0
2 | 7568 | 1 | 311 | 46857 | 0
3 | 7256 | 1 | 311 | 46858 | 0
4 | 6944 | 1 | 311 | 46859 | 0
5 | 6632 | 1 | 311 | 46860 | 0
6 | 6320 | 1 | 311 | 46861 | 0
7 | 6008 | 1 | 311 | 46862 | 0
8 | 5696 | 1 | 311 | 46863 | 0
9 | 5384 | 1 | 311 | 46864 | 0
10 | 5072 | 1 | 311 | 46865 | 0
11 | 4760 | 1 | 311 | 46866 | 0
12 | 4448 | 1 | 311 | 46867 | 0
13 | 4136 | 1 | 311 | 46868 | 0
14 | 3824 | 1 | 311 | 46869 | 0
15 | 3512 | 1 | 311 | 46870 | 0
16 | 3200 | 1 | 311 | 46871 | 0
17 | 2888 | 1 | 311 | 46872 | 0
18 | 2576 | 1 | 311 | 46873 | 0
19 | 2264 | 1 | 311 | 46874 | 0
20 | 1952 | 1 | 311 | 46875 | 0
21 | 1640 | 1 | 311 | 46876 | 0
22 | 1328 | 1 | 311 | 46877 | 0
23 | 1016 | 1 | 311 | 46878 | 0
(23 rows)

testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 1));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46879 | 0
2 | 7568 | 1 | 311 | 46880 | 0
(2 rows)

testdb=# SELECT pg_relation_size('t1');
pg_relation_size
------------------
16384
(1 row)

testdb=#
この状態でUPDATE処理を行ってみます。

testdb=# UPDATE t1 SET uname = 'update 101 ' WHERE uid=101;
UPDATE 1
testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 0));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46856 | 46881
2 | 7568 | 1 | 311 | 46857 | 0
3 | 7256 | 1 | 311 | 46858 | 0
4 | 6944 | 1 | 311 | 46859 | 0
5 | 6632 | 1 | 311 | 46860 | 0
6 | 6320 | 1 | 311 | 46861 | 0
7 | 6008 | 1 | 311 | 46862 | 0
8 | 5696 | 1 | 311 | 46863 | 0
9 | 5384 | 1 | 311 | 46864 | 0
10 | 5072 | 1 | 311 | 46865 | 0
11 | 4760 | 1 | 311 | 46866 | 0
12 | 4448 | 1 | 311 | 46867 | 0
13 | 4136 | 1 | 311 | 46868 | 0
14 | 3824 | 1 | 311 | 46869 | 0
15 | 3512 | 1 | 311 | 46870 | 0
16 | 3200 | 1 | 311 | 46871 | 0
17 | 2888 | 1 | 311 | 46872 | 0
18 | 2576 | 1 | 311 | 46873 | 0
19 | 2264 | 1 | 311 | 46874 | 0
20 | 1952 | 1 | 311 | 46875 | 0
21 | 1640 | 1 | 311 | 46876 | 0
22 | 1328 | 1 | 311 | 46877 | 0
23 | 1016 | 1 | 311 | 46878 | 0
24 | 704 | 1 | 311 | 46881 | 0
(24 rows)

testdb=# SELECT lp,lp_off,lp_flags,lp_len,t_xmin,t_xmax FROM heap_page_items(get_raw_page('t1', 1));
lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax
----+--------+----------+--------+--------+--------
1 | 7880 | 1 | 311 | 46879 | 0
2 | 7568 | 1 | 311 | 46880 | 0
(2 rows)

testdb=# SELECT pg_relation_size('t1');
pg_relation_size
------------------
16384
(1 row)

testdb=#
UPDATE処理を行った結果を見てみると、1番目のブロックのlp==1のレコードが削除され、同じブロックにlp==24として更新されていることが分かります。つまり、1番目のブロックに空き領域が確保されていたために、UPDATE処理の際にその領域を使うことができている、ということが分かります。

FILLFACTORの設定方法はCREATE TABLEにオプションを付ける方法、ALTER TABLEで設定する方法などがあります。詳細はマニュアルを参照してください。

CREATE TABLE
http://www.postgresql.jp/document/9.0/html/sql-createtable.html
ALTER TABLE
http://www.postgresql.jp/document/9.0/html/sql-altertable.html

なお、FILLFACTORの値は「10~100(%)」で指定しますが、テーブルにおける設定のデフォルトは「100」、つまり「空き領域無しで全部詰め込む」という設定になっています。つまり、何も設定しないとFILLFACTORの恩恵はまったく受けられませんのでご注意ください。

■まとめ


今回は、UPDATE処理の際にできるだけ同一ブロック内の空き領域を使うための「FILLFACTOR」の仕組みについて、その実際の動作を見ながら解説してきました。

FILLFACTORは、特に更新処理の多いワークロードで性能の安定に寄与します。一方で、「空き領域を確保しておく」という機能の特性上、テーブルファイル全体のサイズは大きくなります。

このようなトレードオフが存在しますので、自身のデータベースのワークロードのタイプをよく見極めて、これらの機能をうまく使いこなしていただければと思います。

では、また。

次期バージョンの9.3で実装された更新可能ビューを試してみる

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 20です。

先日、ネット上でも少し話題になっていましたが、開発中のPostgreSQLの次バージョン(9.3)に「更新可能ビュー」をサポートするコードがコミットされました。

pgsql: Support automatically-updatable views.
http://archives.postgresql.org/pgsql-committers/2012-12/msg00154.php

今回はこの更新可能ビューについて、その制約なども含めてどのようなものなのかを見てみたいと思います。

なお、今回は開発中のバージョンで試しますので、試してみたい方はPostgreSQLのGitレポジトリからソースコードを取得して自身でビルドしてください。

Working with Git - PostgreSQL wiki
http://wiki.postgresql.org/wiki/Working_with_Git

■「更新可能ビュー」とは


「更新可能ビュー」とはどういった機能でしょうか。

RDBMSにおけるビューのしくみを理解している方は、それを思い浮かべていただければ分かるかと思いますが、ビューの定義が「十分に簡単」である場合、ビューに対するINSERTやUPDATE、DELETE処理は、元テーブルに対する更新処理に書き換えることが可能になるはずです。

この「ビューに対する更新処理を、元テーブルに対する更新処理へ自動的に書き換える」という機能が、「automatically updatable、自動的に更新できる」ビューと言われる機能になります。

■更新可能ビューの制約・前提条件


更新可能ビューが実装されたとは言え、「十分に簡単」という条件が付いているように、すべてのビューに対して更新が可能なわけではなく、その制約・前提条件があります。

なぜなら、「ビューへの更新処理」というのは、ビューの定義から参照元テーブルのレコード構造、つまり通常のビューとは「逆方向」に展開・変換する作業に他なりませんので、論理的に元テーブルのレコードを再構成できないと実行ができないのです。

更新可能ビューとしての前提条件は、開発版のオフィシャルマニュアルに記載がありましたので、以下に簡単に日本語に翻訳してきます。
  • 単一のテーブル、または更新可能ビューのみをFROM句に持つこと。(結合をしていないこと)
  • ビューの定義がWITH、DISTINCT、GROUP BY、HAVING、LIMIT、OFFSETを持たないこと。
  • ビューの定義がUNION、INTERSECT、EXCEPTを持たないこと。
  • ビューの定義のselectリストに出てくるカラムが、元テーブルのカラムをそのまま参照していること。expressionやリテラル、関数であってはならない。システムカラムであってもならない。
  • 元テーブルのカラムが二回以上ビューの定義に出てこないこと。
  • ビューがsecurity_barrier属性を持たないこと。
この辺りの制約・前提条件は、他のRDBMSと似ているところかもしれません。

詳細については、オフィシャルマニュアルの記載を参照してください。

PostgreSQL: Documentation: devel: CREATE VIEW
http://www.postgresql.org/docs/devel/static/sql-createview.html

■更新可能ビューを使ってみる


では最初に、もっとも基本的なビューを定義してレコードを更新できるか試してみます。

[snaga@devsv02 updatableview]$ /tmp/pgsql/bin/psql testdb
psql (9.3devel)
Type "help" for help.

testdb=# CREATE TABLE t1 ( uid INTEGER PRIMARY KEY, uname TEXT NOT NULL );
CREATE TABLE
testdb=# CREATE VIEW v1 AS SELECT * FROM t1;
CREATE VIEW
testdb=# select * FROM t1;
uid | uname
-----+-------
(0 rows)

testdb=#
まず、元テーブルに対して1件レコードをINSERTします。

testdb=# insert into t1 values ( 101, 'Park Gyu-Ri' );
INSERT 0 1
testdb=# select * from t1;
uid | uname
-----+-------------
101 | Park Gyu-Ri
(1 row)

testdb=# select * from v1;
uid | uname
-----+-------------
101 | Park Gyu-Ri
(1 row)

testdb=#
当然、元テーブルからもビューからもレコードが見えます。

次に、今度はビューに対して2件目のレコードをINSERTしてみます。

testdb=# insert into v1 values ( 102, 'Han Seung-Yeon' );
INSERT 0 1
testdb=# select * from t1;
uid | uname
-----+----------------
101 | Park Gyu-Ri
102 | Han Seung-Yeon
(2 rows)

testdb=# select * from v1;
uid | uname
-----+----------------
101 | Park Gyu-Ri
102 | Han Seung-Yeon
(2 rows)

testdb=#
ビューに対するレコードのINSERTも成功し、元テーブルに対してレコードがINSERTされていることが分かります。この時の実行プランを見てみると、INSERT文にビューv1を指定しているにも関わらず、内部的にはテーブルt1に更新しようとしていることが分かります。

testdb=# explain insert into v1 values ( 103, 'Nicole' );
QUERY PLAN
------------------------------------------------
Insert on t1 (cost=0.00..0.01 rows=1 width=0)
-> Result (cost=0.00..0.01 rows=1 width=0)
(2 rows)

testdb=#
ビューv1に対するUPDATEについても、同様にテーブルt1への更新として内部的に書き換えられています。

testdb=# explain update v1 set uname = 'Nicole' where uid = 102;
QUERY PLAN
-------------------------------------------------------------------------
Update on t1 (cost=0.00..8.27 rows=1 width=10)
-> Index Scan using t1_pkey on t1 (cost=0.00..8.27 rows=1 width=10)
Index Cond: (uid = 102)
(3 rows)

testdb=#

■ビューのselectリストに含まれないカラム


次に、ビューのselectリストに含まれないカラムがあった場合に、どのように扱われるのかを見てみます。

「ビューのselectリストに含まれない」ということは、ビューへの更新時に値が存在していないため元テーブルに値を渡せない、という状況になるはずです。

以下の例では、uidとunameという元テーブルのカラムのうち、uidカラムだけを使ってビューを定義し、そのビューに対して更新(INSERT)を行おうとしています。

testdb=# create view v2 as select uid from t1;
CREATE VIEW
testdb=# insert into v2 values ( 103 );
ERROR: null value in column "uname" violates not-null constraint
DETAIL: Failing row contains (103, null).
STATEMENT: insert into v2 values ( 103 );
testdb=#
このINSERT文はエラーになりましたが、エラーになっている理由を見ると、「元テーブルのunameカラムにnot-null制約が付いており、それに違反しているのでエラー」と言われていることが分かります。

ここで、元テーブルのunameカラムにデフォルト値を設定して、再度ビューv2に対して更新してみます。

testdb=# ALTER TABLE t1 ALTER COLUMN uname SET DEFAULT '(noname)';
ALTER TABLE
testdb=# insert into v2 values ( 103 );
INSERT 0 1
testdb=# select * from t1;
uid | uname
-----+----------------
101 | Park Gyu-Ri
102 | Han Seung-Yeon
103 | (noname)
(3 rows)

testdb=#
今度はエラーにはならず、unameカラムにはデフォルト値の "(noname)" という値が設定されました。

ここまで見ると分かりますが、ビューのselectリストに含まれないカラムは、
  • 元テーブルのカラムにデフォルト値があればデフォルト値に設定。
  • デフォルト値が無ければnullに設定。
  • その上で、元テーブルの制約でチェックされる。
という動きになっていることが分かります。

■まとめ


今回は、次のリリースで提供されることになった「更新可能ビュー」の機能について、どのような機能なのか、そしてどのように動くのかを、簡単にではありますが見てきました。

前述した通り、すべてのビューが更新可能になるわけではありませんし、実際に使うビューはここで示したより複雑ですので、利用にはいろいろ制約があるかと思います。

しかし、場合によっては開発時に便利に使えることもあるかと思いますので、機会があればぜひ試してみていただければと思います。

では、また。

pgTAPを使ってPostgreSQL上でデータベースの単体テストを行う

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 21です。

PostgreSQLはその拡張性の高さが大きな特徴となっており、「プロシージャ言語」、いわゆる「PL」として、一般的なSQLやPL/pgSQLだけではなく、PerlやPython、RubyやV8なども使うことができます。

これらのPLを使うと、自分の馴染んだ言語、特に広く一般的に使われているLLで簡単にロジックを書き、これをデータベース内で実行することができるようになります。このことが、最近PostgreSQLがアプリケーション開発プラットフォームとして注目を集めてきている大きな理由の一つでしょう。

一方で、ロジックを実装するということは、そのロジックが正しく動くことを確認するためのテストを行わなければなりません。

というわけで、今回はPostgreSQL上で開発を行う場合にユニットテストに使えるツール「pgTAP」を紹介します。

■単体テストツール「pgTAP」とは


pgTAPは、David E. Wheeler氏によって開発されているPostgreSQL用の単体テストツールです。

pgTAP: Unit Testing for PostgreSQL
http://pgtap.org/

単体テストを実行するのに必要なさまざまなSQL関数を提供しており、テストスクリプト内でこれらのSQL関数を使うことで、スキーマの構造やプロシージャ(ユーザ定義関数)のロジックの正しさなどをテストすることができます。

データベースの単体テストをする場合、一般的にはスキーマの構造、テーブルの内容、プロシージャやユーザ定義関数の動作が主なテスト対象となると思われますので、ここではこれらに絞ってテストの実施方法を解説します。

■インストール方法


pgTAPはcontribモジュールと同じようにインストールすることができます。

コンパイルする際には、pg_configコマンド(通常はPostgreSQLをインストールしたbinディレクトリにあります)にPATHが通っていることを確認し、また、環境変数USE_PGXSを "1" に設定してmakeします。

[snaga@devsv02 pgtap-0.90.0]$ ls
Changes doc META.json pgtap-schema.control src
compat getos.sh pgtap.control README.md test
contrib Makefile pgtap-core.control sql tocgen
[snaga@devsv02 pgtap-0.90.0]$ env PATH=/usr/pgsql-9.1/bin:$PATH USE_PGXS=1 make
cp sql/pgtap.sql.in sql/pgtap.sql
sed -e 's,MODULE_PATHNAME,$libdir/pgtap,g' -e 's,__OS__,linux,g' -e 's,__VERSION__,0.90,g' sql/pgtap.sql > sql/pgtap.tmp
mv sql/pgtap.tmp sql/pgtap.sql
(...snip...)
cp sql/pgtap.sql sql/pgtap--0.90.0.sql
cp sql/pgtap-core.sql sql/pgtap-core--0.90.0.sql
cp sql/pgtap-schema.sql sql/pgtap-schema--0.90.0.sql
[snaga@devsv02 pgtap-0.90.0]$
コンパイルできたら、rootになってmake installを実行します。

[snaga@devsv02 pgtap-0.90.0]$ su
Password:
[root@devsv02 pgtap-0.90.0]# env PATH=/usr/pgsql-9.1/bin:$PATH USE_PGXS=1 make install
/bin/mkdir -p '/usr/pgsql-9.1/share/extension'
/bin/mkdir -p '/usr/share/doc/pgsql/extension'
/bin/sh /usr/pgsql-9.1/lib/pgxs/src/makefiles/../../config/install-sh -c -m 644 ./pgtap.control ./pgtap-core.control ./pgtap-schema.control '/usr/pgsql-9.1/share/extension/'
/bin/sh /usr/pgsql-9.1/lib/pgxs/src/makefiles/../../config/install-sh -c -m 644 ./sql/pgtap--0.90.0.sql ./sql/pgtap-core--0.90.0.sql ./sql/pgtap-schema--0.90.0.sql ./sql/pgtap--unpackaged--0.26.0.sql ./sql/pgtap--0.90.0.sql ./sql/pgtap-core--0.90.0.sql ./sql/pgtap-schema--0.90.0.sql '/usr/pgsql-9.1/share/extension/'
/bin/sh /usr/pgsql-9.1/lib/pgxs/src/makefiles/../../config/install-sh -c -m 644 ./doc/pgtap.mmd '/usr/share/doc/pgsql/extension/'
[root@devsv02 pgtap-0.90.0]#
ここまで完了すれば、あとはcontribモジュールと同じように、データベースに対して登録(9.1以降ならCREATE EXTENSION)を行います。

[snaga@devsv02 pgtap-0.90.0]$ psql -U postgres testdb
psql (9.1.2)
Type "help" for help.

testdb=# CREATE EXTENSION pgtap;
CREATE EXTENSION
testdb=# \q
[snaga@devsv02 pgtap-0.90.0]$

■単体テストの構造


pgTAPでテストを書く場合には、「基本的なお作法」と言えるスクリプトの構造があります。

まず、テストを開始する際には、トランザクションを開始し、plan()関数を呼び出して、これから実行しようとするテストの数を指定します。

わざわざplan()関数を呼び出してテストの数を指定するのは、どうやら「これから実行しようとしているテストについて、きちんと理解・把握しているべきだ」という思想からのようです。(ドキュメントを読む限り)

例えば、13個のユニットテストを実行するスクリプトの場合、

BEGIN;
SELECT plan(13);
のような形でスクリプトを始めます。

plan()関数を実行したら各種テストを実行します。各テストの書き方は後述します。

必要なテストを実行したら、最後にfinish()関数を呼び出してテストの完了を宣言し、トランザクションをロールバックします。

SELECT * FROM finish();
ROLLBACK;
これらを記述したテストスクリプトは以下のようになります。

BEGIN;
SELECT plan(1);

SELECT tables_are (
'public',
ARRAY[
't1'
]
);

SELECT * FROM finish();
ROLLBACK;
このようなpgTAP用テストスクリプトをpsqlコマンドで実行すると、以下のような出力を得られます。

[snaga@devsv02 t]$ psql -f t1.sql testdb
BEGIN
plan
------
1..1
(1 row)

tables_are
-----------------------------------------------------
ok 1 - Schema public should have the correct tables
(1 row)

finish
--------
(0 rows)

ROLLBACK
[snaga@devsv02 t]$
ここでは、「tables_are」というテストをひとつだけ実行していますが、当該テストの結果は「Schema public should have the correct tables」となって成功しており、finish()関数の結果も何も無いことから、全体として問題なくテストが通ったことが分かります。

なお、テストに失敗すると、finish()関数が以下のような結果を返すので、それを見て判別することができます。

finish
-------------------------------------
# Looks like you failed 1 test of 1
(1 row)

■テーブルとカラムのテスト


それでは、まずはテーブルの存在のチェックからテストを行ってみます。テーブルの存在をチェックするためには tables_are() 関数を使います。

最初の引数はスキーマ名を、二番目の引数にはテーブル名のリスト(配列)を指定します。

SELECT tables_are (
'pgperf',
ARRAY[
'snapshot',
'snapshot_pg_stat_bgwriter'
]
);
成功すると、

tables_are
-----------------------------------------------------
ok 1 - Schema pgperf should have the correct tables
(1 row)

失敗すると、

tables_are
-----------------------------------------------------------------
not ok 1 - Schema pgperf should have the correct tables +
# Failed test 1: "Schema pgperf should have the correct tables"+
# Missing tables: +
# snapshot_pg_stat_user_tables +
# snapshot_pg_database_size
(1 row)

といったメッセージが返されます。

上記のエラーメッセージでは snapshot_pg_stat_user_tables と snapshot_pg_database_size が足りないテーブル(Missing tables)として指摘されています。

次にカラムのテストを行います。指定したテーブル、指定した名前と型のカラムが存在しているかどうかを確認します。このテストには col_type_is() という関数を使います。

以下の例は、pgperfスキーマのsnapshot_pg_stat_bgwriterテーブルに、それぞれinteger型のsidカラム、bigint型のcheckpoints_timedカラムが存在しているかどうかをテストしています。

SELECT col_type_is('pgperf', 'snapshot_pg_stat_bgwriter', 'sid', 'integer', 'sid');
SELECT col_type_is('pgperf', 'snapshot_pg_stat_bgwriter', 'checkpoints_timed', 'bigint', 'checkpoints_timed');
カラムの有無のみを確認する(型のチェックをしない)has_column()という関数もあるのですが、col_type_is() 関数はカラムの有無もチェックしてくれるので、こちらを使えば十分でしょう。

成功すると、

col_type_is
-------------
ok 3 - sid
(1 row)
という結果を返し、失敗すると、

col_type_is
-------------------------
not ok 4 - sid +
# Failed test 4: "sid" +
# have: integer+
# want: bigint
(1 row)
という結果を返します。これは「bigintであることを期待しているのに(want)、実際にはintegerだった(have)」という意味です。

また、カラムそのものが存在しない場合には、

col_type_is
------------------------------------------------------------------
not ok 4 - sid2 +
# Failed test 4: "sid2" +
# Column pgperf.snapshot_pg_stat_bgwriter.sid2 does not exist
(1 row)
というエラーを返します。

■クエリ実行結果のテスト


次は、クエリの実行結果のテスト方法です。

以下のテーブルに対するクエリをサンプルとしてテストの書き方を見てみます。

testdb=> SELECT * FROM t1;
uid | uname
-----+----------------
101 | Park Gyu-Ri
102 | Han Seung-Yeon
103 | Nicole
104 | Koo Ha-Ra
105 | Kang Ji-Young
(5 rows)
クエリを実行した結果をテストするためには、results_eq()関数を使います。

一つ目の引数は実行するクエリの文字列、二つ目のクエリは結果を記述します。

以下のように出力される結果が単一の値の場合、

testdb=> SELECT uname FROM t1 WHERE uid=103;
uname
--------
Nicole
(1 row)
出力は、単一の要素を持つ配列として定義します。

SELECT results_eq (
'SELECT uname FROM t1 WHERE uid=103',
ARRAY[ 'Nicole' ]
);
また、複数の値を返却するクエリの場合には、

testdb=> SELECT uname FROM t1 WHERE uid=103 OR uid=104;
uname
-----------
Nicole
Koo Ha-Ra
(2 rows)
以下のように配列要素を増やすことで指定することができます。

SELECT results_eq (
'SELECT uname FROM t1 WHERE uid=103 OR uid=104',
ARRAY[ 'Nicole', 'Koo Ha-Ra' ]
);
また、レコードを返却するクエリの場合、

testdb=> SELECT * FROM t1 WHERE uid=103;
uid | uname
-----+--------
103 | Nicole
(1 row)
VALUESの表記を用いてレコードとして記述します。

SELECT results_eq (
'SELECT * FROM t1 WHERE uid=103',
$$ VALUES (103, 'Nicole') $$
);
レコードについても、複数の結果が返却される場合には、

testdb=> SELECT * FROM t1 WHERE uid=103 OR uid=104;
uid | uname
-----+-----------
103 | Nicole
104 | Koo Ha-Ra
(2 rows)
以下のように複数のレコードを記述することができます。

SELECT results_eq (
'SELECT * FROM t1 WHERE uid=103 OR uid=104',
$$ VALUES (103, 'Nicole'), (104, 'Koo Ha-Ra') $$
);

■プロシージャ、ユーザ定義関数のテスト


プロシージャやユーザ定義関数のテストも、クエリのテストと同じ方法で実行することができます。

SELECTクエリでプロシージャや関数を呼び出し、その結果を確認します。

SELECT results_eq(
'SELECT pgperf.create_snapshot_pg_stat_bgwriter(1)',
ARRAY[ true ]
);

■pg_proveコマンドによる一括実行と集計


なお、pgTAPによるテストの実行を支援するツールとして、同じ作者からpg_proveというツールがCPANで提供されています。

David E. Wheeler / TAP-Parser-SourceHandler-pgTAP - search.cpan.org
http://search.cpan.org/dist/TAP-Parser-SourceHandler-pgTAP/

このpg_proveは、pgTAPで使用するテストスクリプトを一括して実行して、その中に含まれるユニットテストの結果を集計してくれるツールで、実行すると「いくつのテストを実行したか、そのうちいくつ失敗したか、どのような失敗だったか」ということをレポートしてくれます。

以下はpg_proveコマンドを使ってテストを実行している様子ですが、「Files=1, Tests=18」とありますので、1つのファイルの中にある18のテストを実行し、その結果が「Result: PASS」、つまりすべて成功であったことを報告しています。

[snaga@devsv02 t]$ pg_prove -d testdb test_pg_stat_bgwriter.sql
t/test_pg_stat_bgwriter.sql .. ok
All tests successful.
Files=1, Tests=18, 0 wallclock secs ( 0.02 usr + 0.01 sys = 0.03 CPU)
Result: PASS
[snaga@devsv02 t]$
一方、以下はテストに失敗しているケースですが、「Failed tests: 4-5」、つまり4番目と5番目のテストに失敗しており、どこの結果が間違っていたのかもレポートされています。

また、結果として(当然ながら)テスト全体としても失敗(Result: FAIL)であったことをレポートしています。

[snaga@devsv02 t]$ pg_prove -d testdb test_pg_stat_bgwriter.sql
t/test_pg_stat_bgwriter.sql .. 1/21
# Failed test 4: "sid"
# have: integer
# want: bigint
# Failed test 5: "sid2"
# Column pgperf.snapshot_pg_stat_bgwriter.sid2 does not exist
# Looks like you failed 2 tests of 21
t/test_pg_stat_bgwriter.sql .. Failed 2/21 subtests

Test Summary Report
-------------------
t/test_pg_stat_bgwriter.sql (Wstat: 0 Tests: 21 Failed: 2)
Failed tests: 4-5
Files=1, Tests=21, 0 wallclock secs ( 0.02 usr + 0.01 sys = 0.03 CPU)
Result: FAIL
[snaga@devsv02 t]$

■まとめ


今回は、PostgreSQLでアプリケーションを開発する際に必要となる単体テストを実行するためツールであるpgTAPの基本的な使い方を紹介しました。

最初に述べたように、PostgreSQLの大きな特徴は拡張性であり、さまざまなプロシージャ言語でアプリケーションロジックをPostgreSQL上で動作させることができます。

また、単体テストのみならず、JenkinsなどのCIツールと組み合わせれば、さらにテストを自動化して開発プロセスの質を向上させることができるようになります。

ぜひ、こういったツールをうまく活用して、アプリケーション開発に役立てていただければと思います。

では、また。

データブロックサイズの変更と分析系クエリへの性能影響(SSD編)

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 22です。

最近、PostgreSQL上でデータ分析処理をよく行うようになってきました。また、いろいろなところでSSDも使うようになってきました。

PostgreSQLとデータ分析とSSD、という組合せを考えた時、どのようにチューニングするのが望ましいのか、あるいはどのようなチューニングができるのか、個人的にはまだまだ試行錯誤中だったりします。

SSDはハードディスクと比べてブロックサイズが大きいという特徴があります。また、DWH系のデータベースでは大きいブロックサイズを使うとパフォーマンス上のメリットがある、と言われています。

今回は、SSD上でPostgreSQLを使ってデータ分析系の処理を行う時に、データブロックのサイズを変えるとクエリのパフォーマンスにどのような影響を与えるのか、実際にクエリを実行しながら見ていきます。

■ブロックサイズの変更方法と確認方法


PostgreSQLでデータブロックのサイズを変更するには、PostgreSQLのブログラムバイナリをビルドする際にブロックサイズを指定する必要があります。

具体的にはconfigureスクリプトのオプションに--with-blocksizeというオプションを与えて、ここでブロックサイズを指定します。


./configure --prefix=/usr/local/pgsql --with-blocksize=32
configureで指定できるブロックサイズはキロバイト単位で指定する必要があり、指定できる値は「1, 2, 4, 8, 16, 32」のいずれかになります。これより大きいサイズにするためには、コードをハックする必要があります(ちょっと変えただけでは make check でエラーになりました・・・)

また、稼働しているPostgreSQLのデータブロックのサイズを確認するためには、SQLコマンドとしてSHOW block_sizeを実行します。

postgres=# show block_size;
block_size
------------
32768
(1 row)

postgres=#

■テスト環境とデータ


今回テストを行ったのは以下の環境です。
  • NEC Express5800 GT110b
  • Xeon Intel Xeon X3440 2.53GHz (1P4C)
  • Unbeffered ECC 16GB
  • Intel 520 Series 180GB
  • Hitachi Deskstar 7K1000 HDS72101
  • Red Hat Enterprise Linux 6.3 (x86_64)
OS領域はハードディスク上に確保し、PostgreSQLのプログラムはOS領域に、PostgreSQLのデータベースクラスタはSSD上に配置しています。

/dev/mapper/vg_devsv03-lv_root on / type ext4 (rw)
/dev/mapper/vg_devsv03-lv_home on /home type ext4 (rw)
/dev/sda1 on /disk/disk1 type ext4 (rw,discard)
PostgreSQLの設定は、postgresql.confの以下のパラメータのみ変更しています。

shared_buffers = 2048MB
wal_buffers = 32MB
checkpoint_segments = 128
checkpoint_timeout = 60min
autovacuum = off
今回、テストで使ったデータは、OSDL DBT-3のスキーマとテストデータです。

テストデータはスケールファクタを「10」として生成しており、データサイズは、ロードする前のCSVファイルで10GB、ロード済みデータベースサイズ(テーブルおよびインデックス)で25GBとなっています。

■データのローディングとインデックスの作成


まず、テーブルへのデータローディングと主キー制約の追加、およびインデックス作成のパフォーマンスを計測します。

対象となるテーブルは以下の8つのテーブルです。

dbt3=# \d+
List of relations
Schema | Name | Type | Owner | Size | Description
--------+-----------------+-------+-------+------------+-------------
public | customer | table | snaga | 276 MB |
public | lineitem | table | snaga | 8324 MB |
public | nation | table | snaga | 8192 bytes |
public | orders | table | snaga | 1979 MB |
public | part | table | snaga | 317 MB |
public | partsupp | table | snaga | 1335 MB |
public | region | table | snaga | 8192 bytes |
public | supplier | table | snaga | 17 MB |
(8 rows)

dbt3=#
テーブルにデータをロードした後、ALTER TABLEコマンドで各テーブルに主キー制約を追加し、かつCREATE INDEXコマンドで15個のインデックスを作成しています。

これらの処理を、PostgreSQLのデータブロックをそれぞれ8kBブロック、16kBブロック、32kBブロックに変更したデータベースに対して実行した結果が以下のグラフです。


青の部分はテーブルにデータをロードするのに要した時間、紫は主キー制約の追加に要した時間、クリーム色の部分は追加のインデックス作成に要した時間で、積み上げグラフで秒数で表示しています。

このグラフを見ると、データのローディングやインデックスの作成などについては、あまりパフォーマンス上の向上が見られないことが分かります。むしろ、ブロックサイズが8kBの時と比べて、32kBの時にはパフォーマンスが若干低下しています。(全体としてはさほどインパクトはありませんが)

なお、今回は時間の関係上、データのローディングとインデックスの作成については、一回しか計測していません。複数回計測して平均を取る、といった処理は行っていませんので、その点はご承知おきください。

■テーブルフルスキャン


データベースの構築が完了したら、クエリの実行を行ってみます。

まずは、もっとも単純なテーブルのフルスキャンを行います。

select count(*) from lineitem;
lineitemテーブルへのcount(*)の処理は、8GB強のテーブルをシーケンシャルスキャンすることになり、実行プランは以下の通りです。

QUERY PLAN
---------------------------------------------------------------------------
Aggregate (cost=1279737.95..1279737.96 rows=1 width=0)
-> Seq Scan on lineitem (cost=0.00..1129772.56 rows=59986156 width=0)
(2 rows)
以下が、ブロックサイズ別のlineitemテーブルへの実行時間(5回平均)です。


これを見るとブロックサイズを8kBから32kBへと大きくするに従ってパフォーマンスが向上しているように見えます。

ブロックサイズを変えることで、本当にこれだけパフォーマンスが向上しているのでしょうか。

その点を確認するために、5回分の実行時間を個別に比較したものが以下のグラフになります。


これを見ると一目瞭然ですが、8kBブロックの場合、1回目から5回目まで実行時間がほとんど変わっていないのに対して、16kBブロックの場合は4回目から、32kBブロックの場合は2回目から実行時間が大幅に短縮されています。

この「実行時間が短くなっていく」理由はまだよく分かっていませんが、少なくとも共有バッファの状態とはあまり関係がないようです。(テスト用のクエリは、PostgreSQLのサービス起動直後、つまり共有バッファが空の状態から5回連続して実行しています)

以下は、lineitemテーブルのフルスキャンを6457.410ミリ秒で実行した時に、lineitemテーブルのブロックは、全1057132ブロックのうち8ブロックしか共有バッファに載っていなかったことを示しています。(呼び出しているrpt_buf_cache.sqlはpg_buffercacheモジュールの値を集計するスクリプトです)

dbt3=# \timing
Timing is on.
dbt3=# SELECT count(*) FROM lineitem;
count
----------
59986052
(1 row)

Time: 6457.410 ms
dbt3=# \i rpt_buf_cache.sql
name | num_buf | num_blks | pct
----------+---------+----------+------
lineitem | 8 | 1057132 | 0.00
<others> | 46 | |
(2 rows)

Time: 32.223 ms
dbt3=# SHOW block_size ;
block_size
------------
32768
(1 row)

Time: 0.223 ms
dbt3=#
上記の通り、(Day7のエントリで紹介したpg_buffercacheモジュールを使って)共有バッファの状態を確認してみた限り、データブロックがほとんど共有バッファに載っていなくても、lineitemテーブルのフルスキャンを6秒強で実行できています。

よって、PostgreSQLではなくファイルシステムレベルに原因があるのかもしれません。

■GROUP BYによる集約


次に、GROUP BYによる集約処理のパフォーマンスを見てみます。

実行するクエリは以下の通りで、月ごとの注文金額総額を集計しています。

select date_trunc('month', o_orderdate), sum(o_totalprice)
from orders
group by date_trunc('month', o_orderdate)
order by 1;
実行プランは以下の通りです。

QUERY PLAN
----------------------------------------------------------------------------------
Sort (cost=363007.84..363013.85 rows=2406 width=8)
Sort Key: (date_trunc('month'::text, (o_orderdate)::timestamp with time zone))
-> HashAggregate (cost=362836.62..362872.71 rows=2406 width=8)
-> Seq Scan on orders (cost=0.00..287837.71 rows=14999781 width=8)
(4 rows)
実行時間をブロックサイズ別に測定したものが以下のグラフです。


このクエリの場合には、ほとんどパフォーマンスが変わりませんでした。

■結合と集約


最後に、もう少し複雑な結合と集約を含むクエリを実行してみます。

以下は、注文時に約束した期日までに配達が終わらなかった注文について、顧客ごとに集計して多い順に20件ランキングしています。

select o.o_custkey,count(*)
from orders o, lineitem l
where l.l_commitdate < l.l_receiptdate
and l.l_orderkey = o.o_orderkey
group by o.o_custkey
order by 2 desc limit 20;
以下は、上記のクエリのブロックサイズ別の実行時間です(5回平均)。


このクエリもパフォーマンスが向上していますが、最初のlineitemテーブルへのシーケンシャルスキャンの場合と違って、1回目から5回目まで、ブロックサイズに応じて実行時間が短縮されています。


但し、8kBブロック、16kBブロックの時にはHash Joinが使われたのに対して、32kBブロックの時にはMerge Joinが使われる、といった違いがありました。

以下は8kBブロックの時の実行プランです。

QUERY PLAN
--------------------------------------------------------------------------------------------------------
Limit (cost=7004441.17..7004441.22 rows=20 width=4)
-> Sort (cost=7004441.17..7006622.04 rows=872351 width=4)
Sort Key: (count(*))
-> GroupAggregate (cost=6822539.33..6981228.22 rows=872351 width=4)
-> Sort (cost=6822539.33..6872527.79 rows=19995384 width=4)
Sort Key: o.o_custkey
-> Hash Join (cost=649382.08..3304284.73 rows=19995384 width=4)
Hash Cond: (l.l_orderkey = o.o_orderkey)
-> Seq Scan on lineitem l (cost=0.00..1815236.90 rows=19995384 width=4)
Filter: (l_commitdate < l_receiptdate)
-> Hash (cost=403281.59..403281.59 rows=15000359 width=8)
-> Seq Scan on orders o (cost=0.00..403281.59 rows=15000359 width=8)
(12 rows)
以下は32kBブロックの際の実行プランです。

QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
Limit (cost=4970682.69..4970682.74 rows=20 width=4)
-> Sort (cost=4970682.69..4972685.24 rows=801019 width=4)
Sort Key: (count(*))
-> GroupAggregate (cost=4791392.62..4949367.86 rows=801019 width=4)
-> Sort (cost=4791392.62..4841380.97 rows=19995341 width=4)
Sort Key: o.o_custkey
-> Merge Join (cost=44.76..2093263.54 rows=19995341 width=4)
Merge Cond: (o.o_orderkey = l.l_orderkey)
-> Index Scan using pk_orders on orders o (cost=0.00..328663.08 rows=15000450 width=8)
-> Index Scan using i_l_orderkey on lineitem l (cost=0.00..1477217.99 rows=19995341 width=4)
Filter: (l_commitdate < l_receiptdate)
(11 rows)

■まとめ


今回は、PostgreSQLのデータブロックの変更方法と、ブロックサイズの変更がクエリのパフォーマンス、特に集計系・分析系のクエリにどのような影響を与えるのかを、簡単なクエリを実行しながら見てきました。

今回の実験では、クエリのパフォーマンスが向上した理由、あるいは向上しなかった理由を踏み込んで解明するところまでには至りませんでしたが、もう少し動作を理解した上で、うまくチューニングポイントを見つければ、データ分析のデータベースパフォーマンスを最適化する方法論が見つかるかもしれません。

興味がある方は、ぜひ試してみていただければと思います。私も継続的に調査していきたいと思います。

では、また。

テーブルパーティショニングを使って実現するパフォーマンス向上

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 23です。

今回は、PostgreSQLにおけるテーブルパーティショニングの機能を取り上げます。

BigDataやデータ分析といったキーワードが多く聞かれるようになってきましたが、PostgreSQLでも大きなデータに対する集計処理のパフォーマンスを向上させるための機能が提供されています。

■「テーブルパーティショニング」


RDBMSにおける「テーブルパーティショニング」と呼ばれる機能には、ディスクへのアクセスを「局所化させる」、または「分散させる」ために提供されている機能です。これらの機能を使うことによって、大規模なデータを対象とした集計系の処理のパフォーマンスを向上させることができます。

RDBMSにおけるパーティションの種類には大きく分けて3つの種類があります。(正確には、2つとそのコンビネーションですが)

・レンジパーティショニング

レンジパーティショニングというのは、連続的な値(と連続的な意味)を持つカラム(日付など)をパーティションキー(分割のキー)として指定することで、テーブルスキャンなどのアクセスを「局所化」させることを目的としたパーティショニングです。「垂直方向のパーティショニング」と呼ばれることもあります。

・ハッシュパーティショニング

ハッシュパーティショニングは、パーティションキーにするカラム(多くは主キー)のハッシュ値によって分割する方法です。こちらは、アクセスを「分散」させることを目的としたパーティショニングです。「水平方向のパーティショニング」と呼ばれることもあります。

・コンポジットパーティショニング

レンジパーティションとハッシュパーティショニングの組合せです。データへのアクセスを分散させつつ、局所化することができます。


なお、PostgreSQL(単体)で利用できるのは、上記のうち「レンジパーティショニング」になります。

■PostgreSQLのテーブルパーティショニング(Constraint Exclusion)


PostgreSQLにおけるテーブルパーティショニングは、「Constraint Exclusion」という機能を使って実現されます。

PostgreSQLは、「オブジェクトリレーショナルデータベース」と呼ばれる通り、PostgreSQL内部で使われるオブジェクトの高い拡張性と柔軟性を提供していますが、その「オブジェクトリレーショナルデータベース」としてのPostgreSQLの特徴のひとつに「テーブルの継承」という機能があります。

PostgreSQL 9.0.4文書:継承
http://www.postgresql.jp/document/9.0/html/ddl-inherit.html

これは、似たようなテーブルを定義する際、親テーブルの定義をベースにして、新たなカラムを追加するなどして「子テーブル(派生テーブル)」を作成できるというもので、オブジェクト指向プログラミングにおける「クラスの継承」の概念に似たものです。


この「派生テーブル」の機能を使って、派生テーブルを複数作成、その中にデータを分割して配置し、クエリを実行する際には必要な派生テーブル(これがパーティションとなる)だけを対象にクエリを実行する、というのがPostgreSQLにおけるテーブルパーティショニングです。

この時、対象となる派生テーブルだけをクエリの処理対象とする必要がありますが、これが「Constraint Exclusion」、日本語で「制約による排他」と呼ばれる機能です。

制約(通常はCHECK制約)を定義することによって、「どのパーティションにどのようなデータ(どのようなCHECK制約を満たすデータ)が入っているか」をオプティマイザが判断することが可能になるのです。

■パーティションの作成


それでは、実際にテーブルのパーティション化を行ってみましょう。

ここでは、以下のような「注文テーブル」を対象として、注文日(o_orderdate)をパーティションキーとしてパーティション化してみます。

CREATE TABLE orders (
o_orderkey INTEGER PRIMARY KEY,
o_custkey INTEGER,
o_orderstatus CHAR(1),
o_totalprice REAL,
o_orderdate DATE,
o_orderpriority CHAR(15),
o_clerk CHAR(15),
o_shippriority INTEGER,
o_comment VARCHAR(79)
);
まず、通常のテーブル orders を作成し、インデックスも作成します。

testdb=# CREATE TABLE orders (
testdb(# o_orderkey INTEGER PRIMARY KEY,
testdb(# o_custkey INTEGER,
testdb(# o_orderstatus CHAR(1),
testdb(# o_totalprice REAL,
testdb(# o_orderDATE DATE,
testdb(# o_orderpriority CHAR(15),
testdb(# o_clerk CHAR(15),
testdb(# o_shippriority INTEGER,
testdb(# o_comment VARCHAR(79)
testdb(# );
NOTICE: CREATE TABLE / PRIMARY KEY will create implicit index "orders_pkey" for table "orders"
CREATE TABLE
testdb=# CREATE INDEX orders_o_orderdate_idx ON orders(o_orderdate);
CREATE INDEX
testdb=# \d orders
Table "public.orders"
Column | Type | Modifiers
-----------------+-----------------------+-----------
o_orderkey | integer | not null
o_custkey | integer |
o_orderstatus | character(1) |
o_totalprice | real |
o_orderdate | date |
o_orderpriority | character(15) |
o_clerk | character(15) |
o_shippriority | integer |
o_comment | character varying(79) |
Indexes:
"orders_pkey" PRIMARY KEY, btree (o_orderkey)
"orders_o_orderdate_idx" btree (o_orderdate)

testdb=#
次に、このordersテーブルを親テーブルとして、これを継承して子テーブルを作成します。

ここでは、注文日(o_orderdate)をパーティションキーとして分割しますので、1992年の注文情報を保持するパーティション orders_1992 を作成してみます。

派生してパーティションを作成するには、CREATE TABLE文でINHERITSオプションを使います。

testdb=# CREATE TABLE orders_1992 () INHERITS (orders);
CREATE TABLE
testdb=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------+-------+----------
public | orders | table | postgres
public | orders_1992 | table | postgres
(2 rows)

testdb=# \d orders_1992
Table "public.orders_1992"
Column | Type | Modifiers
-----------------+-----------------------+-----------
o_orderkey | integer | not null
o_custkey | integer |
o_orderstatus | character(1) |
o_totalprice | real |
o_orderdate | date |
o_orderpriority | character(15) |
o_clerk | character(15) |
o_shippriority | integer |
o_comment | character varying(79) |
Inherits: orders

testdb=#
子テーブル(orders_1992)が作成され、「Inherits: orders」とあるようにordersテーブルからの派生テーブルであることも確認できましたが、テーブルを派生して作成しただけでは、主キー制約やインデックスなどは作成されませんので、必要に応じてこれらを追加で作成します。

testdb=# ALTER TABLE orders_1992 ADD PRIMARY KEY (o_orderkey);
NOTICE: ALTER TABLE / ADD PRIMARY KEY will create implicit index "orders_1992_pkey" for table "orders_1992"
ALTER TABLE
testdb=#
派生テーブルを作成し、主キー制約や必要なインデックスを作成したら、最後にCHECK制約を作成します。

testdb=# ALTER TABLE orders_1992 ADD CHECK(o_orderdate >= '1992-01-01' AND o_orderdate < '1993-01-01');
ALTER TABLE
testdb=# \d orders_1992
Table "public.orders_1992"
Column | Type | Modifiers
-----------------+-----------------------+-----------
o_orderkey | integer | not null
o_custkey | integer |
o_orderstatus | character(1) |
o_totalprice | real |
o_orderdate | date |
o_orderpriority | character(15) |
o_clerk | character(15) |
o_shippriority | integer |
o_comment | character varying(79) |
Indexes:
"orders_1992_pkey" PRIMARY KEY, btree (o_orderkey)
Check constraints:
"orders_1992_o_orderdate_check" CHECK (o_orderdate >= '1992-01-01'::date AND o_orderdate < '1993-01-01'::date)
Inherits: orders2

testdb=#
このCHECK制約を作成することで、このorder_1992テーブル(パーティション)には、注文日が1992年のデータのみが含まれているということをPostgreSQLのオプティマイザが知ることができるようになります。

上記の処理を他の年についても繰り返し、必要なパーティションすべてを作成したのが以下の状態です。

testdb=# \d
List of relations
Schema | Name | Type | Owner
--------+-------------+-------+----------
public | orders | table | postgres
public | orders_1992 | table | postgres
public | orders_1993 | table | postgres
public | orders_1994 | table | postgres
public | orders_1995 | table | postgres
public | orders_1996 | table | postgres
public | orders_1997 | table | postgres
public | orders_1998 | table | postgres
(8 rows)

testdb=#

■パーティションへのデータのローディング


パーティションへのデータのローディング方法は、大きく2種類あります。
  • 親テーブルにINSERTトリガを設定し、親テーブルへのINSERTを適切なパーティション(子テーブル)に自動的に振り分ける方法
  • パーティションを直接指定してデータをローディングする方法
です。

UPDATEやDELETEについても同じことが言えますので、状況に応じて選択すると良いでしょう。ある程度事前にデータを分割できるのであれば、パーティションに直接ロードしても良いかもしれません。(簡単ですし)

■パーティションテーブルに対する実行プラン


それでは、実際に通常のテーブルとパーティションテーブルで、処理やパフォーマンスがどのように異なってくるのかを見てみます。

まずは、条件無しでテーブルフルスキャンを実施し、すべての注文金額を合計する処理の実行プランを見てみます。クエリとしては以下のようになります。

SELECT sum(o_totalprice) FROM orders;
以下は通常のテーブルに対するクエリの実行プランです。

testdb=# EXPLAIN
SELECT sum(o_totalprice) FROM orders;
QUERY PLAN
----------------------------------------------------------------------
Aggregate (cost=44076.00..44076.01 rows=1 width=4)
-> Seq Scan on orders (cost=0.00..40326.00 rows=1500000 width=4)
(2 rows)

testdb=#
次にパーティションテーブルに対して同じクエリを実行してみます。

testdb=# EXPLAIN SELECT sum(o_totalprice) FROM orders2;
QUERY PLAN

--------------------------------------------------------------------------------
-------
Aggregate (cost=44079.01..44079.02 rows=1 width=4)
-> Append (cost=0.00..40329.00 rows=1500001 width=4)
-> Seq Scan on orders2 (cost=0.00..0.00 rows=1 width=4)
-> Seq Scan on orders_1992 orders2 (cost=0.00..6104.89 rows=227089 width=4)
-> Seq Scan on orders_1993 orders2 (cost=0.00..6093.45 rows=226645 width=4)
-> Seq Scan on orders_1994 orders2 (cost=0.00..6117.97 rows=227597 width=4)
-> Seq Scan on orders_1995 orders2 (cost=0.00..6146.37 rows=228637 width=4)
-> Seq Scan on orders_1996 orders2 (cost=0.00..6148.26 rows=228626 width=4)
-> Seq Scan on orders_1997 orders2 (cost=0.00..6124.83 rows=227783 width=4)
-> Seq Scan on orders_1998 orders2 (cost=0.00..3593.23 rows=133623 width=4)
(10 rows)

testdb=#
パーティションテーブルに対するスキャンでは、orders_1992からorders_1998まで、すべてのパーティションをスキャンしているため、結果として通常のテーブルをスキャンするコスト(44076.01)とほとんど同じ実行コスト(44079.02)になっていることが分かります。

これは、パーティション化されたテーブルであっても、一部のパーティションだけではなくすべてのパーティションをスキャンすれば、総実行コストは同じになる、ということを意味しています。

■テーブルパーティショニングによるパフォーマンス向上


では、次に条件を追加して集計処理をしてみます。例えば、1993年9月の注文金額だけを合計してみます。クエリとしては以下のようになります。

SELECT sum(o_totalprice) FROM orders
WHERE o_orderdate >= '1993-09-01'
AND o_orderdate <= '1993-09-30';
上記のクエリを通常のテーブルに対してEXPLAINしたものが以下です。

testdb=# EXPLAIN SELECT sum(o_totalprice) FROM orders
WHERE o_orderdate >= '1993-09-01'
AND o_orderdate <= '1993-09-30';
QUERY PLAN
-----------------------------------------------------------------------------------------------
Aggregate (cost=33690.55..33690.56 rows=1 width=4)
-> Seq Scan on orders (cost=0.00..33683.58 rows=2786 width=4)
Filter: ((o_orderdate >= '1993-09-01'::date) AND (o_orderdate <= '1993-09-30'::date))
(3 rows)

testdb=#
実行プランとしては、ordersテーブルに対するフルスキャンであり、実行コストは「33690.56」と見積もられています。

一方で、パーティションテーブルに対してEXPLAINした結果が以下です。

testdb=# EXPLAIN SELECT sum(o_totalprice) FROM orders
WHERE o_orderdate >= '1993-09-01'
AND o_orderdate <= '1993-09-30';
QUERY PLAN
-----------------------------------------------------------------------------------------------------
Aggregate (cost=7270.49..7270.50 rows=1 width=4)
-> Append (cost=0.00..7226.67 rows=17525 width=4)
-> Seq Scan on orders (cost=0.00..0.00 rows=1 width=4)
Filter: ((o_orderdate >= '1993-09-01'::date) AND (o_orderdate <= '1993-09-30'::date))
-> Seq Scan on orders_1993 orders (cost=0.00..7226.67 rows=17524 width=4)
Filter: ((o_orderdate >= '1993-09-01'::date) AND (o_orderdate <= '1993-09-30'::date))
(6 rows)

testdb=#
実行プランとしては、orders_1993パーティションに対するスキャンであり、実行コストは「7270.50」と見積もられており、通常のテーブルに比べて、実行コストが1/5程度に低減していることが分かります。

これは、スキャンをする対象がテーブル全体(全パーティション)ではなく、一部のパーティションに局所化することができたため、実行コストが小さくなっていることを意味しています。

このクエリを、通常のテーブルとパーティションテーブルそれぞれに実行した結果が以下になります。

以下は通常のテーブルに対する実行結果です。

testdb=# SELECT sum(o_totalprice) FROM orders
WHERE o_orderdate >= '1993-09-01'
AND o_orderdate <= '1993-09-30';
sum
------------
2.8482e+09
(1 row)

Time: 405.432 ms
testdb=#
以下はパーティションテーブルに対する実行結果です。

testdb=# SELECT sum(o_totalprice) FROM orders
WHERE o_orderdate >= '1993-09-01'
AND o_orderdate <= '1993-09-30';
sum
------------
2.8482e+09
(1 row)

Time: 72.615 ms
testdb=#
通常のテーブルに対して実行すると405ミリ秒、パーティションテーブルに対して実行すると72ミリ秒となっており、実際の実行時間の観点からも集約処理のパフォーマンスを大幅に向上できたことが分かります。

■まとめ


今回は、PostgreSQLで利用できるテーブルパーティショニングの機能について簡単に紹介しました。

まだ少し手間がかかるのが難点ではありますが、PostgreSQLでもテーブルパーティショニングの機能を使うことはできますし、BigDataやデータ分析が大きなトレンドとなりつつある今、こういった機能をうまく使いこなすことも大事になってくるのではないかと思います。

ぜひ、機会を見つけて試してみていただければと思います。

では、また。

テーブルパーティショニングツール「pg_part」を使ってみる

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 24です。

前回のエントリでは、PostgreSQLのテーブルパーティショニングの基本的なしくみとその使い方を解説してきました。

前回解説した通り、PostgreSQLのパーティショニングの機能は「理屈としては」確かに動くのですが、実際にはそのためにいろいろなコマンドを実行したりしなければならず、なかなか手間がかかるのも事実です。

■テーブルパーティションを操作するpg_partパッケージ


テーブルパーティショニングは、特に分析系の処理をしている時には便利なのですが、PostgreSQLの場合、作成や管理に結構手間がかかるため、なかなか手を出せない、という方もいるのではないでしょうか。(私も以前はそうでした)

そのため、パーティション操作のための処理を一括して実施してくれる関数群を提供するpg_partパッケージを作成しました。

uptimejp/pg_part
https://github.com/uptimejp/pg_part

今回は、このpg_partパッケージに含まれるSQL関数の使い方を紹介します。

pg_partを使うと、
  • パーティションの作成(+データの移行)
  • パーティションの解除(+データの移行)
  • パーティションの追加(アタッチ)
  • パーティションの切り離し(デタッチ)
が簡単に行えるようになります。

なお、pg_partパッケージはSQL関数の定義の集まり(SQLスクリプト)ですので、psqlコマンドを使ってデータベースに登録して使用します。

[snaga@devsv03 ~]$ psql -f pg_part.sql dbt3
BEGIN
CREATE FUNCTION
CREATE FUNCTION
(...snip...)
CREATE FUNCTION
CREATE FUNCTION
COMMIT
[snaga@devsv03 ~]$

■パーティションの作成


パーティションの作成には、pgpart.add_partition()関数を使います。

SELECT pgpart.add_partition(
schema_name,
table_name,
partition_name,
check_condition,
temp_file);
schema_nameはテーブルの存在するスキーマ名です。通常は 'public' などでしょう。

table_nameは、パーティションを作成する元となる親テーブルのテーブル名です。

partition_nameは、作成するパーティションの名前です。

check_conditionは、作成するパーティションが保持しているレコードの範囲を表すCHECK制約の条件です。特定のカラムについて範囲指定などの形で記述します。

temp_fileは、親テーブルからパーティションにデータを移行させる時に利用する一時ファイルのファイル名です。

これらのパラメータを指定して実行すると、成功するとtrue、失敗するとfalseを返します。

dbt3=# SELECT pgpart.add_partition(
dbt3(# 'public',
dbt3(# 'orders',
dbt3(# 'orders_1992',
dbt3(# ' ''1992-01-01'' <= o_orderdate AND o_orderdate < ''1993-01-01'' ',
dbt3(# '/tmp/orders.tmp');
add_partition
---------------
t
(1 row)

dbt3=#
pgpart.add_partition()関数は、
  • 親テーブルを継承して子テーブル(パーティション)を作成
  • 親テーブルからパーティションに移動させるレコードをエクスポート
  • 親テーブルからパーティションに移動させるレコードを削除
  • エクスポートしておいたレコードをパーティションにインポート
  • パーティションに親テーブルと同等の主キー制約を追加
  • パーティションに親テーブルと同等のインデックスを追加
という処理を行います。

なお、pgpart.add_partition()関数を実行すると、以下のようにいくつかNOTICEメッセージが出力されます。これは、パーティションを作成するために内部的に実行しているDDL文です。

psql:add_part.sql:8: NOTICE: add_partition: CREATE TABLE public.orders_1992( CONSTRAINT __orders_1992_check CHECK( '1992-01-01' <= o_orderdate AND o_orderdate < '1993-01-01' )) INHERITS (public.orders);
psql:add_part.sql:8: NOTICE: add_partition: COPY ( SELECT * FROM public.orders WHERE '1992-01-01' <= o_orderdate AND o_orderdate < '1993-01-01' ) to '/tmp/orders.tmp';
psql:add_part.sql:8: NOTICE: add_partition: DELETE FROM public.orders WHERE '1992-01-01' <= o_orderdate AND o_orderdate < '1993-01-01' ;
psql:add_part.sql:8: NOTICE: add_partition: COPY public.orders_1992 FROM '/tmp/orders.tmp';
psql:add_part.sql:8: NOTICE: add_partition: ALTER TABLE public.orders_1992 ADD PRIMARY KEY (o_orderkey);
psql:add_part.sql:8: NOTICE: add_partition: CREATE INDEX orders_1992_o_orderdate_idx ON public.orders_1992 USING btree (o_orderdate);
psql:add_part.sql:8: NOTICE: add_partition: CREATE INDEX orders_1992_o_custkey_idx ON public.orders_1992 USING btree (o_custkey);
pgpart.add_partition()関数だけではなく、pg_partパッケージの提供する関数群ではこのように内部的に呼び出されるDDL文をNOTICEメッセージとして出力しますが、すべて記載すると冗長になるためこのエントリでは省いています。

■パーティション一覧の取得


指定したテーブルが、どのようなパーティションから構成されているかを取得するための関数が pgpart.show_partition()関数です。

SELECT pgpart.show_partition(schema_name, table_name);
schema_nameはテーブルの存在するスキーマ名で、table_nameは親テーブルのテーブル名です。

例えば、ordersテーブルをパーティション化している場合、以下のように実行することで、ordersテーブルを構成するパーティションの一覧を取得することができます。

dbt3=# SELECT pgpart.show_partition('public', 'orders');
show_partition
----------------
orders_1992
orders_1993
orders_1994
orders_1995
orders_1996
orders_1997
(6 rows)

dbt3=#

■パーティションのマージ(解除)


作成したパーティションを、もとのテーブルに戻すには pgpart.merge_partition()関数を使います。

SELECT pgpart.merge_partition(
schema_name,
table_name,
partition_name,
check_constraint,
temp_file);
引数は、pgpart.add_partition()関数とほとんど同じです。

schema_nameはテーブルの存在するスキーマ名です。

table_nameはパーティションのデータを戻す先となる親テーブルのテーブル名です。

partition_nameは親テーブルにマージするパーティションの名前です。

check_conditionは、作成するパーティションが保持しているレコードの範囲を表すCHECK制約の条件です。特定のカラムについて範囲指定などの形で記述します(但し、現時点では未使用です)。

temp_fileは、パーティションから親テーブルにデータを移行させる時に利用する一時ファイルのファイル名です。

dbt3=# SELECT pgpart.merge_partition('public', 'orders', 'orders_1992', null, '/tmp/orders.tmp');
merge_partition
-----------------
t
(1 row)

dbt3=#

■パーティションのアタッチ(追加)


親テーブルとまったく同じスキーマ構造を持つテーブルを作成すると、そのテーブルをパーティションとして追加することができます。

SELECT pgpart.attach_partition (
schema_name,
table_name,
partition_name,
check_condition);
schema_nameはテーブルの存在するスキーマ名です。

table_nameはアタッチするパーティションの親テーブルのテーブル名です。

partition_nameはアタッチするパーティションの名前です。

check_conditionは、アタッチするパーティションが保持しているレコードの範囲を表すCHECK制約の条件です。

以下は、パーティション化されているordersテーブルにorders_1998パーティションを追加している例です。

dbt3=# SELECT pgpart.show_partition('public', 'orders');
show_partition
----------------
orders_1992
orders_1993
orders_1994
orders_1995
orders_1996
orders_1997
(6 rows)

dbt3=# SELECT pgpart.attach_partition('public', 'orders', 'orders_1998', ' ''1998-01-01'' <= o_orderdate AND o_orderdate < ''1999-01-01'' ');
attach_partition
------------------
t
(1 row)

dbt3=# SELECT pgpart.show_partition('public', 'orders');
show_partition
----------------
orders_1992
orders_1993
orders_1994
orders_1995
orders_1996
orders_1997
orders_1998
(7 rows)

dbt3=#
パーティションをアタッチする前には1997年のパーティションまでしか無かったものの、pgpart.attach_partition()関数で1998年のパーティションを追加した結果、以下のEXPLAINを見ると、SELECTクエリでordersテーブルに検索を行う際に1998年のパーティションも検索対象になっていることが分かります。

dbt3=# EXPLAIN SELECT count(*) FROM orders WHERE o_orderdate = '1998-01-01';
QUERY PLAN
----------------------------------------------------------------------------------------------------------
Aggregate (cost=6044.09..6044.10 rows=1 width=0)
-> Append (cost=0.00..6027.42 rows=6667 width=0)
-> Seq Scan on orders (cost=0.00..0.00 rows=1 width=0)
Filter: (o_orderdate = '1998-01-01'::date)
-> Bitmap Heap Scan on orders_1998 orders (cost=72.27..6027.42 rows=6666 width=0)
Recheck Cond: (o_orderdate = '1998-01-01'::date)
-> Bitmap Index Scan on orders_1998_o_orderdate_idx (cost=0.00..70.61 rows=6666 width=0)
Index Cond: (o_orderdate = '1998-01-01'::date)
(8 rows)

dbt3=#

■パーティションのデタッチ(削除)


逆に、特定のパーティションをテーブルから切り離す(デタッチ)することも可能です。

SELECT pgpart.detach_partition (
schema_name,
table_name,
partition_name);
schema_nameはテーブルの存在するスキーマ名です。

table_nameはデタッチするパーティションの親テーブルのテーブル名です。

partition_nameはデタッチするパーティションの名前です。

以下は、ordersテーブルを構成するパーティションのひとつであるorders_1992パーティションをデタッチしている例です。

dbt3=# SELECT pgpart.show_partition('public', 'orders');
show_partition
----------------
orders_1992
orders_1993
orders_1994
orders_1995
orders_1996
orders_1997
orders_1998
(7 rows)

dbt3=# SELECT pgpart.detach_partition('public', 'orders', 'orders_1992');
detach_partition
------------------
t
(1 row)

dbt3=# SELECT pgpart.show_partition('public', 'orders');
show_partition
----------------
orders_1993
orders_1994
orders_1995
orders_1996
orders_1997
orders_1998
(6 rows)

dbt3=#

■パーティション作成時の手順


テーブルをパーティション化するためには、pgpart.add_partition()関数を使って必要な全パーティションを作成していきます。

全パーティションの作成が終わって、親テーブルに残っているデータが無くなったら(親テーブルが論理的に空になったら)、親テーブルに対してVACUUMないしCLUSTERを実行してください。

パーティション作成の際には、パーティションに移動したレコードを親テーブルからDELETEしていますが、親テーブルにはまだ削除済みレコードが残っているため、明示的に親テーブルを切り詰める(作り直す)必要があります。

以下はordersテーブルをパーティション化している例ですが、VACUUMによってordersテーブルのサイズがゼロになっていることを確認してください。

dbt3=# \d+
List of relations
Schema | Name | Type | Owner | Size | Description
--------+-----------------+-------+-------+---------+-------------
public | orders | table | snaga | 1964 MB |
public | orders_1992 | table | snaga | 299 MB |
public | orders_1993 | table | snaga | 298 MB |
(...snip...)

dbt3=# VACUUM orders;
VACUUM
dbt3=# \d+
List of relations
Schema | Name | Type | Owner | Size | Description
--------+-----------------+-------+-------+---------+-------------
public | orders | table | snaga | 0 bytes |
public | orders_1992 | table | snaga | 299 MB |
public | orders_1993 | table | snaga | 298 MB |
(...snip...)

dbt3=#

■まとめ


今回は、PostgreSQLのテーブルパーティションの機能をより簡単に使うためのユーティリティを紹介しました。

まだ機能としては必ずしも完璧ではないですが、PostgreSQLのパーティショニングを気軽に試していただくための機能としては一通り揃えてみたつもりです。

世間(の一部?)的にはRDBMSは古いテクノロジーだと思われているようですが、データ分析のプラットフォームとしては、まだまだ可能性を持っていると思います。むしろ、今までのスキルや業務データとの整合性を考えると、PostgreSQLはもっと活用できるはず、と言えるでしょう。

これからは、PostgreSQLをデータ分析のプラットフォームとして今まで以上に活用するノウハウを探っていきたいと思います。

では、また。

PostgreSQL用MPPミドルウェア「Stado」の導入

$
0
0
PostgreSQL Advent Calendar 2012(全部俺)のDay 25です。

Advent Calendar最終日の今回は、少し大物としてPostgreSQLのMPPミドルウェア「Stado」の導入方法を紹介します。

Stado: The Open Source MPP Solution
https://launchpad.net/stado

■「MPP」とは何か


皆さんは「MPP」という言葉を聞いたことがあるでしょうか。

コンピュータの世界で「MPP」と言えば「Massive Parallel Processing」、日本語で言うところの「超並列処理」のことを指します。

Massively parallel (computing) - Wikipedia, the free encyclopedia
http://en.wikipedia.org/wiki/Massive_parallel_processing

データベースの世界で「MPP」と言うと、通常は「シェアードナッシング・アーキテクチャ」のスケーラブルな並列処理用のコンピュータアーキテクチャのことを指します。ベンダ製品で言うと、Teradata、Netezza、Greenplumなどが有名どころでしょう。

MPPは昔からデータウェアハウス(DWH)と呼ばれる領域で採用されてきたことからも分かる通り、特に大規模なデータの分析や集計処理に威力を発揮します。

(実は、筆者は新入社員の頃、PostgreSQLを使ったオープンソースのMPPの研究開発プロジェクトに参加していたこともあり、MPPテクノロジーにはちょっと思い入れがあったりします。ちなみに、学生の頃には今で言うHadoopのような分散処理の研究をしていました。)

■PostgreSQL用MPPミドルウェア「Stado」


そのMPPのアーキテクチャをPostgreSQLと組み合わせて実現するためのオープンソースのミドルウェアが「Stado」です。

Stado: The Open Source MPP Solution
https://launchpad.net/stado

Stadoは、複数のPostgreSQLサーバを束ねて、あたかも単一のデータベースサーバであるかのように見せることができるミドルウェアです。


Stadoは、前々回のエントリで紹介したパーティショニングのうち、「ハッシュパーティショニング」の機能を提供します。Stadoを使うことによって、大容量のデータに対して並列処理をすることができ、単体のPostgreSQLと比べて実行時間を大幅に短縮することができます。

Stadoの紹介をしているとこれもまた長くなりますので、Stadoの概要については上記の資料をご参照ください。

今回は、このStadoを実際に導入する手順を説明します。

とは言っても、誰もが複数台のサーバを用意できるわけでもないでしょうから、今回は1台のサーバにマルチコアCPUと複数のハードディスクを積んで、一台のサーバで並列処理をする環境を構築する方法を解説します。

以下のように、ディスクを4本積んでいるサーバで並列処理をさせることを想定してStadoの導入を行ってみます。

■導入手順


Stadoの大まかな導入手順は以下の通りです。
  • Stadoのインストール
  • PostgreSQLのセットアップ
  • Stadoのセットアップ
  • ユーザデータベースの作成
  • テーブルスペースへのパーティションの移動
  • テーブルの作成とデータのロード
順を追って説明していきます。

■導入環境


今回Stadoを導入した環境は以下の通りです。
  • NEC Express5800 GT110b
  • Xeon Intel Xeon X3440 2.53GHz (1P4C)
  • Unbeffered ECC 16GB
  • Hitachi Deskstar 7K1000 HDS72101
  • Red Hat Enterprise Linux 6.3 (x86_64)
なお、Stadoを動作させるにはPostgreSQLおよびJDKを必要とします。

今回、PostgreSQLは9.2を、JDKはOpenJDKの1.6.0を使っています。

[snaga@devsv03 ~]$ rpm -qa | grep postgresql
postgresql92-9.2.2-1PGDG.rhel6.x86_64
postgresql92-devel-9.2.2-1PGDG.rhel6.x86_64
postgresql92-server-9.2.2-1PGDG.rhel6.x86_64
postgresql92-contrib-9.2.2-1PGDG.rhel6.x86_64
postgresql92-libs-9.2.2-1PGDG.rhel6.x86_64
[snaga@devsv03 ~]$ rpm -qa | grep openjdk
java-1.6.0-openjdk-devel-1.6.0.0-1.45.1.11.1.el6.x86_64
java-1.6.0-openjdk-1.6.0.0-1.45.1.11.1.el6.x86_64
[snaga@devsv03 ~]$
PostgreSQLとOpenJDKのインストールは完了しているものとして、ここからはStadoの導入を進めていきます。

■Stadoのインストール


まずは、Stadoのソースコードを取得します。

通常は、分散ソースコード管理システムであるBazaarを使って以下から取得します。

https://code.launchpad.net/~sgdg/stado/stado

[snaga@devvm03 tmp]$ bzr branch lp:~sgdg/stado/stado
You have not informed bzr of your Launchpad ID, and you must do this to
write to Launchpad or access private data. See "bzr help launchpad-login".
Branched 55 revision(s).
[snaga@devvm03 tmp]$
とは言え、Bazzarをインストールしているユーザが多いとも思えないので、以下にソースコードとコンパイル済のjarファイルのパッケージを用意しました。

http://www.uptime.jp/go/stado

上記のURLから stado-20121223.tar.gz と install.sh スクリプトを取得します。

install.shスクリプトは、root権限で実行すると、OSアカウントのstadoユーザとstadoグループを作成し、必要なファイルを/usr/local/stadoにインストールします。

[snaga@devsv03 stado]$ wget http://www.uptime.jp/downloads/stado/stado-20121223.tar.gz
(...snip...)

[snaga@devsv03 stado]$ wget http://www.uptime.jp/downloads/stado/install_stado.sh
(...snip...)

[snaga@devsv03 stado]$ ls
install_stado.sh stado-20121223.tar.gz
[snaga@devsv03 stado]$ tar zxf stado-20121223.tar.gz
[snaga@devsv03 stado]$ ls
install_stado.sh stado-20121223/ stado-20121223.tar.gz
[snaga@devsv03 stado]$ cd stado-20121223
[snaga@devsv03 stado-20121223]$ ls -F
bin/ build.xml docs/ lib/ README.TXT stado.config
build/ dist/ jars/ misc/ src/
[snaga@devsv03 stado-20121223]$ su
Password:
[root@devsv03 stado-20121223]# sh ../install_stado.sh
[root@devsv03 stado-20121223]# ls -l /usr/local/stado/
total 16
drwxr-xr-x. 2 stado stado 4096 Dec 23 18:06 bin/
drwxr-xr-x. 2 stado stado 4096 Dec 23 18:06 config/
drwxr-xr-x. 2 stado stado 4096 Dec 23 18:06 lib/
drwxrwxr-x. 2 stado stado 4096 Dec 23 18:06 log/
[root@devsv03 stado-20121223]#

■PostgreSQLのセットアップ


Stadoのインストールが終わったら、まずはPostgreSQLの設定を行います。

・基本設定

postgresql.confの設定を変更した項目は以下の通りです。

listen_addresses = '*'
max_connections = 100
shared_buffers = 2GB
work_mem = 2GB
maintenance_work_mem = 2GB
wal_buffers = 32MB
checkpoint_segments = 128
checkpoint_timeout = 60min
log_filename = 'postgresql-%Y%m%d.log'
log_min_duration_statement = 0
log_checkpoints = on
log_connections = on
log_disconnections = on
log_line_prefix = '[%t] %p: '
log_temp_files = 0
autovacuum = off
PostgreSQLの設定を行ったら、PostgreSQLサーバを起動します。

・テーブルスペースの作成

PostgreSQLサーバを起動したら、デフォルトのデータベースクラスタ /var/lib/pgsql/9.2/data 以外にテーブルスペースを作成します。

これらのテーブルスペースは、データベースクラスタとは別のディスク上に配置し、それによってI/O処理を分散させる目的で使用します。

ここでは、/disk/disk2, /disk/disk3, /disk/disk4 に、合計3本のディスクをマウントしているものとして、それらのディスクにそれぞれ tblspc2, tblspc3, tblspc4 というテーブルスペースを作成する設定を行います。

まず、テーブルスペースとして使用するディレクトリを作成し、postgresユーザ/postgresグループを所有者として権限を設定します。

[root@devsv03 pgsql]# df
Filesystem 1K-blocks Used Available Use% Mounted on
/dev/sda3 936146792 739057756 148768328 84% /
/dev/sda1 101086 12010 83857 13% /boot
tmpfs 8213720 0 8213720 0% /dev/shm
/dev/sdb1 961432072 135545524 777048548 15% /disk/disk2
/dev/sdc1 961432072 53769008 858825064 6% /disk/disk3
/dev/sdd1 961432072 134776980 777817092 15% /disk/disk4
[root@devsv03 pgsql]#
[root@devsv03 pgsql]# mkdir /disk/disk2/pgsql
[root@devsv03 pgsql]# mkdir /disk/disk3/pgsql
[root@devsv03 pgsql]# mkdir /disk/disk4/pgsql
[root@devsv03 pgsql]# chown postgres:postgres /disk/disk4/pgsql /disk/disk3/pgsql /disk/disk2/pgsql
次に、作成したディレクトリをテーブルスペースとしてPostgreSQL上で登録します。

postgres=# select * from pg_tablespace;
spcname | spcowner | spcacl | spcoptions
------------+----------+--------+------------
pg_default | 10 | |
pg_global | 10 | |
(2 rows)

postgres=# create tablespace tblspc2 location '/disk/disk2/pgsql';
CREATE TABLESPACE
postgres=# create tablespace tblspc3 location '/disk/disk3/pgsql';
CREATE TABLESPACE
postgres=# create tablespace tblspc4 location '/disk/disk4/pgsql';
CREATE TABLESPACE
postgres=# select * from pg_tablespace;
spcname | spcowner | spcacl | spcoptions
------------+----------+--------+------------
pg_default | 10 | |
pg_global | 10 | |
tblspc2 | 10 | |
tblspc3 | 10 | |
tblspc4 | 10 | |
(5 rows)

postgres=#
データベースクラスタ内の pg_tblspc ディレクトリの中身を確認して、テーブルスペースが作成されたことを確認します。

[root@devsv03 pgsql]# ls -l /var/lib/pgsql/9.2/data/pg_tblspc
total 0
lrwxrwxrwx. 1 postgres postgres 17 Dec 23 18:38 16384 -> /disk/disk2/pgsql
lrwxrwxrwx. 1 postgres postgres 17 Dec 23 18:38 16385 -> /disk/disk3/pgsql
lrwxrwxrwx. 1 postgres postgres 17 Dec 23 18:38 16386 -> /disk/disk4/pgsql
[root@devsv03 pgsql]#
これでテーブルスペースの作成は完了です。

・データベースユーザの作成

Stadoから接続するためのデータベースユーザを作成します。

このユーザは「Stado→PostgreSQL」の間で接続する際のデータベースユーザになります(ユーザからは直接は見えないユーザです)。ここでは「stado」という名前でデータベースユーザを作成します。

[stado@devsv03 ~]$ createuser -d -E -U postgres -P stado
Enter password for new role:
Enter it again:
[stado@devsv03 ~]$ psql -h localhost -U stado postgres
psql (9.2.2)
Type "help" for help.

postgres=> \q
[stado@devsv03 ~]$
ここまででPostgreSQLのセットアップは完了です。

■Stadoのセットアップ


・Stadoの設定ファイル

/usr/local/stado/config にある stado_agent.config は以下を変更します。

xdb.coordinator.host=127.0.0.1
これは「エージェントノードから見たコーディネータノードのホスト名/IPアドレス」ですが、今回は一台のサーバの中でエージェントもコーディネータも動作させるため、上記のような設定となります。

同じディレクトリにある stado.config は、特に変更する項目はありません。ノードが4台であること、すべて 127.0.0.1 として扱うこと、として各種のパラメータが設定されていることを確認してください。

xdb.port=6453
xdb.maxconnections=10

xdb.default.dbusername=stado
xdb.default.dbpassword=stado

xdb.default.dbport=5432

xdb.default.threads.pool.initsize=2
xdb.default.threads.pool.maxsize=10

xdb.metadata.database=XDBSYS
xdb.metadata.dbhost=127.0.0.1

xdb.nodecount=4

xdb.node.1.dbhost=127.0.0.1
xdb.node.2.dbhost=127.0.0.1
xdb.node.3.dbhost=127.0.0.1
xdb.node.4.dbhost=127.0.0.1

xdb.coordinator.node=1
・メタデータベースとStado管理者ユーザの作成

ここまで設定ができたら、gs-createmdb.shコマンドを使ってStadoのメタデータベースを作成します。このメタデータベースでは、ノードの情報や、テーブルのパーティション情報などを管理するものです。

この時、同時にStadoの管理者ユーザの情報も(メタデータベース内に)作成されます。ここでは、Stadoの管理者ユーザを「stadoadm」として作成しています。

[stado@devsv03 ~]$ cd /usr/local/stado/bin
[stado@devsv03 bin]$ ./gs-createmddb.sh -u stadoadm -p password
Executed Statement: create table xsystablespaces ( tablespaceid serial, tablespacename varchar(255) not null, ownerid int not null, primary key(tablespaceid))
Executed Statement: create unique index idx_xsystablespaces_1 on xsystablespaces (tablespacename)
(...snip...)
Executed Statement: create unique index idx_xsyschecks_1 on xsyschecks (constid, seqno)
Executed Statement: alter table xsyschecks add foreign key (constid) references xsysconstraints (constid)
User stadoadm is created
[stado@devsv03 bin]$
いろいろなメッセージが出て、最後に「User [USER] is created」と出たらメタデータベースの作成は成功です。

■コーディネータプロセスの起動


メタデータベースの作成が終わったら、コーディネータプロセスを起動します。コーディネータプロセスの起動には gs-server.sh スクリプトを使います。

[stado@devsv03 bin]$ ./gs-server.sh
Starting....
[stado@devsv03 bin]$ ps -aef | grep stado
root 29892 29721 0 18:59 pts/3 00:00:00 su - stado
stado 29893 29892 0 18:59 pts/3 00:00:00 -bash
stado 30045 1 2 19:06 pts/3 00:00:00 java -classpath ../lib/stado.jar:../lib/log4j.jar:../lib/postgresql.jar: -Xms512M -Xmx512M -Xss256K -Dconfig.file.path=../config/stado.config org.postgresql.stado.util.XdbServer
postgres 30064 29604 0 19:06 ? 00:00:00 postgres: stado XDBSYS 127.0.0.1(48401) idle
stado 30099 29893 1 19:06 pts/3 00:00:00 ps -aef
stado 30100 29893 0 19:06 pts/3 00:00:00 grep stado
[stado@devsv03 bin]$
Javaのプロセスが起動したら成功です。

■エージェントプロセスの起動


コーディネータの起動ができたら、次にエージェントプロセスの起動を行います。

エージェントプロセスは gs-agent.sh スクリプトで行い、-nオプションでStadoクラスタ内におけるエージェントの番号を指定します。

[stado@devsv03 bin]$ ./gs-agent.sh -n 4
Starting....
[stado@devsv03 bin]$ ps -aef | grep stado
root 29892 29721 0 18:59 pts/3 00:00:00 su - stado
stado 29893 29892 0 18:59 pts/3 00:00:00 -bash
stado 30045 1 0 19:06 pts/3 00:00:00 java -classpath ../lib/stado.jar:../lib/log4j.jar:../lib/postgresql.jar: -Xms512M -Xmx512M -Xss256K -Dconfig.file.path=../config/stado.config org.postgresql.stado.util.XdbServer
postgres 30064 29604 0 19:06 ? 00:00:00 postgres: stado XDBSYS 127.0.0.1(48401) idle
stado 30112 1 4 19:07 pts/3 00:00:00 java -classpath ../lib/stado.jar:../lib/log4j.jar:../lib/postgresql.jar: -Xms256M -Xmx256M -Dconfig.file.path=../config/stado_agent.config org.postgresql.stado.util.XdbAgent -n 4
stado 30135 29893 1 19:07 pts/3 00:00:00 ps -aef
stado 30136 29893 0 19:07 pts/3 00:00:00 grep stado
[stado@devsv03 bin]$

■ユーザーデータベースの作成


エージェントプロセスが起動したら、ユーザデータベースを作成することができます。

gs-createdb.shコマンドを使って、ユーザデータベースを作成します(メタデータベースを作成するコマンド gs-createmdb.sh とは違うコマンドであることに注意してください。名前が似ていますが)

[stado@devsv03 bin]$ ./gs-createdb.sh -d testdb -u stadoadm -p password -n 1,2,3,4
OK
[stado@devsv03 bin]$
ユーザデータベースを作成したら、gs-cmdline.shコマンドでデータベースの状態を確認します。

[stado@devsv03 bin]$ ./gs-cmdline.sh -d testdb -u stadoadm -p password

Stado -> show databases;
+------------------------------+
| DATABASE | STATUS | NODES |
+------------------------------+
| testdb | Started | 1,2,3,4 |
+------------------------------+
1 row(s).

Stado ->
[stado@devsv03 bin]$
show databasesコマンドの結果を見ると、ここで作成したデータベースtestdbは、ステータスは利用開始されており、ノードは1から4に配置されていることが分かります。

■パーティションのテーブルスペースへの移動


データベースの一覧を見ると、今回作成したユーザデータベースtestdbが4つのパーティションを持っていることが分かります。gs-createdb.shコマンドを使ってStado上で作成した "testdb" というデータベースは、実際には "__testdb__N1" から "__testdb__N4" というパーティションに分割されています。

ここで、これらパーティションのテーブルスペースを見ると、すべて同じデフォルトのテーブルスペースに配置されていることが分かります。

postgres=> SELECT datname,dattablespace,spcname
postgres-> FROM pg_database d LEFT OUTER JOIN pg_tablespace s
postgres-> ON d.dattablespace=s.oid;
datname | dattablespace | spcname
--------------+---------------+------------
template1 | 1663 | pg_default
template0 | 1663 | pg_default
postgres | 1663 | pg_default
XDBSYS | 1663 | pg_default
__testdb__N1 | 1663 | pg_default
__testdb__N2 | 1663 | pg_default
__testdb__N3 | 1663 | pg_default
__testdb__N4 | 1663 | pg_default
(8 rows)

postgres=>
このままでは、すべてのパーティションが単一のディスク上に配置されてしまってクエリのI/O処理が分散されませんので、先ほど作成したテーブルスペースにこれらのデータベースパーティションを移動させます。

まずは、gs-dbstop.sh コマンドを使って一旦データベースを停止させます。(内部的にStadoのコーディネータプロセスからPostgreSQLインスタンスへの切断を終了します)

[stado@devsv03 bin]$ ./gs-dbstop.sh -u stadoadm -p password -d testdb
Database(s) testdb stopped.
[stado@devsv03 bin]$
データベースを停止したら、各パーティションをそれぞれのテーブルスペースに移動させます。

[stado@devsv03 bin]$ psql -U postgres postgres
psql (9.2.2)
Type "help" for help.

postgres=# ALTER DATABASE "__testdb__N2" SET TABLESPACE tblspc2;
ALTER DATABASE
postgres=# ALTER DATABASE "__testdb__N3" SET TABLESPACE tblspc3;
ALTER DATABASE
postgres=# ALTER DATABASE "__testdb__N4" SET TABLESPACE tblspc4;
ALTER DATABASE
postgres=# SELECT datname,dattablespace,spcname
postgres-# FROM pg_database d LEFT OUTER JOIN pg_tablespace s
postgres-# ON d.dattablespace=s.oid;
datname | dattablespace | spcname
--------------+---------------+------------
template1 | 1663 | pg_default
template0 | 1663 | pg_default
postgres | 1663 | pg_default
XDBSYS | 1663 | pg_default
__testdb__N1 | 1663 | pg_default
__testdb__N2 | 16384 | tblspc2
__testdb__N3 | 16385 | tblspc3
__testdb__N4 | 16386 | tblspc4
(8 rows)

postgres=# \q
[stado@devsv03 bin]$ ./gs-dbstart.sh -u stadoadm -p password -d testdb
Database(s) testdb started.
[stado@devsv03 bin]$
テーブルスペースへの移動が終わったら、最後にgs-dbstart.shコマンドを使ってデータベースを再開させます。

■ユーザデータベースへの接続


ここまで終われば、Stadoのセットアップは完了です。Stadoのコーディネータプロセスにpsqlコマンドを使って通常のPostgreSQLと同じように接続することができます。(ポート番号はstado.confgで指定したポート番号、ユーザはメタデータベース作成時に指定したユーザになります)

[stado@devsv03 bin]$ psql -p 6453 -h localhost -U stadoadm testdb
Password for user stadoadm:
psql (9.2.2, server 9.0.1)
WARNING: psql version 9.2, server version 9.0.
Some psql features might not work.
Type "help" for help.

testdb=>

■テーブルの作成とデータのロード


それでは、実際にユーザデータベース testdb の中にテーブルを作ってみます。

パーティションに分割させずにすべてのノードに持たせるテーブルはCREATE TABLE文に "REPLICATED" の修飾子を付加します。

CREATE TABLE orders (
o_orderkey INTEGER,
o_custkey INTEGER,
o_orderstatus CHAR(1),
o_totalprice REAL,
o_orderDATE DATE,
o_orderpriority CHAR(15),
o_clerk CHAR(15),
o_shippriority INTEGER,
o_comment VARCHAR(79)
)
REPLICATED;
パーティション分割させるテーブルは、CREATE TABLE文でパーティションキーを指定して "PARTITIONING KEY ... ON ALL" の修飾子を付加します。

CREATE TABLE orders2 (
o_orderkey INTEGER,
o_custkey INTEGER,
o_orderstatus CHAR(1),
o_totalprice REAL,
o_orderDATE DATE,
o_orderpriority CHAR(15),
o_clerk CHAR(15),
o_shippriority INTEGER,
o_comment VARCHAR(79)
)
PARTITIONING KEY o_orderkey ON ALL;
以下は、実際に CREATE TABLE 文を記述したスクリプトを実行してテーブルを作成している様子です。

testdb=> \i create_tables.sql
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
testdb=> \d+
List of relations
Schema | Name | Type | Owner | Size | Description
--------+-----------+-------+-------+---------+-------------
public | lineitem | table | stado | 0 bytes |
public | lineitem2 | table | stado | 0 bytes |
public | orders | table | stado | 0 bytes |
public | orders2 | table | stado | 0 bytes |
(4 rows)

testdb=>
テーブルを作成した後、gs-cmdline.shコマンドを起動して show tables コマンドを実行すると、作成したテーブルがどのように配置されているかを確認することができます。

以下の例では、ordersテーブルとlineitemテーブルはレプリケーションテーブルとして全4ノードに同じものを配置、orders2テーブルとlineitem2テーブルはパーティションテーブルとして、それぞれo_orderkeyカラムとl_orderkeyカラムをパーティションキーとしてハッシュ分割して4分割されていることが分かります。

[stado@devsv03 ~]$ cd /usr/local/stado/bin/
[stado@devsv03 bin]$ ./gs-cmdline.sh -u stadoadm -p password -d testdb

Stado -> show tables;
+-----------------------------------------------------+
| TABLE | TABLE_PARTITIONING_COLUMN | TABLE_NODES |
+-----------------------------------------------------+
| lineitem | | 1,2,3,4 |
| lineitem2 | l_orderkey | 1,2,3,4 |
| orders | | 1,2,3,4 |
| orders2 | o_orderkey | 1,2,3,4 |
+-----------------------------------------------------+
4 row(s).

Stado ->
データのローディングにはCOPYコマンドを使うことができます。

以下は、COPYコマンドを含むスクリプトを実行してデータをロードしている様子です。データロード実行後にテーブルサイズが大きくなっていることが分かります。

testdb=> \i load_tables.sql
Timing is on.
Time: 162623.103 ms
Time: 64810.198 ms
Time: 763729.597 ms
Time: 320365.227 ms
testdb=> \d+
List of relations
Schema | Name | Type | Owner | Size | Description
--------+-----------+-------+-------+---------+-------------
public | lineitem | table | stado | 8326 MB |
public | lineitem2 | table | stado | 2081 MB |
public | orders | table | stado | 1979 MB |
public | orders2 | table | stado | 495 MB |
(4 rows)

testdb=>

■クエリの実行


データをロードしたテーブルには、psqlコマンドを使って通常のPostgreSQLと同じようにクエリを実行することができます。

testdb=> select count(*) from orders;
count(*)
----------
15000000
(1 row)

Time: 1870.665 ms
testdb=> select count(*) from orders2;
count(*)
----------
15000000
(1 row)

Time: 650.026 ms
testdb=>

■まとめ


今回は、PostgreSQLを使ってMPPを実現するミドルウェア「Stado」について、その導入方法をご紹介してきました。(かなり駆け足になってしまいましたが)

BigDataやデータ分析の重要性が語られる時代になってきました。巷ではNoSQLなどの「新しいテクノロジー」が耳目を集めていますが、エンジニアの持っているスキルや業務データとの親和性を考えると、RDBMSが活躍できる余地はまだまだあるように(個人的には)感じています。

興味を持たれましたら、ぜひ試してみていただければと思います。

では、Happy Holidays!

■参考資料


Stado - The Open Source MPP Solution | StormDB
http://www.stormdb.com/community/stado

Stadoマニュアル 日本語(一部)対訳版
http://www.uptime.jp/go/stado/

PostgreSQL並列分散ミドルウェア「Stado」の紹介と検証報告
http://www.uptime.jp/ja/resources/techdocs/2012/07/stado/

【資料公開】「“今そこにある危機”を捉える ~ pg_stat_statements revisited」

$
0
0
2013年2月16日に開催された「PostgreSQLアンカンファレンス」でのセッション「“今そこにある危機”を捉える ~ pg_stat_statements revisited」で使ったスライドを公開しました。

SQLのパフォーマンス分析、チューニングに今や不可欠なpg_stat_statementsビューの使い方を簡単に解説した資料です。


当日、セッションに参加している方に聞いてみたら、「pg_stat_statementsを知らなかった」とか「知っていたけど使ったことがなかった」という方も多くいましたので、改めてぜひ見てみていただければと思います。

資料の中で便利スクリプトも紹介していますので、そちらも併せてどうぞ。

※関連エントリ
実行が遅いSQL文をpg_stat_statementsで抽出する

【9.3新機能チェック】マテリアライズドビューを試してみる

$
0
0
昨日、PostgreSQLの次期リリースである9.3のソースコードに、マテリアライズドビューのコードが追加されました。

pgsql: Add a materialized view relations.
http://www.postgresql.org/message-id/E1UCJDN-00042x-0w@gemulon.postgresql.org

PostgreSQLの開発者Wikiによると、マテリアライズドビューはもっとも要望の多かった機能のようです。

Materialized Views - PostgreSQL wiki
http://wiki.postgresql.org/wiki/Materialized_Views

今回は、このマテリアライズドビューがどのようなものなのか、そしてどのように使えるのかを見てみます。

■マテリアライズドビューとは


「マテリアライズドビュー」とは、特に集約や集計系の処理をする際に使われる機能で、ビューから取得できるデータの実体を持つ(materialized)ビューです。

通常、ビューというのは「見え方を定義する」だけですので、ビューに対する参照処理を行うと、その都度、元のテーブルに対してSQLの参照処理が行われることになります。

しかし、集計系や集約系のビューの場合、参照する都度、元テーブルに対してSQLが実行され、非常に時間がかかることになります。

そのため、「ビューから取得できるデータを実体として保持しておく」ための機能として「マテリアライズドビュー」という機能が考えられました。

簡単に言うと、ビューから取得するデータをキャッシュしておく機能と考えれば良いでしょう。そのため、ビューへの問い合わせを非常に高速に行うことができるようになります。

マテリアライズドビュー - Wikipedia

■マテリアライズドビューを作成する


それでは、実際にマテリアライズドビューを使ってみます。

今回は、PostgreSQLのDWH系ワークロードツールであるDBT-3のスキーマを使い、月別の売り上げを顧客ごとに集計する以下のクエリをマテリアライズドビューとして定義することを考えてみます。

select c_name,
date_trunc('month', o_orderdate),
sum(o_totalprice)::numeric
from orders o, customer c
where o.o_custkey = c.c_custkey
group by 1, 2;
このクエリのEXPLAINを取得すると402055程度となっています。

dbt3=# explain select c_name,
date_trunc('month', o_orderdate),
sum(o_totalprice)::numeric
from orders o, customer c
where o.o_custkey = c.c_custkey
group by 1, 2;
QUERY PLAN
----------------------------------------------------------------------------------------------------
GroupAggregate (cost=360805.98..402055.98 rows=1500000 width=27)
-> Sort (cost=360805.98..364555.98 rows=1500000 width=27)
Sort Key: c.c_name, (date_trunc('month'::text, (o.o_orderdate)::timestamp with time zone))
-> Hash Join (cost=7785.00..99265.00 rows=1500000 width=27)
Hash Cond: (o.o_custkey = c.c_custkey)
-> Seq Scan on orders o (cost=0.00..40326.00 rows=1500000 width=12)
-> Hash (cost=5031.00..5031.00 rows=150000 width=23)
-> Seq Scan on customer c (cost=0.00..5031.00 rows=150000 width=23)
(8 rows)

dbt3=#
このクエリをマテリアライズドビューとして定義します。マテリアライズドビューを作成するにはCREATE MATERIALIZED VIEWを使います。

PostgreSQL: Documentation: devel: CREATE MATERIALIZED VIEW
http://www.postgresql.org/docs/devel/static/sql-creatematerializedview.html

dbt3=# create materialized view customer_monthly_totalprice
dbt3-# as select c_name,
dbt3-# date_trunc('month', o_orderdate),
dbt3-# sum(o_totalprice)::numeric
dbt3-# from orders o, customer c
dbt3-# where o.o_custkey = c.c_custkey
dbt3-# group by 1, 2;
SELECT 1353175
dbt3=#
上記のクエリによって、customer_monthly_totalpriceというマテリアライズドビューが作成されました。

dbt3=# select relname,relkind from pg_class where relname like 'cust%';
relname | relkind
-----------------------------+---------
customer | r
customer_monthly_totalprice | m
(2 rows)

dbt3=#

■マテリアライズドビューを使って検索する


それでは作成したマテリアライズドビューに対してクエリを実行してみます。

作成したマテリアライズドビューに対してフルスキャンを行うと、以下のように実行コストは25188程度となります。

dbt3=# explain select * from customer_monthly_totalprice;
QUERY PLAN
--------------------------------------------------------------------------------------
Seq Scan on customer_monthly_totalprice (cost=0.00..25188.75 rows=1353175 width=34)
(1 row)

dbt3=#
ビューを定義する前の元クエリの実行コストが402055でしたので、マテリアライズドビューを作成することによって集約処理をかなり高速化できていることが分かります。

このように、通常のビューと違って「データの実体」をキャッシュのように持っていますので、マテリアライズドビューを使うとクエリを高速化することができるようになります。

■マテリアライズドビューを更新する


マテリアライズドビューは、ビューをキャッシュするような機能ですので、元テーブルが更新された場合には更新されなければなりません。

ここでは、テーブルの元データを削除した際に、マテリアライズドビューがどのように動作するのかを見てみます。

まず、マテリアライズドビューで取得できるデータのうち、顧客名 'Customer#000000007' の注文データを削除してみます。

dbt3=# select * from customer_monthly_totalprice where c_name='Customer#000000007';
c_name | date_trunc | sum
--------------------+------------------------+---------
Customer#000000007 | 1992-03-01 00:00:00+00 | 322432
Customer#000000007 | 1992-04-01 00:00:00+00 | 434825
Customer#000000007 | 1993-06-01 00:00:00+00 | 501704
Customer#000000007 | 1993-11-01 00:00:00+00 | 80777.5
Customer#000000007 | 1994-02-01 00:00:00+00 | 176604
Customer#000000007 | 1994-12-01 00:00:00+00 | 192318
Customer#000000007 | 1995-09-01 00:00:00+00 | 327616
Customer#000000007 | 1995-10-01 00:00:00+00 | 190890
Customer#000000007 | 1996-06-01 00:00:00+00 | 79104.5
Customer#000000007 | 1997-01-01 00:00:00+00 | 181378
Customer#000000007 | 1997-02-01 00:00:00+00 | 151988
Customer#000000007 | 1998-02-01 00:00:00+00 | 31698.3
Customer#000000007 | 1998-07-01 00:00:00+00 | 286525
(13 rows)

dbt3=# delete from orders where o_custkey in ( select c_custkey from customer where c_name = 'Customer#000000007' );
DELETE 16
元テーブル orders からレコードを削除した後、再度マテリアライズドビューからデータを取得してみます。

dbt3=# select * from customer_monthly_totalprice where c_name='Customer#000000007';
c_name | date_trunc | sum
--------------------+------------------------+---------
Customer#000000007 | 1992-03-01 00:00:00+00 | 322432
Customer#000000007 | 1992-04-01 00:00:00+00 | 434825
Customer#000000007 | 1993-06-01 00:00:00+00 | 501704
Customer#000000007 | 1993-11-01 00:00:00+00 | 80777.5
Customer#000000007 | 1994-02-01 00:00:00+00 | 176604
Customer#000000007 | 1994-12-01 00:00:00+00 | 192318
Customer#000000007 | 1995-09-01 00:00:00+00 | 327616
Customer#000000007 | 1995-10-01 00:00:00+00 | 190890
Customer#000000007 | 1996-06-01 00:00:00+00 | 79104.5
Customer#000000007 | 1997-01-01 00:00:00+00 | 181378
Customer#000000007 | 1997-02-01 00:00:00+00 | 151988
Customer#000000007 | 1998-02-01 00:00:00+00 | 31698.3
Customer#000000007 | 1998-07-01 00:00:00+00 | 286525
(13 rows)

この段階では、まだマテリアライズドビューが以前のデータを保持しているために、ordersテーブルから元データが削除されたことが反映されていません。

ここで、マテリアライズドビューを更新します。マテリアライズドビューを更新するためにはREFRESH MATERIALIZED VIEWを使います。

PostgreSQL: Documentation: devel: REFRESH MATERIALIZED VIEW
http://www.postgresql.org/docs/devel/static/sql-refreshmaterializedview.html

dbt3=# refresh materialized view customer_monthly_totalprice;
REFRESH MATERIALIZED VIEW
dbt3=# select * from customer_monthly_totalprice where c_name='Customer#000000007';
c_name | date_trunc | sum
--------+------------+-----
(0 rows)

dbt3=#
マテリアライズドビューを更新すると、元テーブル orders からレコードが削除されたことが反映されて、マテリアライズドビューからデータが消えました。

■まとめ


以上、簡単ではありますが次期バージョン9.3に向けてコミットされたての機能である「マテリアライズドビュー」を試してみました。

見てきたように、マテリアライズドビューは集約や集計系の処理のパフォーマンスを大幅に向上させる可能性を秘めた強力な機能です。

私自身も、最近はデータ分析のプラットフォームとしてPostgreSQLを使うケースが増えており、非常に楽しみにしている機能の一つです。

興味を持った方は、ぜひご自身でもトライしてみていただければと思います。

では、また。
Viewing all 94 articles
Browse latest View live