psql の SQL で分岐処理
◆ \if で分岐できる
◆ 変数も使えて :foo 形式で参照できる
◆ psql コマンドの -v オプションで変数をセットできる
◆ SELECT と \gset を使って実行中に SELECT の結果を変数にセットもできる
◆ 変数も使えて :foo 形式で参照できる
◆ psql コマンドの -v オプションで変数をセットできる
◆ SELECT と \gset を使って実行中に SELECT の結果を変数にセットもできる
SQL ファイルの中で 分岐したり変数を使ったりしたいことがあります
環境ごとにスキーマやテーブル名が少し違ったり 特定の環境のみ追加のテーブルが必要だったり 権限の関係で追加の GRANT が必要だったり そういうのです
以前は 適当なテンプレートエンジンを使って そのテンプレートエンジンの構文で変数や分岐を処理して環境ごとの SQL ファイルを一時的に生成してそれを実行ということをやってました
これが複雑になるので SQL だけでやりたいと思うのですが PL/pgSQL はこれはこれで複雑です
ここまで高機能じゃなくていいんですよね
探すと psql の中のメタコマンドだけで実現できそうだったのでやってみます
一応メタコマンドの if は以前から存在は知ってたものの 機能不足でやりたいことができなさそうと思って使ってなかったのですが ちゃんとドキュメントを見るとちょっと面倒なだけで機能としては実現できました
foo という変数の値によって分岐します
[a.sql]
コマンドラインから foo に色々な値を入れて実行してみます
ここだと通常の SQL を実行してませんが \echo のところに INSERT とか書くと特定の条件の場合だけ SQL を実行できます
変数をコマンドラインから指定する場合は -v を使って foo=bar 形式で指定します
複数の変数を指定するなら -v を複数回使います
変数の指定を必須にしたい場合は 変数が存在することを確認して存在しないならエラーにします
「:{?foo}」 のように書くと 変数 foo が存在するかで TRUE/FALSE になります
\if や \elif には expression を指定できるのですが expression といいつつ ここで計算ができません
= とか < みたいなものも使えないです
変数名が指定できるだけで 変数が TRUE/FALSE で分岐するだけです
TRUE/FALSE の反転が簡単にできず unless のようなものもないので FALSE のときだけしたい処理が簡単にできないです
\if は空にして \else の方を使うみたいなことになります
\if を使うには変数に TRUE/FALSE を入れないといけないです
SQL ファイルの処理中に変数に代入するには SELECT と \gset を使います
のように ; の代わりに \gset を書くと SELECT の結果を変数に入れられます
as で指定した名前が変数名になります
SELECT で列数を増やせば 同時に複数の変数へ代入できますし SELECT で実行できる処理は何でもできるので TRUE/FALSE を反転したり足し算したり比較したり色々できます
他にも \set を使って変数に代入することもできます
ただこれは固定値になるのであまり使いどころがなくて \if の分岐の中で固定値をセットするような使い道です
プロンプトとして 「password: 」 までが表示されてその後の foo はユーザーが入力したものです
次の行の foo が変数を表示したものです
そこまでのことをするなら PL/pgSQL を使ったほうがいいんじゃないかと思います
ただ include 機能があるのでそれっぽいことは一応できなくはないです
[main.sql]
[loop.sql]
繰り返したい部分を別の SQL ファイルに切り出して 条件に一致する限り再帰的に include し続けます
\gexec を使うと SELECT の結果を実行できます
これを実行します
ただ やることがこれなら \gexec を通さなくても INSERT を使ってできます
https://www.postgresql.org/docs/current/app-psql.html
環境ごとにスキーマやテーブル名が少し違ったり 特定の環境のみ追加のテーブルが必要だったり 権限の関係で追加の GRANT が必要だったり そういうのです
以前は 適当なテンプレートエンジンを使って そのテンプレートエンジンの構文で変数や分岐を処理して環境ごとの SQL ファイルを一時的に生成してそれを実行ということをやってました
これが複雑になるので SQL だけでやりたいと思うのですが PL/pgSQL はこれはこれで複雑です
ここまで高機能じゃなくていいんですよね
探すと psql の中のメタコマンドだけで実現できそうだったのでやってみます
一応メタコマンドの if は以前から存在は知ってたものの 機能不足でやりたいことができなさそうと思って使ってなかったのですが ちゃんとドキュメントを見るとちょっと面倒なだけで機能としては実現できました
サンプル
まずサンプルですfoo という変数の値によって分岐します
[a.sql]
\if :{?foo}
\else
\echo '変数 foo を入力してください'
\q
\endif
SELECT :'foo' = 'A' as is_a, :'foo' = 'B' as is_b \gset
\if :is_a
\echo 'foo は A です'
\elif :is_b
\echo 'foo は B です'
\else
\echo 'foo はその他です'
\endif
コマンドラインから foo に色々な値を入れて実行してみます
root@b3c9f1fa843b:/# psql -U postgres -f a.sql
変数 foo を入力してください
root@b3c9f1fa843b:/# psql -U postgres -f a.sql -v foo=A
foo は A です
root@b3c9f1fa843b:/# psql -U postgres -f a.sql -v foo=B
foo は B です
root@b3c9f1fa843b:/# psql -U postgres -f a.sql -v foo=C
foo はその他です
root@b3c9f1fa843b:/# psql -U postgres -f a.sql -v foo=
foo はその他です
メタコマンド
\ から始まるのが psql のメタコマンドですここだと通常の SQL を実行してませんが \echo のところに INSERT とか書くと特定の条件の場合だけ SQL を実行できます
変数をコマンドラインから指定する場合は -v を使って foo=bar 形式で指定します
複数の変数を指定するなら -v を複数回使います
変数の指定を必須にしたい場合は 変数が存在することを確認して存在しないならエラーにします
「:{?foo}」 のように書くと 変数 foo が存在するかで TRUE/FALSE になります
\if や \elif には expression を指定できるのですが expression といいつつ ここで計算ができません
= とか < みたいなものも使えないです
変数名が指定できるだけで 変数が TRUE/FALSE で分岐するだけです
TRUE/FALSE の反転が簡単にできず unless のようなものもないので FALSE のときだけしたい処理が簡単にできないです
\if は空にして \else の方を使うみたいなことになります
\if を使うには変数に TRUE/FALSE を入れないといけないです
SQL ファイルの処理中に変数に代入するには SELECT と \gset を使います
SELECT 1 as var1 \gset
のように ; の代わりに \gset を書くと SELECT の結果を変数に入れられます
as で指定した名前が変数名になります
SELECT で列数を増やせば 同時に複数の変数へ代入できますし SELECT で実行できる処理は何でもできるので TRUE/FALSE を反転したり足し算したり比較したり色々できます
他にも \set を使って変数に代入することもできます
postgres=# \set foo bar
postgres=# \echo :foo
bar
ただこれは固定値になるのであまり使いどころがなくて \if の分岐の中で固定値をセットするような使い道です
ユーザー入力
\prompt を使ってユーザー入力を受け取って変数に代入もできます\prompt 'password: ' pw
\echo :pw
password: foo
foo
プロンプトとして 「password: 」 までが表示されてその後の foo はユーザーが入力したものです
次の行の foo が変数を表示したものです
繰り返し
psql のメタコマンドはそこまで高機能ではないので 分岐はできますが繰り返しや関数のような機能はありませんそこまでのことをするなら PL/pgSQL を使ったほうがいいんじゃないかと思います
ただ include 機能があるのでそれっぽいことは一応できなくはないです
[main.sql]
drop table if exists foo;
create table foo (id integer primary key);
\set i 0
\ir loop.sql
select * from foo;
[loop.sql]
insert into foo (id) values (:'i');
select :'i'::integer + 1 as i \gset
select :'i'::integer < 10 as continue \gset
\if :continue
\ir loop.sql
\else
\echo 'done'
\endif
root@2676da1df87f:/# psql -U postgres -f main.sql
DROP TABLE
CREATE TABLE
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
done
id
----
0
1
2
3
4
5
6
7
8
9
(10 rows)
繰り返したい部分を別の SQL ファイルに切り出して 条件に一致する限り再帰的に include し続けます
繰り返し 2
別の考え方で SQL を作って実行させるという方法もあります\gexec を使うと SELECT の結果を実行できます
postgres=# select 'insert into foo (id) values (' || generate_series(0, 9) || ')';
?column?
---------------------------------
insert into foo (id) values (0)
insert into foo (id) values (1)
insert into foo (id) values (2)
insert into foo (id) values (3)
insert into foo (id) values (4)
insert into foo (id) values (5)
insert into foo (id) values (6)
insert into foo (id) values (7)
insert into foo (id) values (8)
insert into foo (id) values (9)
(10 rows)
これを実行します
postgres=# truncate foo;
TRUNCATE TABLE
postgres=# select 'insert into foo (id) values (' || generate_series(0, 9) || ')'\gexec
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
INSERT 0 1
postgres=# select * from foo;
id
----
0
1
2
3
4
5
6
7
8
9
(10 rows)
ただ やることがこれなら \gexec を通さなくても INSERT を使ってできます
insert into foo (id) values (generate_series(0, 9));
その他
psql のメタコマンドは他にも色々あってドキュメントにまとまってますhttps://www.postgresql.org/docs/current/app-psql.html