2015年4月20日月曜日

SQLでWHERE区に複数列を指定

0 コメント
PostgreSQLで、次のようなテーブルを作成し、データを入れます。
ここで、WHERE区にlast_nameとfirst_nameに条件を設定してデータを取得します。
結果は次のようになります。

これと同様のことをOracleで試してみました。
結果は次のようになります。
エラーとなりました。
そこで、WHERE区を
と修正して実行してみると
正しく動作しました。
Oracleでは、値を入れる辺を括弧で括ってやるとうまくいくようです。
元々、次のように
IN区で複数の条件を指定するように作られているのかな?っという気がしますが、どうなんでしょう。

2014年10月24日金曜日

UNIQUE制約でのNULLの扱い

0 コメント
PostgreSQLで、テーブルの複数列でUNIQUE制約をかけたとき、その中にNULLを許可する列があった場合の挙動について。

次のテーブルを作成します。
このテーブルに次のINSERT文でデータを入れてみます。
結果は次のようになります。
id=3は、UNIQUE制約違反でINSERTに失敗しています。
ここで引っかかったのはid=5がUNIQUE制約違反にならず、INSERTに成功しているところです。

私のイメージだと、NULLは何もないという感覚だったので、id=4とid=5のデータはUNIQUE制約に違反してid=5はINSERTに失敗すると思っていました。
調べてみると、PostgreSQLの一意制約の解説では、
一般に、制約の対象となる列について同じ値を持つ行が、テーブル内に1行を上回る場合は、一意性制約違反になります。 しかし、この比較では2つのNULL値は等価とはみなされません。 つまり、一意性制約があったとしても、制約対象の列の少なくとも1つにNULL値を持つ行を複数格納することができるということです。 この振舞いは標準SQLに準拠していますが、この規則に従わないSQLデータベースがあることを聞いたことがあります。 ですから、移植する予定のアプリケーションを開発する際には注意してください。
とあり、NULLは等価ではないとあり、上の例ではid=4とid=5はUNIQUE制約に違反せず、INSERTできるということになります。
文中の
この規則に従わないSQLデータベースがあることを聞いたことがあります。
は、MicrosoftのSQL Serverが該当するみたいです。
OracleやMySQLなどはPostgreSQLと同じ挙動をするんだとか。
移植の際などには気をつける必要があります。

対策としては、std列をNOT NULLとして、id=4,id=5には''を入れるようにすれば、意図した動きになります。

では、ADO.NETのDataTableはどうなんでしょう?
こんなサンプルを作って試してみました。 結果はこうなりました。 PostgreSQLとは異なり、NULLを同じ値として扱っているようです。
PostgreSQLにNULLを許可した列を含むUNIQUE制約を持つテーブルからADO.NETのDataTableにデータを取り込んだ場合、エラーになる可能性が考えられます。
UNIQUE制約をかける列は、NOT NULLにするべきでしょうね。

2014年5月15日木曜日

ubuntu14.04にoracle-xe 11gをインストール

1 コメント
ubuntu14.04 LTS ServerにOracle Database Express Edition 11g Release2をインストールしたときの手順のまとめ。

こちらのサイトを参考にさせていただきました。というか、パクリです。

まず、Oracle-XE-11.2.0のLinux x64版をダウンロードして、ubuntuにコピーしておく。

必要なパッケージをインストール。
sudo apt-get install alien libaio1 unixodbc unzip

oracle-xeのパッケージを解凍。
unzip oracle-xe-11.2.0-1.0.x86_64.rpm.zip

rpmパッケージをdebパッケージに変換。
cd Disk1/
sudo alien --to-deb --scripts oracle-xe-11.2.0-1.0.x86_64.rpm

パッケージをインストール。
/sbin/chkconfig をviで編集し
sudo vi /sbin/chkconfig
以下の内容を記述。
#!/bin/bash
# Oracle 11gR2 XE installer chkconfig hack for Debian by Dude
file=/etc/init.d/oracle-xe
if [[ ! `tail -n1 $file | grep INIT` ]]; then
   echo >> $file
   echo '### BEGIN INIT INFO' >> $file
   echo '# Provides:             OracleXE' >> $file
   echo '# Required-Start:       $remote_fs $syslog' >> $file
   echo '# Required-Stop:        $remote_fs $syslog' >> $file
   echo '# Default-Start:        2 3 4 5' >> $file
   echo '# Default-Stop:         0 1 6' >> $file
   echo '# Short-Description:    Oracle 11g Express Edition' >> $file
   echo '### END INIT INFO' >> $file
fi
update-rc.d oracle-xe defaults 80 01
configureのawkのパスが違うのでシンボリックリンクを作成。
sudo ln -s /usr/bin/awk /bin/awk
ファイルのパーミッションを変更。
sudo chmod 755 /sbin/chkconfig

パッケージのインストール。
sudo dpkg --install ./oracle-xe_11.2.0-2_amd64.deb

/etc/init.d/oracle-xeの /var/lock/subsys/ を /var/lock/ に変更。
sudo vi /etc/init.d/oracle-xe


※私の試した環境(仮想マシンに1Gのメモリを設定)では、このままconfigureを実行してうまくいきましたが、configureでエラーが出るような場合は、以下のパラメータを変更してみてください。
/u01/app/oracle/product/11.2.0/xe/config/scripts/init.ora
/u01/app/oracle/product/11.2.0/xe/config/scripts/initXETemp.ora
の2つのファイルの、memory_targetをコメントにして、pga_aggregate_targetとsga_targetを設定します。
sudo vi /u01/app/oracle/product/11.2.0/xe/config/scripts/init.ora
sudo vi /u01/app/oracle/product/11.2.0/xe/config/scripts/initXETemp.ora
以下の内容を記述。
#memory_target=418381824
pga_aggregate_target=200540160
sga_target=601620480


configureを実行。
sudo /etc/init.d/oracle-xe configure

.profileを編集して、oracleの環境変数が設定されるようにする。
vi ~/.profile
以下の内容を追加する。
source /u01/app/oracle/product/11.2.0/xe/bin/oracle_env.sh

編集した.profileの内容を反映する。
source ~/.profile

oracleに接続できるか確認する。
sqlplus system/[設定したパスワード]@XE

これでoracleに接続できたらOK!

2013年3月29日金曜日

ADO.NETのDataTableクラスのCaseSensitiveプロパティ

0 コメント
ADO.NETのDataTableで、プライマリキーに文字列型の列を指定したとき、その列に例えば、"A"という値と"a"という値が入るとエラーとなる。
大文字/小文字の区別するためには、プロパティのCaseSensitiveをtrueにするとよい。

2013年1月11日金曜日

ubuntuにredmineをapt-getでインストール

0 コメント
ubuntu 12.04 LTS Serverにapt-getでredmineを入れてみました。

データベースはPostgreSQLを使用します。
sudo apt-get install postgresql libapache2-mod-passenger redmine-pgsql redmine

apache2の設定は自分でやらないといけないみたいなので、Oh!Happy.JPさんを参考に(というかそのまま真似して)設定しました。

まず、/usr/share/redmine/publicを/var/www/redmineにシンボリックリンク。
sudo ln -s /usr/share/redmine/public /var/www/redmine
/etc/apache2/mod-available/passenger.confに、PassengerDefaultUserの設定を追加。
PassengerDefaultUser www-data
/etc/apache2/sites-enabled/000-defaultに、/var/www/redmineのディレクトリ情報を追加。
<Directory /var/www/redmine>
    RailsBaseURI /redmine
    PassengerResolveSymlinksInDocumentRoot on
</Directory>
passegerの有効化。
cd /etc/apache2/mods-available
sudo a2enmod passenger
apache2の再起動。
sudo /etc/init.d/apache2 restart
これでブラウザから、http://サーバー名/redmine/にアクセスすると、redmineのトップページが開かれました。

2012年12月7日金曜日

PostgreSQL9.2で同期レプリケーション

0 コメント
次のような環境を構築してみようと思います。
各サーバーの構成
名前役割
pgpoolpgpool2で負荷分散と障害時のフェイルオーバー
pgsql1PostgreSQL9.2で同期レプリケーションのプライマリ
pgsql2PostgreSQL9.2で同期レプリケーションのセカンダリ
OSはすべてubuntu 12.04 LTS Serverを使います。

[PostgreSQLのセットアップ]

まず、各サーバーにPostgreSQLをソースからビルドしてセットアップします。
ビルドに必要なパッケージをインストールします。
sudo apt-get install build_essential libreadline6-dev zlib1g-dev
PostgreSQLユーザーとディレクトリを作成します。
sudo adduser postgres
sudo mkdir /usr/local/pgsql
sudo chown postgres:postgres /usr/local/pgsql
PostgreSQLのソースをダウンロードします。
cd /usr/local/src
sudo wget http://ftp.postgresql.org/pub/source/v9.2.1/postgresql-9.2.1.tar.gz
sudo tar zxvf postgresql-9.2.1.tar.gz
sudo chown -R postgres:postgres postgresql-9.2.1
PostgreSQLのソースをビルドします。
su - postgres
cd /usr/local/source/postgresql-9.2.1
./configure
make
make install
postgresユーザーの.profileに
# User specific environment and startup programs
PGHOME=/usr/local/pgsql
PGDATA=$PGHOME/data
PGLIB=$PGHOME/lib
PATH=$PATH:$HOME/bin:$PGHOME/bin
export PGHOME PGDATA PGLIB PATH
を追加して
source ~/.profile
で、環境変数に反映します。
PostgreSQLの初期化を行います。
initdb
sudoの権限のあるユーザーに戻って、PostgreSQLの自動起動の設定を行います。
sudo vi /etc/ld.so.conf
で、ファイルの末尾に /usr/local/pgsql/lib を追加して、
sudo ldconfig -v
を実行します。
PostgreSQL起動スクリプトをコピーします。
sudo cp /usr/local/src/postgresql-9.2.1/contrib/start-scripts/linux /etc/init.d/postgresql
sudo chmod 755 /etc/init.d/postgresql
sudo update-rc.d postgresql start 90 2 3 4 5 . stop 10 0 1 6 .
PostgreSQLを起動してみます。
sudo /etc/init.d/postgresql start
問題なく起動できたら、データベースのユーザーのpostgresにパスワードを設定します。
su - postgres
psql -c "alter user postgres with password 'postgresのパスワード';"

[下準備]

pgpoolから、pgsql1,pgsql2のコマンドを、パスワードなしのsshで実行できるようにします。
これはpgsql1かpgsql2で障害が発生した時、その対応処理をpgpoolから実行させるためです。
まず、pgpoolで
su - postgres
mkdir .ssh
chmod 700 .ssh
ssh-keygen -t dsa
で、パスワードなしの鍵を作成し、作成された公開鍵をpgsql1とpgsql2にコピーします。
scp .ssh/id_dsa.pub postgres@pgsql1:~/
scp .ssh/id_dsa.pub postgres@pgsql2:~/
次に、pgsql1とpgsql2の両方で、
su - postgres
mkdir .ssh
chmod 700 .ssh
cd .ssh
cat ~/id_dsa.pub >> authorized_keys
chmod 600 authorized_keys
rm ~/id_dsa.pub
として、公開鍵を追加します。
試しに、pgpoolからpgsql1とpgsql2のコマンドを実行してみて
ssh postgres@pgsql1 "/usr/local/pgsql/bin/psql -l"
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
 postgres  | postgres | UTF8     | ja_JP.UTF-8 | ja_JP.UTF-8 |
 template0 | postgres | UTF8     | ja_JP.UTF-8 | ja_JP.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | ja_JP.UTF-8 | ja_JP.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
(3 rows)

ssh postgres@pgsql2 "/usr/local/pgsql/bin/psql -l"
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
 postgres  | postgres | UTF8     | ja_JP.UTF-8 | ja_JP.UTF-8 |
 template0 | postgres | UTF8     | ja_JP.UTF-8 | ja_JP.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | ja_JP.UTF-8 | ja_JP.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
(3 rows)
と表示されれば成功です。

[PostgreSQLの同期レプリケーションの設定]

pgsql1をプライマリ、pgsql2をセカンダリという構成で同期レプリケーションするように設定します。

まず、プライマリのpgsql1の設定を変更します。
su - postgres
cd /usr/local/pgsql/data
vi postgresql.conf
postgresql.confに以下の設定を追加します。
listen_addresses = '*'
wal_level = hot_standby
archive_mode = on
archive_command= '/bin/cp %p /usr/local/pgsql/data/pg_archive/%f'
max_wal_senders = 3
wal_keep_segments = 5
synchronous_standby_names = 'pgsql2'
postgresql.confをコピーして、同期レプリケーション用(postgresql.conf.sync)と非同期レプリケーション用(postgresql.conf.async)を作成します(これはフェイルオーバーした時に使用します)。
cp postgresql.conf postgresql.conf.sync
cp postgresql.conf postgresql.conf.async
非同期レプリケーション用のpostgresql.conf.asyncのsynchronous_standby_namesをコメントアウトして無効にします(行を削除してもいいです)。
#synchronous_standby_names = 'pgsql2'
次に、pgsql1にpgsql2からアクセスできるようにpg_hba.confを編集します。
vi pg_hba.conf
pg_hba.confに以下の内容を追加します。
host    replication     postgres        192.168.0.0/24        trust
※IPアドレスはサーバーのセグメントに合わせます。
続けて、アーカイブログのディレクトリを作成します。
mkdir -p pg_archive
chmod 700 pg_archive
sudoの権限のあるユーザーで、PostgreSQLを再起動します。
sudo /etc/init.d/posgresql restart

次にセカンダリのpgsql2の設定を変更します。
もし、PostgreSQLが起動しているなら停止します。
sudo /etc/init.d/postgresql stop
su - postgres
cd /usr/local/pgsql
rm -rf data
pg_basebackup -h pgsql1 -p 5432 -D /usr/local/pgsql/data --xlog --progress --verbose
cd data
vi postgresql.conf
postgresql.confに以下の設定を追加します。
#synchronous_standby_names = ''    #をつけてコメントにするか行を削除する
hot_standby = on
recovery.confを作成します。
cp ../share/recovery.conf.sample recovery.conf
vi recovery.conf
recovery.confに以下の設定を追加します。
standby_mode = on
primary_conninfo = 'host=pgsql1 port=5432 application_name=pgsql2'
sudoの権限のあるユーザーで、PostgreSQLを起動します。
sudo /etc/init.d/posgresql start

同期レプリケーションできているか確認してみます。
pgsql1で次のコマンドを実行して確認します。
su - postgres
psql -c "select application_name, state, sync_priority, sync_state from pg_stat_replication;"
 application_name |   state   | sync_priority | sync_state
------------------+-----------+---------------+------------
 pgsql2           | streaming |             1 | sync
(1 rows)
pgsql2のsync_stateがsyncになっていれば、同期レプリケーションで動作しています。
サンプルのデータベースを作成してみます。
createuser -P test1
createdb test1 -O test1 -E UTF8 -T template0
psql test1 -U test1 -c "create table table1 (id integer not null primary key, value text);"
pgsql2でデータベースとテーブルが作成されているか確認します。
su - postgres
psql -l
                                  List of databases
   Name    |  Owner   | Encoding |   Collate   |    Ctype    |   Access privileges
-----------+----------+----------+-------------+-------------+-----------------------
 postgres  | postgres | UTF8     | ja_JP.UTF-8 | ja_JP.UTF-8 |
 template0 | postgres | UTF8     | ja_JP.UTF-8 | ja_JP.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 template1 | postgres | UTF8     | ja_JP.UTF-8 | ja_JP.UTF-8 | =c/postgres          +
           |          |          |             |             | postgres=CTc/postgres
 test1     | test1    | UTF8     | ja_JP.UTF-8 | ja_JP.UTF-8 |
(4 rows)

psql test1 -U test1 -c "\dt;"
         List of relations
 Schema |  Name  | Type  |  Owner
--------+--------+-------+----------
 public | table1 | table | postgres
(1 row)

psql -c "select pg_is_in_recovery();"
 pg_is_in_recovery
-------------------
 t
(1 row)
データベースtest1とテーブルtable1がpgsql2にも作成されていればOKです。
また、pg_is_in_recovery()の結果が"t"(true)になっていれば、セカンダリとして動作していることが確認できます。

[pgpool-IIの設定]

まず、pgpoolサーバーにpgpool-IIをソースからビルドしてセットアップします。
pgpool-IIのディレクトリを作成します。
sudo mkdir /usr/local/pgpool2
sudo chown postgres:postgres /usr/local/pgpool2
cd /usr/local/src
sudo wget http://www.pgpool.net/download.php?f=pgpool-II-3.2.1.tar.gz
sudo tar zxvf pgpool-II-3.2.1.tar.gz
sudo chown -R postgres:postgres pgpool-II-3.2.1
PostgreSQLのソースをビルドします。
su - postgres
cd /usr/local/source/pgpool-II-3.2.1
./configure --prefix=/usr/local/pgpool2 -with-pgsql=/usr/local/pgsql
make
make install
pgpool-IIの設定を変更します。
cd /usr/local/pgpool2/etc
cp pcp.conf.sample pcp.conf
vi pcp.conf
pcp.confの末尾に、postgresユーザーのパスワードをMD5で暗号化したものを記述します。パスワードのMD5変換したものは、
/usr/local/pgpool2/bin/pg_md5 postgresのパスワード
で取得できます。これを
# USERID:MD5PASSWD
postgres:MD5変換されたパスワード
というように追加します。
次に、pgpool.confを設定します。
cp pgpool.conf.sample-stream pgpool.conf
vi pgpool.conf
pgpool.confに以下の設定を追加します。
listen_addresses = '*'
port = 9999
pid_file_name = '/usr/local/pgpool2/pgpool.pid'

failover_command = '/usr/local/pgpool2/etc/failover.sh %d "%h" %p %D %m %M "%H" %P %r %R'

# pgsql1
backend_hostname0 = 'pgsql1'
backend_port0 = 5432
backend_weight0 = 1
backend_data_directory0 = '/usr/local/pgsql/data'
backend_flag0 = 'ALLOW_TO_FAILOVER'
# pgsql2
backend_hostname1 = 'pgsql2'
backend_port1 = 5432
backend_weight1 = 1
backend_data_directory1 = '/usr/local/pgsql/data'
backend_flag1 = 'ALLOW_TO_FAILOVER&#8216;

sr_check_user = 'postgres'
sr_check_password = 'postgresのパスワード'
health_check_user = 'postgres'
health_check_password = 'postgresのパスワード'
recovery_user = 'postgres'
recovery_password = 'postgresのパスワード'
フェイルオーバー時のスクリプトを作成します。
vi failover.sh
failover.shに以下のようにスクリプトを記述します。
#!/bin/sh

failed_node_id=$1
failed_host_name=$2
failed_port=$3
failed_db_cluster=$4
new_master_id=$5
old_master_id=$6
new_master_host_name=$7
old_primary_node_id=$8
new_master_port=$9
new_master_db_cluster=$10
logfile=/usr/local/pgpool2/log/failover.log

echo "------------------------------------------------------------------" >> $logfile
date >> $logfile
echo "failed_node_id=$failed_node_id" >> $logfile
echo "failed_host_name=$failed_host_name" >> $logfile
echo "failed_port=$failed_port" >> $logfile
echo "failed_db_cluster=$failed_db_cluster" >> $logfile
echo "new_master_id=$new_master_id" >> $logfile
echo "old_master_id=$old_master_id" >> $logfile
echo "new_master_host_name=$new_master_host_name" >> $logfile
echo "old_primary_node_id=$old_primary_node_id" >> $logfile
echo "new_master_port=$new_master_port" >> $logfile
echo "new_master_db_cluster=$new_master_db_cluster" >> $logfile

if [ $new_master_id -eq -1 ]
then
    echo "unknown new_master_id=$new_master_id" >> $logfile
    exit 0
fi

if [ -z $new_master_host_name ]
then
    echo "unknown new_master_host_name=$new_master_host_name" >> $logfile
    exit 0
fi

if [ $failed_node_id = $old_primary_node_id ] # primary failed
then
    echo "secondary promote" >> $logfile
    /usr/bin/ssh postgres@$new_master_host_name "/usr/local/pgsql/bin/pg_ctl -D $new_master_db_cluster promote" >> $logfile 2>&1
else # secondary failed
    echo "primary async mode" >> $logfile
    /usr/bin/ssh postgres@$new_master_host_name "/bin/cp /usr/local/pgsql/data/postgresql.conf.async /usr/local/pgsql/data/postgresql.conf" >> $logfile 2>&1
    /usr/bin/ssh postgres@$new_master_host_name "/usr/local/pgsql/bin/pg_ctl reload -D $new_master_db_cluster" >> $logfile 2>&1
fi
ログを入れるディレクトリを作成します。
cd ..
mkdir log

pgsql1とpgsql2のpg_hba.confを編集して、pgpoolからアクセスできるようにします。
psql1とpsql2で、
su - postgres
cd /usr/local/pgsql/data
vi pg_hba.conf
pg_hba.confに以下の設定を追加します。
host    all             all             pgpoolのIPアドレス/32      password
pgsql1とpgsqlのPostgreSQLのパラメータを再読み込みします。
sudoの権限のあるユーザーで
sudo /etc/init.d/postgresql reload

pgpoolを起動します。
起動のためのスクリプトをpostgresqlの起動スクリプトを真似して次のように作ってみました。
#! /bin/sh

# Installation prefix
prefix=/usr/local/pgpool2

# Who to run the postmaster as, usually "postgres".  (NOT "root")
PGUSER=postgres

# Where to keep a log file
PGLOG="$prefix/log/serverlog"

# It's often a good idea to protect the postmaster from being killed by the
# OOM killer (which will tend to preferentially kill the postmaster because
# of the way it accounts for shared memory).  Setting the OOM_SCORE_ADJ value
# to -1000 will disable OOM kill altogether.  If you enable this, you probably
# want to compile PostgreSQL with "-DLINUX_OOM_SCORE_ADJ=0", so that
# individual backends can still be killed by the OOM killer.
#OOM_SCORE_ADJ=-1000
# Older Linux kernels may not have /proc/self/oom_score_adj, but instead
# /proc/self/oom_adj, which works similarly except the disable value is -17.
# For such a system, enable this and compile with "-DLINUX_OOM_ADJ=0".
#OOM_ADJ=-17

## STOP EDITING HERE

# The path that is to be used for the script
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin

# What to use to start up the pgpool.
DAEMON="$prefix/bin/pgpool"

# What to use to shut down the pgpool
PGPOOL="$prefix/bin/pgpool"

set &#8211;e

# Only start if we can find the pgpool.
test -x $DAEMON ||
{
        echo "$DAEMON not found"
        if [ "$1" = "stop" ]
        then exit 0
        else exit 5
        fi
}

# Parse command line parameters.
case $1 in
  start)
        echo -n "Starting pgpool-II: "
        test x"$OOM_SCORE_ADJ" != x && echo "$OOM_SCORE_ADJ" > /proc/self/oom_score_adj
        test x"$OOM_ADJ" != x && echo "$OOM_ADJ" > /proc/self/oom_adj
        su - $PGUSER -c "$DAEMON &" >>$PGLOG 2>&1
        echo "ok"
        ;;
  stop)
        echo -n "Stopping pgpool-II: "
        su - $PGUSER -c "$PGPOOL -m fast stop"
        echo "ok"
        ;;
  restart)
        echo -n "Restarting pgpool-II: "
        su - $PGUSER -c "$PGPOOL -m fast stop"
        test x"$OOM_SCORE_ADJ" != x && echo "$OOM_SCORE_ADJ" > /proc/self/oom_score_adj
        test x"$OOM_ADJ" != x && echo "$OOM_ADJ" > /proc/self/oom_adj
        su - $PGUSER -c "$DAEMON &" >>$PGLOG 2>&1
        echo "ok"
        ;;
 reload)
        echo -n "Reload pgpool-II: "
        su - $PGUSER -c "$PGPOOL reload"
        echo "ok"
        ;;
  *)
        # Print help
        echo "Usage: $0 {start|stop|reload|restart}" 1>&2
        exit 1
        ;;
esac

exit 0
これを /etc/init.d/pgpool に保存します。
※完全に理解できていない箇所もあるので、もし真似してやった方がいて何か問題が起きても自己責任でお願いします(^^;
ちなみにubuntuのpgpool2のパッケージをインストールした時に生成される /etc/init.d/pgpool2 は以下のようになっていました。
#! /bin/sh

### BEGIN INIT INFO
# Provides:          pgpool2
# Required-Start:    $remote_fs $syslog
# Required-Stop:     $remote_fs $syslog
# Should-Start:      postgresql
# Default-Start:     2 3 4 5
# Default-Stop:      0 1 6
# Short-Description: start pgpool-II
# Description: pgpool-II is a connection pool server and replication
#              proxy for PostgreSQL.
### END INIT INFO


PATH=/sbin:/bin:/usr/sbin:/usr/bin
DAEMON=/usr/sbin/pgpool
PIDFILE=/var/run/postgresql/pgpool.pid

test -x $DAEMON || exit 5

# Include pgpool defaults if available
if [ -f /etc/default/pgpool2 ] ; then
 . /etc/default/pgpool2
fi

OPTS=""
if [ x"$PGPOOL_LOG_DEBUG" = x"yes" ]; then
 OPTS="$OPTS -d"
fi

. /lib/lsb/init-functions


is_running() {
 pidofproc -p $PIDFILE $DAEMON >/dev/null
}


d_start() {
 if is_running; then
  :
 else
  su -c "$DAEMON -n $OPTS 2>&1 </dev/null | logger -t pgpool -p ${PGPOOL_SYSLOG_FACILITY:-local0}.info >/dev/null 2>&1 &" - postgres
 fi
}


d_stop() {
 killproc -p $PIDFILE $DAEMON -INT
 status=$?
 [ $status -eq 0 ] || [ $status -eq 3 ]
 return $?
}


case "$1" in
    start)
 log_daemon_msg "Starting pgpool-II" pgpool
 d_start
 log_end_msg $?
 ;;
    stop)
 log_daemon_msg "Stopping pgpool-II" pgpool
 d_stop
 log_end_msg $?
 ;;
    status)
 is_running
 status=$?
 if [ $status -eq 0 ]; then
  log_success_msg "pgpool-II is running."
 else
  log_failure_msg "pgpool-II is not running."
 fi
 exit $status
 ;;
    restart|force-reload)
 log_daemon_msg "Restarting pgpool-II" pgpool
 d_stop && sleep 1 && d_start
 log_end_msg $?
 ;;
    try-restart)
 if $0 status >/dev/null; then
  $0 restart
 else
  exit 0
 fi
 ;;
    reload)
 exit 3
 ;;
    *)
 log_failure_msg "Usage: $0 {start|stop|status|restart|try-restart|reload|force-reload}"
 exit 2
 ;;
esac
pgpoolに -n をつけて、デーモンとして走らせないようにしてるっぽいです。
こっちに合わせて書きなおしたほうがいいかもしれません。

pgpoolを自動起動するように設定します。
sudo chmod 755 /etc/init.d/pgpool
sudo update-rc.d pgpool start 91 2 3 4 5 . stop 11 0 1 6 .
pgpoolを実行します。
sudo /etc/init.d/pgpool start


ここまでで、設定は完了です。


[pgsql1を落とすテスト]

プライマリのpgsql1を停止させてみます。
su - postgres
pg_ctl -D /usr/local/pgsql/data -m immediate stop
適当なクライアントからアクセスしてみます。
psql -h pgpool -p 9999 -U test1 test1
psql: server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.
一旦エラーになるようです。
どうも、pgsql1を落としたあと、次にアクセスがきた時にフェイルオーバー処理が走るみたいです(ちょっと自信はありませんが)。
もう一度アクセスしてみると、
psql -h pgpool -p 9999 -U test1 test1
Password for user test1: test1のパスワード

test1=> select * from table1;
 id | value
----+-------
(0 row)

test1=> insert into table1 (id, value) values (1, 'aaaa');
INSERT 0 1
無事接続でき、更新クエリも処理されています。
もし、pgsql2がプライマリに昇格できていなければ、更新クエリはエラーになるはずなので、問題なくフェイルオーバー処理が実行されているようです。
念のため、pgsql2の状態を確認してみると
psql -c "select pg_is_in_recovery();"
 pg_is_in_recovery
-------------------
 f
(1 row)
pg_is_in_recovery()の結果が"f"(false)になっており、プライマリに昇格しています。

[pgsql2を落とすテスト]

※一旦pgsql1とpsql2を元に戻します。

セカンダリのpgsql2を停止させてみます。
su - postgresql
pg_ctl -D /usr/local/pgsql/data -m immediate stop
適当なクライアントからアクセスしてみます。
psql -h pgpool -p 9999 -U test1 test1
psql: server closed the connection unexpectedly
        This probably means the server terminated abnormally
        before or while processing the request.
※一旦エラーになるようです。
もう一度アクセスしてみると、
psql -h pgpool -p 9999 -U test1 test1
Password for user test1: test1のパスワード

test1=> select * from table1;
 id | value
----+-------
(0 row)

test1=> insert into table1 (id, value) values (1, 'aaaa');
INSERT 0 1
無事接続でき、更新クエリも処理されています。
もし、pgsql1が非同期レプリケーションに切り替わっていなければ、更新クエリはエラーになる(タイムアウトする?)はずなので、ちゃんとフェイルオーバー処理が実行されています。
念のため、pgsqlの/usr/local/pgsql/data/recovery.confを確認してみると、
#synchronous_standby_names = 'pgsql2'
のように、synchronous_standby_namesがコメントアウトされて無効になっているはずです。

長ーくなりましたが、これで同期レプリケーションのまとめはここまで。


もし、サーバーの構成で、セカンダリが複数台あったとき、pgpoolに設定したフェイルオーバースクリプトのsecondary failedの部分の処理は不要かと思います。
同期しているセカンダリが落ちたときは sync_state が potential になっているサーバーが、sync になるので問題ないはず。

2012年9月28日金曜日

PostgreSQLの列の型のtimestamp with time zoneとNpgsqlでのアクセス

0 コメント
昨日のエントリで、PostgreSQLのtimestamp型のwith time zoneについて色々試してみましたが、もう少し調べてayakobabaの日記というブログの[Postgresql]Postgresql のTimezoneというエントリを見つけました。

PostgreSQLのマニュアルにも記述があり、timestamp with time zone型の列にアクセスするとき、at time zone構文を使うことでタイムスタンプを異なる時間帯に変換できるようです。 例えば、こんな感じ。
そこで、昨日のコードを修正して、次のようにしてみました。
ポイントは、DataAdapterのSelectCommand,InsertCommand,UpdateCommandのそれぞれのSQLで、datetime2の列にアクセスするところで、at time zone構文を使っているところです。
SelectCommandでは、datetime2の値を取得するところで、
datetime2 at time zone interval '+09:00' as datetime2
となるようにしています。ここで'+09:00'はクライアントのタイムゾーンが東京の場合です。
この'+09:00'は、
DateTime tmpTime = new DateTime(2000, 1, 1);
TimeSpan diffUTC = tmpTime - tmpTime.ToUniversalTime();
string strDiffUTC = (diffUTC.TotalHours >= 0 ? "+" : "") + diffUTC.Hours.ToString("00") + ":" + diffUTC.Minutes.ToString("00");
の部分で、ある適当な時間(ここでは2000年1月1日0時0分0秒)から、その時間のUTCを引いた差から生成しています。
InsertCommandでは
:datetime2 at time zone interval '+09:00'
UpdateCommandでは
datetime2=:datetime2 at time zone interval '+09:00'
としています。

まず、クライアントPCのタイムゾーンを東京、サーバーのタイムゾーンも東京の状態で実行してみると
データの追加が終わったところ(にブレークポイントをいれて確認)で、コンソールに
id=1, datetime1=2012/10/01 9:00:00, datetime2=2012/10/01 9:00:00
と表示され、データベース側でtable1の内容を確認すると、
db1=> select * from table1;
 id |      datetime1      |       datetime2
----+---------------------+------------------------
  1 | 2012-10-01 09:00:00 | 2012-10-01 09:00:00+09
(1 row)
となっています。
そのまま処理を続けて、次の更新処理が終わったところで、コンソールに
id=1, datetime1=2012/12/01 9:00:00, datetime2=2012/12/01 9:00:00
と表示され、データベース側でtable1の内容を確認すると、
db1=> select * from table1;
 id |      datetime1      |       datetime2
----+---------------------+------------------------
  1 | 2012-12-01 09:00:00 | 2012-12-01 09:00:00+09
(1 row)
となります。

次に、サーバー側のタイムゾーンを台北に変更して、同じ事をやってみます。
データの追加処理が終わったところでは、コンソールに
id=1, datetime1=2012/10/01 9:00:00, datetime2=2012/10/01 11:00:00
と表示され、データベース側のtable1の内容は
db1=> select * from table1;
 id |      datetime1      |       datetime2
----+---------------------+------------------------
  1 | 2012-10-01 09:00:00 | 2012-10-01 10:00:00+08
(1 row)
となり、続けて更新処理を行うと、コンソールに
id=1, datetime1=2012/12/01 9:00:00, datetime2=2012/12/01 11:00:00
と表示され、データベース側でtable1の内容を確認すると、
db1=> select * from table1;
 id |      datetime1      |       datetime2
----+---------------------+------------------------
  1 | 2012-12-01 09:00:00 | 2012-12-01 10:00:00+08
となります。

あれれれ、予定では追加処理のあと、datetime2の値は、コンソールでは2012/10/01 09:00:00と表示され、データベース側のtable1のdatetime2の値は、2012-10-01 08:00:00+08になっているはずなのに、2時間ズレているようです。

原因を調べるために、追加処理で実際に実行されたSQLを調べてみると、
insert into table1 (
  id
, datetime1
, datetime2
) values (
  ((1)::int4)
, ((E'2012-10-01 09:00:00.000000')::timestamp)
, ((E'2012-10-01 09:00:00.000000')::timestamptz) at time zone interval '+09:00'
)
となっていました。
datetime2の値となる
((E'2012-10-01 09:00:00.000000')::timestamptz) at time zone interval '+09:00'
の部分をよく考えてみると、((E'2012-10-01 09:00:00.000000')::timestamptz)の、'2012-10-01 09:00:00'はtimestamptz型(with time zone)なので、9:00はUTCでは1:00となります。さらにat time zone interval '+09:00'となるので、1:00+9:00で、10:00という時間がdatetime2に書き込まれます。
これで2時間のズレが発生しているようです。
ようするに、
db1=> select timestamp with time zone '2012-10-01 9:00:00' at time zone interval '+09:00';
      timezone
---------------------
 2012-10-01 10:00:00
(1 row)

db1=> select timestamp without time zone '2012-10-01 9:00:00' at time zone interval '+09:00';
        timezone
------------------------
 2012-10-01 08:00:00+08
(1 row)

この2つのselect文の違いと同じことです。

そうなると、今回のコードで問題になるのは、InsertCommandとUpdateCommandのdatetime2に対するパラメータの型ということになります。
da.InsertCommand.Parameters.Add(new Npgsql.NpgsqlParameter("datetime2", NpgsqlTypes.NpgsqlDbType.TimestampTZ, 0, "datetime2", ParameterDirection.Input, true , 0, 0, DataRowVersion.Current, DBNull.Value));
この部分は
da.InsertCommand.Parameters.Add(new Npgsql.NpgsqlParameter("datetime2", NpgsqlTypes.NpgsqlDbType.Timestamp, 0, "datetime2", ParameterDirection.Input, true , 0, 0, DataRowVersion.Current, DBNull.Value));
このように、NpgsqlTypes.NpgsqlDbType.TimestampTZではなく、NpgsqlTypes.NpgsqlDbType.Timestampでなければいけないということのようです。

コードを修正して、もう一度、クライアントは東京、サーバーは台北という状態で実行してみます。
追加処理が終わったところで、コンソールには
id=1, datetime1=2012/10/01 9:00:00, datetime2=2012/10/01 9:00:00
と表示され、この時データベースのtable1は
db1=> select * from table1;
 id |      datetime1      |       datetime2
----+---------------------+------------------------
  1 | 2012-10-01 09:00:00 | 2012-10-01 08:00:00+08
(1 row)
となっていて、続けて更新処理が終わったところで、コンソールには
id=1, datetime1=2012/12/01 9:00:00, datetime2=2012/12/01 9:00:00
と表示され、データベースのtable1は
db1=> select * from table1;
 id |      datetime1      |       datetime2
----+---------------------+------------------------
  1 | 2012-12-01 09:00:00 | 2012-12-01 08:00:00+08
(1 row)
となっています。
これで予定通りの処理となりました。


念のため、クライアントのタイムゾーンを台北、サーバーのタイムゾーンを東京にして同じ処理を実行してみます。
追加処理が終わると、コンソールに
id=1, datetime1=2012/10/01 9:00:00, datetime2=2012/10/01 9:00:00
と表示され、この時データベースのtable1は
db1=> select * from table1;
 id |      datetime1      |       datetime2
----+---------------------+------------------------
  1 | 2012-10-01 09:00:00 | 2012-10-01 10:00:00+09
(1 row)
となり、続けて更新処理が終わると、コンソールには
id=1, datetime1=2012/12/01 9:00:00, datetime2=2012/12/01 9:00:00
と表示され、データベースのtable1は
db1=> select * from table1;
 id |      datetime1      |       datetime2
----+---------------------+------------------------
  1 | 2012-12-01 09:00:00 | 2012-12-01 10:00:00+09
(1 row)
となっています。

これで、クライアント側のプログラムでの日時は、クライアントのタイムゾーンに従った日時が入り、データベース上の日時は、UTC(with time zone)で入るようにできそうです。


一応、修正したソースを載せておきます。
using System;
using System.Collections.Generic;
using System.Text;
using System.Data;

namespace pgTimestamp
{
    class Program
    {
        static void Main(string[] args)
        {
            // データベース接続
            Npgsql.NpgsqlConnection conn = new Npgsql.NpgsqlConnection("Server=xxxx;"
                                                                     + "Port=5432;"
                                                                     + "User Id=yyyy;"
                                                                     + "Password=zzzz;"
                                                                     + "Database=db1;"
                                                                     + "Pooling=false;"
                                                                     + "Encoding=UNICODE;");
            // データベース接続
            try
            {
                conn.Open();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.ReadKey();
                return;
            }

            // クライアントの時間とUTCとの差を取得
            DateTime tmpTime = new DateTime(2000, 1, 1);             // 適当な日時をセットして
            TimeSpan diffUTC = tmpTime - tmpTime.ToUniversalTime();  // UTCとの差を取得
            string strDiffUTC = (diffUTC.TotalHours >= 0 ? "+" : "") + diffUTC.Hours.ToString("00") + ":" + diffUTC.Minutes.ToString("00");

            // テーブルの生成
            DataTable table1 = new DataTable("table1");
            table1.Columns.Add(new DataColumn("id"       , typeof(int)     ));
            table1.Columns.Add(new DataColumn("datetime1", typeof(DateTime)));
            table1.Columns.Add(new DataColumn("datetime2", typeof(DateTime)));
            table1.PrimaryKey = new DataColumn[] { table1.Columns["id"] };

            // データアダプタの生成
            Npgsql.NpgsqlDataAdapter da = new Npgsql.NpgsqlDataAdapter();
            da.SelectCommand = new Npgsql.NpgsqlCommand
            (
                   "select"
                +     " id"
                +    ", datetime1"
                +    ", datetime2 at time zone interval '" + strDiffUTC + "' as datetime2"
                + " from"
                +     " table1"
                , conn
            );
            da.InsertCommand = new Npgsql.NpgsqlCommand
            (
                  "insert into table1 ("
                +      "id"
                +    ", datetime1"
                +    ", datetime2"
                + ") values ("
                +      ":id"
                +    ", :datetime1"
                +    ", :datetime2 at time zone interval '" + strDiffUTC + "'"
                + ")"
                , conn
            );
            da.InsertCommand.Parameters.Add(new Npgsql.NpgsqlParameter("id"       , NpgsqlTypes.NpgsqlDbType.Integer  , 0, "id"       , ParameterDirection.Input, false, 0, 0, DataRowVersion.Current, DBNull.Value));
            da.InsertCommand.Parameters.Add(new Npgsql.NpgsqlParameter("datetime1", NpgsqlTypes.NpgsqlDbType.Timestamp, 0, "datetime1", ParameterDirection.Input, true , 0, 0, DataRowVersion.Current, DBNull.Value));
            da.InsertCommand.Parameters.Add(new Npgsql.NpgsqlParameter("datetime2", NpgsqlTypes.NpgsqlDbType.Timestamp, 0, "datetime2", ParameterDirection.Input, true , 0, 0, DataRowVersion.Current, DBNull.Value));
            da.UpdateCommand = new Npgsql.NpgsqlCommand
            (
                   "update table1 set"
                +     " id=:id"
                +    ", datetime1=:datetime1"
                +    ", datetime2=:datetime2 at time zone interval '" + strDiffUTC + "'"
                + " where"
                +     " id=:org_id"
                , conn
            );
            da.UpdateCommand.Parameters.Add(new Npgsql.NpgsqlParameter("id"       , NpgsqlTypes.NpgsqlDbType.Integer  , 0, "id"       , ParameterDirection.Input, false, 0, 0, DataRowVersion.Current , DBNull.Value));
            da.UpdateCommand.Parameters.Add(new Npgsql.NpgsqlParameter("datetime1", NpgsqlTypes.NpgsqlDbType.Timestamp, 0, "datetime1", ParameterDirection.Input, true , 0, 0, DataRowVersion.Current , DBNull.Value));
            da.UpdateCommand.Parameters.Add(new Npgsql.NpgsqlParameter("datetime2", NpgsqlTypes.NpgsqlDbType.Timestamp, 0, "datetime2", ParameterDirection.Input, true , 0, 0, DataRowVersion.Current , DBNull.Value));
            da.UpdateCommand.Parameters.Add(new Npgsql.NpgsqlParameter("org_id"   , NpgsqlTypes.NpgsqlDbType.Integer  , 0, "id"       , ParameterDirection.Input, false, 0, 0, DataRowVersion.Original, DBNull.Value));
            da.DeleteCommand = new Npgsql.NpgsqlCommand
            (
                   "delete from table1"
                + " where"
                +     " id=:org_id"
                , conn
            );
            da.DeleteCommand.Parameters.Add(new Npgsql.NpgsqlParameter("org_id"   , NpgsqlTypes.NpgsqlDbType.Integer    , 0, "id"       , ParameterDirection.Input, false, 0, 0, DataRowVersion.Original, DBNull.Value));

            // データの取得
            da.Fill(table1);
            // データの一旦削除
            try
            {
                foreach (DataRow row in table1.Rows)
                {
                    row.Delete();
                }
                Update(conn, da, table1);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.ReadKey();
                return;
            }

            // データの追加
            DateTime value = DateTime.Parse("2012-10-01 09:00:00");
            DataRow newRow = table1.NewRow();
            newRow["id"       ] = 1;
            newRow["datetime1"] = value;
            newRow["datetime2"] = value;
            table1.Rows.Add(newRow);
            // 保存
            try
            {
                Update(conn, da, table1);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.ReadKey();
                return;
            }
            // テーブルを一旦クリアして取得し直す
            table1.Rows.Clear();
            da.Fill(table1);
            // テーブル内容の表示
            Show(table1);

            // データの更新
            value = DateTime.Parse("2012-12-01 09:00:00");
            table1.Rows[0]["datetime1"] = value;
            table1.Rows[0]["datetime2"] = value;
            // 保存
            try
            {
                Update(conn, da, table1);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
                Console.ReadKey();
                return;
            }
            // テーブルを一旦クリアして取得し直す
            table1.Rows.Clear();
            da.Fill(table1);
            // テーブル内容の表示
            Show(table1);

            // データベース切断
            try
            {
                conn.Close();
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.Message);
            }
            Console.ReadKey();
        }

        /// <summary>
        /// データの保存
        /// </summary>
        /// <param name="conn">データベース接続子</param>
        /// <param name="da">データアダプタ</param>
        /// <param name="table1">データテーブル</param>
        static private void Update(Npgsql.NpgsqlConnection conn, Npgsql.NpgsqlDataAdapter da, DataTable table1)
        {
            // トランザクション開始
            Npgsql.NpgsqlTransaction tran = null;
            try
            {
                tran = conn.BeginTransaction();
                da.InsertCommand.Transaction = tran;
                da.UpdateCommand.Transaction = tran;
                da.DeleteCommand.Transaction = tran;
            }
            catch
            {
                throw;
            }

            // 保存
            try
            {
                da.Update(table1);
            }
            catch
            {
                tran.Rollback();
                throw;
            }

            // コミット
            try
            {
                tran.Commit();
            }
            catch
            {
                throw;
            }
        }

        /// <summary>
        /// テーブルの内容を表示
        /// </summary>
        /// <param name="table1">データテーブル</param>
        static private void Show(DataTable table1)
        {
            foreach (DataRow row in table1.Rows)
            {
                Console.WriteLine("id=" + row["id"].ToString()
                              + ", datetime1=" + ((DateTime)row["datetime1"]).ToString()
                              + ", datetime2=" + ((DateTime)row["datetime2"]).ToString());
            }
        }
    }
}