2012年12月15日

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

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モジュールを御紹介しました。

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

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

ではまた。

0 件のコメント:

コメントを投稿