CodeGym /コース /SQL SELF /トランザクションを考慮したプロシージャの最適化:パフォーマンス分析とロールバック

トランザクションを考慮したプロシージャの最適化:パフォーマンス分析とロールバック

SQL SELF
レベル 54, レッスン 3
使用可能

プロシージャを作るとき、それがDBの「ハート」になって色々な操作をやることが多いよね。でも、同時に「ボトルネック」にもなりがち。特にこんな場合は:

  1. 無駄な操作をしてる(例えば、同じデータに何度もアクセスしてるとか)。
  2. インデックスをうまく使えてない。
  3. 1つのトランザクションで操作が多すぎる。

ある賢いエンジニアが言ってたけど、「ダメなコードを速くするのは、怠け者の友達にもっと速く走れって言うようなもんだ」って。だからプロシージャの最適化は、単なるスピードアップじゃなくて、土台から良くすることなんだ!

トランザクション内の操作数を最小限にする

PostgreSQLのトランザクションは、操作ごとにオーバーヘッドが発生する。トランザクションが大きいほどロックも長くなって、他のユーザーもブロックされやすい。これを減らすには:

  1. 1つのトランザクションに操作を詰め込みすぎない。
  2. EXCEPTION ENDを使って、ローカルに変更を制限する。部分的なロールバックが必要なときに便利。
  3. 大きなトランザクションは、アプリのロジックが許せば小さく分割しよう。

例:大量データの挿入を「バッチ」に分ける:

-- 例:段階的コミット付きバッチロード用プロシージャ
CREATE PROCEDURE batch_load()
LANGUAGE plpgsql
AS $$
DECLARE
    r RECORD;
    batch_cnt INT := 0;
BEGIN
    FOR r IN SELECT * FROM staging_table LOOP
        BEGIN
            INSERT INTO target_table (col1, col2) VALUES (r.col1, r.col2);
            batch_cnt := batch_cnt + 1;
        EXCEPTION
            WHEN OTHERS THEN
                -- エラーをログする;この要素の変更はロールバックされる
                INSERT INTO load_errors(msg) VALUES (SQLERRM);
        END;
        IF batch_cnt >= 1000 THEN
            COMMIT; -- 1000件ごとにコミット
            batch_cnt := 0;
        END IF;
    END LOOP;
    COMMIT; -- 最後のコミット
END;
$$;

ヒント:それぞれのCOMMITで変更が確定するから、トランザクション分割でデータの整合性が崩れないか事前に確認しよう。

インデックスでクエリを高速化する

例えば、ordersテーブルに100万件あって、customer_idでよく検索する場合。インデックスがなければ全件スキャンになる:

CREATE INDEX idx_customer_id ON orders(customer_id);

これで、こんなクエリ:

SELECT * FROM orders WHERE customer_id = 42;

が、全テーブルスキャンせずにめっちゃ速くなるよ。

重要:プロシージャ作るときは、使うカラムがインデックスに含まれてるか確認しよう。特にフィルタ、ソート、JOIN条件で使うやつ。

EXPLAIN ANALYZEでパフォーマンス分析

EXPLAINはクエリの実行プラン(PostgreSQLがどうやって実行するか)を見せてくれるし、ANALYZEを付けると実際の実行統計(どれくらい時間かかったか等)も出る。例えば:

EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;

プロシージャ内でどう使う?

複雑なクエリは、EXPLAIN ANALYZEで個別に実行して「分解」してみよう:

DO $$
BEGIN
    RAISE NOTICE 'クエリプラン: %',
    (
        SELECT query_plan
        FROM pg_stat_statements
        WHERE query = 'SELECT * FROM orders WHERE customer_id = 42'
    );
END $$;

分析と改善の例

元のプロシージャ(遅いバージョン):

CREATE OR REPLACE FUNCTION update_total_sales()
RETURNS VOID AS $$
BEGIN
    UPDATE sales
    SET total = (
        SELECT SUM(amount)
        FROM orders
        WHERE orders.sales_id = sales.id
    );
END $$ LANGUAGE plpgsql;

何が起きてる? salesテーブルの各行ごとにSUM(amount)のサブクエリが実行されて、めっちゃ操作が多くて遅い。

改善バージョン:

CREATE OR REPLACE FUNCTION update_total_sales()
RETURNS VOID AS $$
BEGIN
    UPDATE sales as s
    SET total = o.total_amount
    FROM (
        SELECT sales_id, SUM(amount) as total_amount
        FROM orders
        GROUP BY sales_id
    ) o
    WHERE o.sales_id = s.id;
END $$ LANGUAGE plpgsql;

これならSUMのサブクエリは1回だけで、全部まとめて更新できる。

エラー時のデータロールバック

プロシージャ内で何か失敗したら、トランザクションの一部だけロールバックできる。例えば:

BEGIN
    -- データ挿入
    INSERT INTO inventory(product_id, quantity) VALUES (1, -5);
EXCEPTION
    WHEN OTHERS THEN
        -- このブロックは内部savepointへのロールバックと同じ!
        RAISE WARNING 'データ更新時のエラー: %', SQLERRM;
END;

実践:堅牢な注文処理プロシージャの実装

例えば、注文を処理するタスクがあるとする。途中でエラー(在庫不足など)が起きたら、注文はキャンセルされてエラーがログに残る。

CREATE OR REPLACE PROCEDURE process_order(p_order_id INT)
LANGUAGE plpgsql
AS $$
DECLARE
    v_in_stock INT;
BEGIN
    -- 在庫チェック
    SELECT stock INTO v_in_stock FROM products WHERE id = p_order_id;

    BEGIN
        IF v_in_stock < 1 THEN
            RAISE EXCEPTION '在庫がありません';
        END IF;
        UPDATE products SET stock = stock - 1 WHERE id = p_order_id;
        -- ... 他の操作
    EXCEPTION
        WHEN OTHERS THEN
            -- このブロック内の変更は全部ロールバック!
            INSERT INTO order_logs(order_id, log_message)
                VALUES (p_order_id, '処理エラー: ' || SQLERRM);
            RAISE NOTICE '注文処理エラー: %', SQLERRM;
    END;

    -- エラーがなければこの後のコードが続く
    -- ログに「注文処理成功」とかも書ける
END;
$$;
  • エラーがあっても注文は処理されず、order_logsテーブルにログが残る。
  • エラー時は内部savepointが効いて、全体の文脈は失われない。

プロシージャ最適化&堅牢化の基本ルール

  1. プロシージャ内のクエリにはインデックスを使おう。
  2. 大きな操作はバッチ(小分け)にして段階的に処理しよう。
  3. エラーはちゃんとログしよう ― 大量操作用のエラーログテーブルを作ると便利。
  4. 「部分的」ロールバックにはEXCEPTION付きのネストブロックだけ使おう。
  5. PL/pgSQL内でROLLBACK TO SAVEPOINTは使わない ― シンタックスエラーになるよ。
  6. プロシージャではCOMMIT/SAVEPOINTはautocommitモードの接続時だけ使おう!
  7. 重いクエリの実行プラン(EXPLAIN ANALYZE)は、プロシージャに組み込む前に外で分析しよう。
2
タスク
SQL SELF, レベル 54, レッスン 3
ロック未解除
大きなトランザクションをバッチに分割する
大きなトランザクションをバッチに分割する
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION