全ての記事へ戻る

【実録】PostgreSQLが過呼吸に! 〜インデックスがないDBは、巨大迷路でウォーリーを探すようなもの〜

先日、本番環境のDBが「ウッ……ガハッ……!」と苦悶の表情(CPU使用率100%)を浮かべていたので、緊急オペを行いました。

その原因は、SQL界の「ウォーリーをさがせ」状態にありました。

1. 現場の惨状:pg_stat_activityの告白

まずは状況把握。pg_stat_activity という「DBの覗き窓」から、今何が起きているのかを見てみます。

SELECT pid, state, query FROM pg_stat_activity WHERE state != 'idle';

すると、そこには**「地獄の底から湧き出てきたような長文クエリ」**が居座っていました。

 pid | state  | query
-----+--------+-------------------------------------------------------------
 615 | active | WITH pgrst_source AS ( 
              |   SELECT "customers".*, 
              |   COALESCE(json_agg("orders"), '[]') AS "orders",
              |   -- (以下、数千文字に及ぶJOINとサブクエリの嵐)

この PID: 615 くん、何をしているかというと: 「顧客(customers)」を呼び出し、その顧客の「注文(orders)」を全件探し、さらにその注文に紐づく「注文商品(items)」や「配送先(destinations)」を……と、芋づる式にデータを探し回っていたのです。

2. なぜDBは死にかけていたのか?

今回の問題は、テーブル間の「つながり」に**索引(Index)**がなかったことです。

イメージしてください。あなたは巨大なAmazonの倉庫に立たされています。 上司から**「顧客ID:99番さんの、過去の注文履歴を全部持ってきて!」**と言われました。

  • インデックスがある場合: 「注文管理台帳」の「顧客ID順」のページをパッと開き、「99番さんは……あ、この5件ね」と1秒で終了。
  • インデックスがない場合(今回のケース): **「全在庫の段ボールを一つずつ開けて、顧客IDが99番かどうかを確認する」**という狂気の作業が始まります。

これを ordersitemsdestinations と全テーブルで繰り返していたのですから、DBが過呼吸になるのも無理はありません。

3. 執刀:3本のインデックスという「地図」

迷えるDBくんに、私は3枚の地図を授けました。

-- 1. 「どの注文がどの顧客のものか」を即座に引ける地図
CREATE INDEX ON orders(customer_id);

-- 2. 「どの商品明細がどの注文のものか」を即座に引ける地図
CREATE INDEX ON order_items(order_id);

-- 3. 「どの配送先がどの注文のものか」を即座に引ける地図
CREATE INDEX ON shipping_destinations(order_id);

4. 奇跡の復活

インデックスを貼った瞬間、世界が変わりました。

  • Before: 「えーっと、顧客ID 99番の注文は……1箱目……違う……2箱目……違う……(数分経過)」
  • After: 「顧客99番? 3番棚の左から5番目にあるよ!(0.01秒)」

CPU使用率は一気に静まり返り、DBは平穏を取り戻しました。

まとめ:そのJOIN、地図はありますか?

PostgRESTなどのツールを使っていると、非常に便利で複雑なクエリが自動生成されます。しかし、その裏側でDBが「フルスキャン(全件しらみつぶし)」という地獄の作業を強いられていないか、時々覗いてあげてください。

「外部キー(ForeignKey)を貼ったら、セットでインデックスも貼る」

この鉄則を守るだけで、救われるDBの命があります。現場からは以上です。