この記事について
本記事は、2022年1月時点で社内向けに執筆された内容をもとに、再編集・公開したものです。
記載されている情報(使用技術・仕様・数値など)は、当時のバージョンや環境に基づいており、2025年現在の最新動向とは異なる可能性があります。
最新の技術仕様やベストプラクティスについては、公式ドキュメント等をご参照ください。
背景
とある案件を推進中、1件のテーブルが200万件以上レコードを持っていることが判明しました。
性能向上のため、インデックスを始めDB性能向上方法について学び直したのでまとめています。
割と初心者向けだと思いますが、なにか今後に活かせることがあれば幸いです。
インデックスについて
出典:Qiita – indexを理解しちゃおう
https://qiita.com/jonnyjonnyj1397/items/267b6c2646db92008640
indexはデータベースのパフォーマンス向上の手段としてよく使われているもの
出典:Qiita – indexを理解しちゃおう
例えるなら本の索引のようなもの
・SQL文を変更しなくても性能改善できる
・テーブルのデータに影響を与えない
・それでいて一定の効果が期待できる
通常、WHERE句などでのデータ検索は
フルスキャン(テーブルに含まれているレコードを最初から最後まで全部読み込む方法)で行われるが、
インデックスを作っておくことにより、
レンジスキャン(テーブルの一部のレコードのみにアクセスする方法)で検索をかけることが出来る。
これによりデータの抽出速度向上が見込める。
Railsでのインデックスの作り方
以下のようにマイグレーションを作成して実行する。
class AddIndexToテーブル名 < ActiveRecord::Migration def change add_index :テーブル名, カラム名 end end
どの程度のデータ量であれば使うべきか?
目安としてレコード100万件以上。
これ以下のテーブルにインデックスを用いてもあまり性能改善が期待できない。
(フルスキャンとレンジスキャンが大差ない)
どのようなカラムに使うべきか?
カーディナリティ(値の分散度)が高いデータに有効。
⇒ 時間・日付系、名前、メールアドレスなど一意性が高いデータには有効
⇒ 逆に言えば、性別やフラグなどEnum型、bool型のデータではあまり効果が無い
主キー、外部キーには暗黙でインデックスが作成されるので、改めて作成する必要は無い。
注意点
インデックスを作ることにより、INSERT、UPDATE時のオーバーヘッドが増加するため、書き込み処理のパフォーマンスは下がる点に注意。
また、インデックスを作りすぎると、意図しないインデックスが使われてパフォーマンスが劣化する可能性もある。
何でも作れば良いというわけではない。
今回の案件で思ったこと
インデックスは最初に作っても後から作っても手間が変わらないので、最初に設計方針を悩むより、最後まで実装が終わってからピンポイントにインデックスを作っていくようなやり方が早いと思います。
その他 DBの性能を向上する手段
N+1について
出典:Qiita – 【Ruby on Rails】N+1問題ってなんだ?
https://qiita.com/massaaaaan/items/4eb770f20e636f7a1361
プログラムからORM等でSQLを発行してDBを操作する場合によく起こる問題。
for(each)などでループ処理させる際、ループの内部でSQLを毎回発行してしまうような状況を指す。
例)
# すべての会社に対し、それぞれ所属する人の名前を出力する Company.all.each do |comp| # usersテーブルから名前を取得し、カンマ区切りで結合する user_names = comp.users.pluck(:name).join(“,”) p “#{comp.name}に所属する人は#{user_names}です” end
Company Load (0.3ms) SELECT `companies`.* FROM `companies` (0.3ms) SELECT `users`.`name` FROM `users` WHERE `users`.`company_id` = 1 “AB商事に所属する人は田中さん,佐藤さん,鈴木さんです” (0.3ms) SELECT `users`.`name` FROM `users` WHERE `users`.`company_id` = 2 “CDテクノロジーに所属する人は吉田さん,高橋さんです” (0.3ms) SELECT `users`.`name` FROM `users` WHERE `users`.`company_id` = 3 “EF株式会社に所属する人は山田さん,渡辺さんです”
ループが100件、1,000件となってくると、SQLの処理時間が積もり積もって大幅に性能が劣化する。
ループの中で子テーブルのデータを参照させたりした時に起こりがち。
複数テーブル結合してると、わりと百件程度でも体感で分かるくらい劣化する印象。
対策
プログラム側で対策する。
Rubyでの対策の例(preloadを使うと、最初にSQLを1回発行してあとはキャッシュを使うようになる)
# すべての会社に対し、それぞれ所属する人の名前を出力する Company.preload(:users).all.each do |comp| # usersテーブルから名前を取得し、カンマ区切りで結合する user_names = comp.users.pluck(:name).join(“,”) p “#{comp.name}に所属する人は#{user_names}です” end
Company Load (0.3ms) SELECT `companies`.* FROM `companies` User Load (0.5ms) SELECT `users`.* FROM `users` WHERE `users`.`company_id` IN (1, 2, 3) “AB商事に所属する人は田中さん,佐藤さん,鈴木さんです” “CDテクノロジーに所属する人は吉田さん,高橋さんです” “EF株式会社に所属する人は山田さん,渡辺さんです”
大体、どのフレームワークにも同じような便利メソッドがある。
prefetchとかpreloadなどの名前がついていることが多い。
Railsなどを使っていてパフォーマンス悪いと感じる場合、まずこのN+1を疑う。
デバッグログにSQLを吐くようにしておき、ループで大量にSQLが発行されていないかを見るとよい。
参考にした記事はこちら
出典:Qiita – gem bulletを入れてみた(N+1問題に自分で気づけない人のために)
https://qiita.com/tanutanu/items/7157446d7c3bfe377d63
技術情報の補足
本記事でご紹介している手法や事例は、筆者が関わった2022年当時の開発案件におけるナレッジをまとめたものです。
現在の技術スタックやフレームワークのバージョンにおいては、より適切な手法がある場合がありますので、あくまで参考情報としてご覧いただき、必要に応じて最新の公式情報をご確認ください。