このレクチャーでは、トランザクション操作でありがちなミスと、それをどうやって避けるかにフォーカスするよ。マジで、どんなSQLマスターでもたまにCOMMIT書き忘れることあるからね!ここで、トランザクションのミスをレアケースにするコツを伝授する。
残念ながら(ある意味ありがたいけど)、データベースって魔法の城じゃないから、全部が完璧に動くわけじゃない。トランザクション操作のミスは、特に初心者にはよくあること。詳しく見ていこう!
COMMITやROLLBACKの書き忘れ
トランザクションを終わらせるのを忘れるのは、まさに「あるあるネタ」。レストランで料理頼んだのに、店員がレシート持ってくるの忘れた感じ。PostgreSQLの世界だと、DBが「トランザクション中」のまま止まって、リソースを掴んだまま他の操作をブロックしちゃう。
ミスの例:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- うっかり!COMMITかROLLBACKを追加し忘れた。
トランザクションが「止まった」ままだと、リソースのロックがテーブル全体に広がることもある。DB管理者が状況を把握していれば、強制終了できるけど、できればそうなる前に防ぎたいよね。
どうやって防ぐ?
- 必ず明示的にトランザクションを終わらせよう:
COMMITかROLLBACK。 - トランザクションが止まってると教えてくれるクライアントツールを使うのもアリ。
- トランザクションが終わってないままアプリを再起動すると、DBが自動で
ROLLBACKしてくれるけど、システムの状態的にはちょっと面倒なこともある。
間違ったアイソレーションレベルの選択
アイソレーションレベルの選択って地味だけど、異常系を防ぐのに超重要。例えば、重要な金融処理でREAD UNCOMMITTED使っちゃうと、「ダーティー」なデータを読んじゃって、後で取り消されることもある。
例:
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
BEGIN;
-- 他のトランザクションが変更中のデータを読む
SELECT balance FROM accounts WHERE account_id = 1;
-- 別のトランザクションがROLLBACKすると、読んだデータが無効になる。
どうやって防ぐ?
- アプリにとってデータの重要度をちゃんと考えよう。
- ほとんどのケースでは
READ COMMITTEDを使って、「ダーティーリード」を避けよう。 - 変更やファントムデータを絶対に避けたいときは、
REPEATABLE READやSERIALIZABLEみたいな厳しめのアイソレーションレベルを使おう。
トランザクションの競合とロック
たまに、2つ以上のトランザクションが同じデータを変更しようとすることがある。そうなると、PostgreSQLはどっちかが終わるまで片方をロックする。これがデッドロック(相互ロック)ってやつ。
ミスの例:
-- 1つ目のトランザクション
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 2つ目のトランザクション
BEGIN;
UPDATE accounts SET balance = balance + 100 WHERE account_id = 1;
-- 1つ目のトランザクションを待ってる...
両方のトランザクションが、お互いに必要なリソースを持ったままだと、行き詰まる。PostgreSQLはデッドロックを検出して、どちらか一方をエラーで終了させて、こんなメッセージを出す:
ERROR: deadlock detected
どうやって防ぐ?
- トランザクション内の操作順序を固定しよう。
- トランザクションの実行時間を短くして、ロックの確率を下げよう。
SERIALIZABLEレベルは本当に必要なときだけ使おう。
SAVEPOINTのミス
SAVEPOINTは部分的なロールバックに便利だけど、使い方を間違えるとややこしくなる。例えば、セーブポイント(RELEASE SAVEPOINT)を解放し忘れると、余計なロックやエラーの原因になる。
ミスの例:
BEGIN;
SAVEPOINT my_savepoint;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
ROLLBACK TO SAVEPOINT my_savepoint;
-- SAVEPOINTの解放を忘れた!
どうやって防ぐ?
- もう使わない
SAVEPOINTはちゃんと消そう。 - セーブポイントを作りすぎるとクエリが複雑になるから、ほどほどに。
外部システムとのトランザクション非互換
例えば、PostgreSQLのトランザクションが外部システムとやり取りしようとする場合(通知送信、API更新など)。外部システムで何か問題が起きると、変更をロールバックするのが難しくなる。
例:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
-- 通知送信できない:email-serverが応答しない。
COMMIT; -- 変更は保存されたけど、通知は送れてない。
どうやって防ぐ?
- できるだけ外部システムとのやり取りは分離しよう。
- 中間テーブルやジョブキューを使って、外部システムとの連携を調整しよう。
大きなトランザクションによるミス
大量の操作を含む大きなトランザクションは、ロックやタイムアウト、デッドロックなどのミスが起きやすい。
例:
BEGIN;
-- 数千件の更新操作
UPDATE orders SET status = '完了' WHERE delivery_date < CURRENT_DATE;
COMMIT; -- 結構時間かかるかも。
どうやって防ぐ?
- 大きなトランザクションは小さく分割しよう。
- データ更新はバッチ処理を使おう。
- 1つのトランザクションで変更するデータ量は最小限に。
エラーチェックのし忘れ
トランザクション内の全てのSQLクエリが必ず成功するとは限らない。例えば、どれかの操作でエラーが出ると、トランザクション全体が失敗状態になる。
例:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE account_id = -1; -- エラー:account_idが存在しない。
UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
COMMIT; -- エラーのせいで実行されない。
どうやって防ぐ?
- 各操作の実行結果は必ずチェックしよう。
- クエリやクライアントコードでエラー処理を入れよう。
ROLLBACKの動作の誤解
多くの開発者は、ROLLBACKで変更が全部元に戻ると思いがち。でもROLLBACKは、今のトランザクションの範囲でしか効かない。
誤解の例:
UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
ROLLBACK; -- エラー!トランザクションじゃないから効かない。
どうやって防ぐ?
BEGINは友達、これがないとROLLBACKは無力だって覚えとこう。- 重要な操作は必ずトランザクションで囲もう。
GO TO FULL VERSION