この記事は、2024年Advent Calendarの12月6日の記事その1です。
私はターミナルページャーを作っていて、psqlでも便利に使えるようになっています。
そこで、その2の記事も書くことにしました。psqlのページャーにovを!
こちらはそれとは関係なく、PostgreSQLのSEARCH_PATH
について書きます。
SCHEMA(スキーマ)
PostgreSQLには、SCHEMA(スキーマ)
という概念があります。SCHEMA
は、データベース内に作られるテーブルやビュー、関数、型などのデータベースオブジェクトを含む名前空間です。
テーブルはSCHEMA
に属していて、テーブルにアクセスするにはSCHEMA名.テーブル名
という形式で指定します。
例)
SELECT * FROM schema_name.table_name;
つまり、階層構造として、データベース名 > スキーマ名 > テーブル名という形になります。標準SQLにならい、その形式でも指定できます。
例)
SELECT * FROM database_name.schema_name.table_name;
ただし、PostgreSQLでは、接続したデータベース以外にはアクセスできません。そのため、実行しているデータベースの間違い防止ぐらいの意味しかありません。通常はデータベース名は省略します。
そして、SCHEMA名も省略できます。省略した場合には、SEARCH_PATH
によって解決される...のですが、そう単純でもないなというのが、この記事の主旨です。
SEARCH_PATH
SEARCH_PATHはUnix系のPATHと同じようにSCHEMA
を登録しておけば、SEARCH_PATHに登録してあるテーブル等はSCHEMA名
を省略してアクセスできる機能です。
SCHEMA
は複数作成でき、違うSCHEMAに同じ名前のテーブル等を作成できます。同じ名前のテーブルを作成しておいて、アクセスするときに違うSCHEMAを使用することで、実際には違うテーブルを参照できるというのは意図した動作です。
例えば、PostgreSQLのデフォルトのSEARCH_PATH
では'$user', public
となっています。'$user'
はユーザー名のSCHEMA
となります。
デフォルトではpublic
というSCHEMA
が作成されていて、ユーザー名のSCHEMA
は作成されていません。そのため、SCHEMA
を指定しない場合はpublic
が使われます。
SEARCH_PATH
は、左側が優先です。
ユーザー名のSCHEMA
を利用すると、以下のようなことができます。
ユーザー用のテーブルをユーザー名のSCHEMA
に作成し、共通のテーブルをpublic
のSCHEMA
に作成することで、ログインしたユーザー向けの結果を返すことができます。
SELECT * FROM user_name.table_usre LEFT JOIN public.table_public ON user_name.table_user.id = public.table_public.id;
を
SELECT * FROM table_user LEFT JOIN table_public ON table_user.id = table_public.id;
と書けることになります。記述が少なくなるだけでなく同じSQLでユーザー毎の結果を返すことができます。
SEARCH_PATHの設定
そして、SEARCH_PATHは、Unixの環境変数のように設定できます。設定できる場所はいくつかあります。
設定ファイル(postgresql.conf)
全体で設定(デフォルトが設定されていて変更がない場合はコメントになっています)。
search_path = '"$user", public' # schema names
ユーザー毎の設定
ALTER ROLE
で設定できます。
ALTER ROLE user_name SET search_path = 'schema_name';
pg_roles
のrolconfig
で確認できます。
SELECT rolconfig FROM pg_roles WHERE rolname = 'user_name';
rolconfig
---------------------------
{search_path=schema_name}
(1 row)
関数作成時に設定
ユーザーを切り替えて関数を所有するユーザーの権限で実行できるため。
マニュアルを参照してください。PostgreSQL: Documentation: 16: CREATE FUNCTION
SET SEARCH_PATHで設定
ログインしてからSET SEARCH_PATH
で設定できます。
# CREATE SCHEMA test;
CREATE SCHEMA
# SET SEARCH_PATH = test;
SET
SEARCH_PATHの確認
そして最終的に設定していあるSEARCH_PATH
は、SHOW SEARCH_PATH
で確認できます。
# show SEARCH_PATH;
search_path
-------------
test
(1 row)
最近のPostgreSQLでは、デフォルトのpublic SCHEMA
の権限が見直されたり、使用についての説明が詳細にされていたりするのでマニュアルを参照してください。PostgreSQL: Documentation: 16: 5.7. スキーマ#5.9.6. 使用パターン
探すのはSEARCH_PATHだけではない
SEARCH_PATH
が設定されたSCHEMAだけが使われるというシンプルな世界であれば良いのですが、実際にはそう単純ではありません。
その要因はpg_catalog SCHEMA
とpg_temp SCHEMA
です。
pg_catalog SCHEMA
は、マニュアルから引用すると
このスキーマにはシステムテーブルと全ての組み込みデータ型、関数および演算子が含まれています。pg_catalogは常に検索パスに含まれています。
となっています。
これが、もしSEARCH_PATH
にpg_catalog
が含まれていない場合は、組み込み関数がSELECT pg_catalog.count(*) FROM test;
のように指定しないと使えなくなってしまいますので、必要な措置です。
さらにややこしいのがpg_temp SCHEMA
です。PostgreSQLでは、一時テーブルを作成するときには、通常のテーブルが作成されるSCHEMA
とは違って、
CREATE TEMP TABLE test (id int);
とすると、pg_temp SCHEMA
にtest
テーブルが作成されます。
このままだとpg_temp.test
としてアクセスする必要があるため、自動でpg_temp SCHEMA
が検索パスに追加されます。それにより、
SELECT * FROM test;
と書けるようになります。そしてpg_temp SCHEMA
は、通常の検索パスより優先されるため、通常のtest
テーブルがあってもpg_temp.test
が参照されます。これは通常テーブルと同じ名前の一時テーブルを作成しても、一時テーブルが参照されるようにするという使い方をするためだと理解できます。
SEARCH_PATHの優先順位
ということで、SCHEMA
を省略した場合はSEARCH_PATH
に加えてpg_temp
とpg_catalog
が探されるということになります。
上で書いたようにSCHEMA
が違えば同じ名前のテーブルを作成できるため、SCHEMA
を省略した場合は、優先順位が重要になります。
デフォルトでは、pg_temp
、pg_catalog
、SEARCH_PATH
に書いた順(SERCH_PATHのデフォルトは'$user', public
)となります。
しかーし、SEARCH_PATH
にpg_temp
やpg_catalog
を加えると優先順位を変えることができ、例えばpublic
を優先することができます。
つまり関数のオーバーライドができるということです。
(この機構を悪用できる問題があり対応が必要な場合があります。A Guide to CVE-2018-1058: Protect Your Search Path)
まず通常のSQL関数を作ります。max()は集約関数ですが、今回は単純な文字列を返す関数にしてみます。これはSCHEMA
を指定していないため、デフォルトのpublic
に作成されます。
# CREATE function max(integer) RETURNS text AS $$
SELECT 'TORA TORA TORA';
$$ LANGUAGE SQL IMMUTABLE;
通常のSEARCH_PATH
で実行。「表示されていないため」pg_caatalogが優先されます。
# SHOW SEARCH_PATH;
search_path
-----------------
"$user", public
(1 row)
SELECT max(1);
max
-----
1
(1 row)
public
を優先するようにSEARCH_PATH
を変更して実行。
# SET search_path = public, pg_catalog;
SET
# SHOW search_path;
search_path
--------------------
public, pg_catalog
(1 row)
# SELECT max(3);
max
----------------
TORA TORA TORA
(1 row)
作成したmax()関数が実行されました。
pg_tempの特殊性
さて、上記で書いたデフォルトの優先順位を思い出して下さい。
pg_temp
、pg_catalog
、SEARCH_PATH
の順です。つまり、pg_temp
に同じ名前の関数を作成すると、pg_temp
の関数が実行されることになりませんか?
# CREATE function pg_temp.max(integer) RETURNS text AS $$
SELECT 'Give me a Shake';
$$ LANGUAGE SQL IMMUTABLE;
SEARCH_PATHをデフォルトに戻して実行するとpg_temp
、pg_catalog
、SEARCH_PATH
の順のはず...
# SHOW search_path ;
search_path
-----------------
"$user", public
(1 row)
SELECT max(1);
max
-----
1
(1 row)
ですが、安心して下さい。なりません。
PostgreSQLのマニュアル から引用すると
同様に、現在のセッションの一時テーブルスキーマpg_temp_nnnも、存在すれば常に検索されます。 これはpg_tempという別名を使用してパスに明示的に列挙させることができます。 パスに列挙されていない場合、最初に(pg_catalogよりも前であっても)検索されます。 しかし、一時スキーマはリレーション(テーブル、ビュー、シーケンスなど)とデータ型名に対してのみ検索されます。 関数や演算子名に対してはまったく検索されません。
となっていて、pg_temp
に関数を作成してもSCHEMA
を指定しない限り実行されないということです。
SHOW SEARCH_PATHでは気づかない
関数や演算子名に関しては、まあ良いことにしてもSEARCH_PATH
の優先順位はややこしいです。
さらに、SEARCH_PATH
のセットにミスっても誰も指摘してくれません。環境変数と同じで、そういうものなのですが。
# SET search_path = 'public, pg_catalog, pg_temp'
SET
# SHOW search_path;
search_path
-------------------------------
"public, pg_catalog, pg_temp"
(1 row)
と一見問題になさそうですが、実際にはSCHEMA
の設定がされていないので、pg_temp
, pg_catalog
の順で検索されてしまいます。
そのため、SEARCH_PATH
を変更するときは、身を清め、白装束に着替え、一点に集中してミスの無いようにセットする必要があります(ウソです)。
真のSEARCH_PATHを確認
SEARCH_PATH
がほんとのところはどうなっているか確認するには、current_schemas()
関数を使います。
SELECT current_schemas(true);
current_schemas
-------------------------------
{pg_temp_1,pg_catalog}
(1 row)
となって、search_pathに'public, pg_catalog, pg_temp'という文字列で渡してしまったことに気づけます(実際にはSET search_patth = public, pg_catalog, pg_temp
と書く必要があった)。
pg_temp_1
は、実際のスキーマ名です。pg_tempはセッションごとに異なりますが、サーバー側で名前が一意になるようにされるため、pg_temp_1
のような名前がつけられ、pg_temp
としてもアクセスできるようになります。
ということで、ほんとのところはcurrent_schemas(true)
関数を使って確認してから、リレーション(テーブル、ビュー、シーケンス)は探す、関数や演算子名はpg_temp_*
を除外して探す...ということになります。
メンテナンスコマンドのSERACH_PATHが変更
pg_tempの関数が実行されないといっても、テーブルをオーバーライドされると想定していた動作と違って困る場合があります。
PostgreSQLの17では、メンテナンスコマンド中はSEARCH_PATH
が変更されるようになりました。
SERACH_PATH
のところは、セキュリティ問題もあり、意図しない動作の要因にもなるため、今後もまた変わるかもしれないですね。
まとめ
入口はシンプルそうに見えて、中はシンプルではない世界でした。