JOINを重ねたSQLが返ってこなくなった話

はじめに

3月に未経験からエンジニアになったハヤシです。 今回は、私が実務で関わっている「健診システム」の開発を通して学んだ、SQLとリレーショナルデータベースの難しさについて書いていきます。
当社が扱っている健診システムは、膨大なデータ量を扱っており、かつ構造が複雑です。 私が所属しているチームは問い合わせの原因調査や不具合の修正をメインで担当しておりますが、調査を行なっていく中でデータベースに登録されている情報を確認することがよくあります。
実際にデータを取得しようとしたところ、SQLを実行しても結果が返ってこず、AWS上のインスタンス負荷が上昇し続ける事象に遭遇しました。
今回は、なぜそのような事象が起きたのか、実務ならではのテーブル構造や の注意点を交えながら整理します。
 

なぜ何度も JOIN を重ねてしまうのか?(健診システムの構造)

調査を進める中で、「複数のデータを確認する必要がある」となる場面はよくあります。
例えば、ある受診者の詳細画面を作るとします。起点となるのは受診を特定するID(例えば受診者のID)ですが、このIDから直接引っ張ってこれる情報には限界があります。
  • 基本情報は別テーブル
  • 健診予約は別のテーブル
  • 問診はさらに別のテーブル……
そのため、目的の情報を集めるために、自然と何度も を重ねる必要があります。
当初は、必要なテーブルを JOIN していけば目的のデータを取得できると考えていました。
しかし、実際のデータ量が多い環境では、クエリの複雑さが大きな負荷につながることが分かりました。
 

JOINの種類:INNER と LEFT の違いで結果が変わる

ここで、 の種類について整理します。 ここを理解していないと、予期せぬバグやパフォーマンス低下を招く可能性があります。
  • (内部結合)
    • 左右のテーブルの両方に存在するデータだけを取得します。
    • 例えば「必ず検査結果が存在するデータ」だけを取得したい場合に使います。ただし、片方にデータがないと、その行自体が結果から除外されます。
  • (外部結合)
    • 左側のテーブルのデータはすべて残し、右側のテーブルに一致するデータがあれば結合します。右側になければ になります。
    • 健診システムのように「受診はしたけれど、特定のオプション検査は受けていない(データがない)」というケースがある場合、 だと受診データ自体が画面から消えてしまうため、 を使う必要があります。
たとえば、以下のような2つのテーブルがあるとします。
 

テーブルA (users / 受診者)

user_idname
1佐藤
2鈴木
3高橋
 

テーブルB (orders / 検査項目)

user_iditem
1血糖値
2中性脂肪
 

【INNER JOIN の結果】

が共通している「1(佐藤)」と「2(鈴木)」だけが取得され、今回の検査結果データがない「3(高橋)」は除外されます。
SQL
nameitem
佐藤血糖値
鈴木中性脂肪
 

【パターン2】 LEFT JOIN の結果

左側()のデータは全員残り、検査結果データがない「3(高橋)」の には が入ります。
SQL
nameitem
佐藤血糖値
鈴木中性脂肪
高橋NULL
しかし、 を何重にも重ねたことが、後述するパフォーマンス悪化の一因になりました。
 

ある日突然、クエリが返らなくなった(画面が応答しなくなる事象)

ローカルの検証環境では、テストデータが数件しか入っていなかったため、何重にも したクエリでも一瞬で結果が返ってきました。
しかし、そのまま本番環境で確認したところ、実際の大量データ(数万〜数百万件レベル)が入った状態では結果が返ってきませんでした。
原因を確認すると、対象のクエリによってサーバーのCPUやメモリ使用率が上昇し、データベースに大きな負荷がかかっている状態でした。
 

なぜクエリが返らなくなるのか?(注意すべきパターン)

今回の原因を振り返ると、以下のような要素が重なっていました。
  1. インデックスが十分に貼られていなかった
      • の条件に使っている外部キーや、 句で絞り込むカラムにインデックスが用意されておらず、MySQLがすべてのデータを最初から最後まで探す「フルスキャン」の状態になっていました。
  1. 何重もの によるデータ量の爆発
      • 巨大なテーブル同士を条件の絞り込み前に したため、データベースの内部で膨大な組み合わせの一時的なデータが生成され、処理しきれない状態になりました。
  1. 多くの情報を一度に取得しようとするクエリ
      • 1回のSQLで必要な情報をすべて取得しようとした結果、クエリが複雑化し、オプティマイザ(実行計画を考える機能)が効率的な経路を見つけにくくなっていました。
 

おわりに

今回は、健診システムという複雑なデータ構造を前に、何度も を重ねた結果クエリが返らなくなった実体験と、 の違いについて整理しました。
「とりあえず繋げば動く」という考えで書いたSQLが、データ量が増えた途端に大きな負荷につながる可能性があることを学びました。
「じゃあ、この重くなったクエリをどうやって調査すればいいのか?」 「データベースの中では何が起きているのか?」
次回は、パフォーマンス改善の必須ツールである「EXPLAINで何がわかるのか?」について書いていきたいと思います。 最後までお読みいただき、ありがとうございました。