PostgreSQL の障害対応で使う構文一覧

障害対応(PostgreSQL) コースで覚える構文の一覧です。共通 SQL コースの構文(SELECT・JOIN・GROUP BY・ウィンドウ関数など)はすべて使えます。

lock_timeout と statement_timeout待ちすぎない・長すぎる SQL を止めるLv12 で覚える

SET lock_timeout = '3s';
SET statement_timeout = '30s';
ALTER ROLE ロール SET statement_timeout = '5min';
ALTER SYSTEM SET lock_timeout = '10s';(全体。ふつうはしない)

lock_timeout は、ロックを取れずに待つ時間の上限です。超えると canceling statement due to lock timeout で、その文だけを取り消します。ALTER TABLE などの DDL は強いロック(AccessExclusiveLock)を待つ間、後から来た読み書きもすべて後ろに並ばせるので、lock_timeout を短く決めてから流し、あきらめたら時間をおいて流し直します。statement_timeout は、1 つの文の時間の上限です(canceling statement due to statement timeout。上限は文を始めたときの設定で決まり、いま動いている文には、あとから決めた値は効きません)。ALTER ROLE … SET で決めると、そのロールの次の接続から効きます。

SET lock_timeout = '2s';
ALTER TABLE orders ADD COLUMN memo text;
ALTER ROLE report SET statement_timeout = '10min';

デッドロックと log_lock_waitsにらみ合いを見つける・待ちを記録するLv13 で覚える

SHOW deadlock_timeout;
ALTER SYSTEM SET log_lock_waits = on;
SELECT deadlocks FROM pg_stat_database WHERE datname = 'quest';
\! tail -n 12 log/postgresql.log

2 つのトランザクションが、相手のつかんだ行をお互いに待つと、どちらも進めません(デッドロック)。待ち始めて deadlock_timeout(既定 1 秒)たつと、サーバーが調べ、片方を deadlock detected で取り消します。ログの DETAIL に、だれが・どのトランザクションを・どの SQL で待っていたかが出ます。直し方は、行を書き換える順番を全部の処理でそろえることです。log_lock_waits = on にすると、deadlock_timeout より長く待った文が still waiting for … after … ms としてログに残ります。

ALTER SYSTEM SET log_lock_waits = on;
SELECT pg_reload_conf();
\! grep 'still waiting' log/postgresql.log

SELECT … FOR UPDATE書き換える行を、先に決まった順でつかむLv14 で覚える

SELECT id FROM 表 WHERE id IN (…) ORDER BY id FOR UPDATE;
… FOR NO KEY UPDATE;(キーの列を変えない書き換え。外部キーの確かめとぶつからない)
… FOR UPDATE NOWAIT;(つかまれていたら待たずにエラー)
… FOR UPDATE SKIP LOCKED;(つかまれた行は飛ばす)

SELECT に FOR UPDATE を付けると、出てきた行をトランザクションの終わりまでつかみます(ほかの接続の UPDATE・DELETE・FOR UPDATE は待たされます)。ORDER BY を付けると、並べた順につかみます。2 つの処理が同じ行を逆の順番で書き換えると、お互いを待ってデッドロックになります。どの処理も、最初に「番号の小さい順に FOR UPDATE でつかむ」と決めておけば、後から来た方が先頭の行で待つだけで、輪になりません。表を LOCK TABLE で丸ごとつかんでも起きなくなりますが、関係のない行の処理まで一列に並ばせてしまいます。

BEGIN;
SELECT id FROM ledger WHERE id IN (2, 1) ORDER BY id FOR UPDATE;
UPDATE ledger SET balance = balance - 300 WHERE id = 2;
UPDATE ledger SET balance = balance + 300 WHERE id = 1;
COMMIT;

auto_explain遅かった SQL の実行計画をログに残すLv22 で覚える

LOAD 'auto_explain';(この接続だけ)
SET auto_explain.log_min_duration = '100ms';
SET auto_explain.log_analyze = on;
ALTER SYSTEM SET shared_preload_libraries = 'auto_explain';(すべての接続。再起動が要る)

log_min_duration より長くかかった SQL の実行計画を、サーバーのログに書きます(duration: … ms plan: の次の行から Query Text: と計画)。後から EXPLAIN すると速いのに、本番では遅かった、というときに、そのときの計画が分かります。LOAD はその接続だけで効くので、アプリの接続にも効かせるには shared_preload_libraries に入れて再起動し、auto_explain.log_min_duration を ALTER SYSTEM で決めて読み直します。log_analyze = on にすると actual の行数も出ますが、すべての文で時間を測るので重くなります。

LOAD 'auto_explain';
SET auto_explain.log_min_duration = 0;
\! grep -A 3 'plan:' log/postgresql.log

複合インデックスいくつかの列で 1 つのインデックスLv37 で覚える

CREATE INDEX 名前 ON 表 (列1, 列2);
CREATE INDEX CONCURRENTLY 名前 ON 表 (列1, 列2, 列3);

インデックスは列の順に並べた目次です。(a, b) は a の順に並び、a が同じ中で b の順です。だから a の条件が無いと使いにくく、a の範囲で探すと b の条件は飛ばし読みになります。等号(=)で探す列を先に、範囲(>=・BETWEEN)や ORDER BY の列を後に置くと、読む行が少なくなり、並べ替え(Sort)も要らなくなります。

CREATE INDEX visits_doctor_visited_idx ON visits (doctor_id, visited_at);

式インデックス関数や式の結果で引くLv39 で覚える

CREATE INDEX 名前 ON 表 ((式));
CREATE INDEX 名前 ON 表 (lower(列));

WHERE lower(email) = … のように列に関数をかけると、列のインデックスは使えません。式の結果を目次にした式インデックスなら使えます。問い合わせには、インデックスと同じ形の式を書きます(lower(trim(email)) と lower(email) は別の式)。関数でない式は ((列::date)) のように括弧を 2 重にします。使えるのは IMMUTABLE(同じ入力なら必ず同じ結果)の関数だけです。

CREATE INDEX patients_lower_email_idx ON patients (lower(email));

CREATE STATISTICS(拡張統計)関わり合う列の統計Lv41 で覚える

CREATE STATISTICS 名前 (dependencies) ON 列1, 列2 FROM 表;
CREATE STATISTICS 名前 (ndistinct, mcv) ON 列1, 列2 FROM 表;
ANALYZE 表;
SELECT statistics_name, attnames, dependencies FROM pg_stats_ext WHERE tablename = '表';

プランナーは、ふつう列どうしは関係が無いと考えて、条件の割合をかけ算します。郵便番号が決まれば市も決まるような列では、見積もり(rows)が小さくなりすぎ、Nested Loop を選んでしまいます。CREATE STATISTICS で列の組を教え、ANALYZE すると、関わり(dependencies)・組の種類の数(ndistinct)・よくある組(mcv)を集めて、見積もりが合います。作っただけでは空で、ANALYZE で中身が入ります。

CREATE STATISTICS patients_city_zip (dependencies) ON city, zip FROM patients;
ANALYZE patients;