Rails大規模テーブル変更の極意!CONCURRENTLYでサイトを止めないインデックス作成
生徒
「先生、アプリのユーザーが増えてきて検索が遅くなったのでインデックスを貼りたいです。でも、データが何百万件もあると作業中にサイトが止まるって聞いたのですが本当ですか?」
先生
「その通りです。普通にインデックスを作ると、データベースがその作業に集中してしまい、他の書き込みを止めてしまう『ロック』が発生します。」
生徒
「サイトが止まるのは困ります!動かしたまま安全に作業する方法はないんですか?」
先生
「ありますよ!Railsの『CONCURRENTLY(コンカレントリ)』という魔法を使えば、オンラインのまま裏側でこっそりインデックスを作れるんです。その手順を詳しく学びましょう!」
1. 大規模テーブルの悩み「ロック」とは?
Ruby on Rails(ルビーオンレイルズ)で作成したアプリケーションが成長し、データベースのテーブル(表)に保存されたデータが数十万、数百万件を超えてくると、検索機能が目に見えて遅くなります。これを解決するために、データの「目次」であるインデックスを作成するのが一般的です。
しかし、パソコンを触ったことがない方にもイメージしやすいように例えると、インデックス作成は「図書館の膨大な蔵書をすべて読み直して、巨大な索引カードを作る作業」のようなものです。普通のやり方だと、その索引を作っている間、司書さんは他の作業ができなくなり、新しい本を棚に入れたり貸し出したりするのを止めてしまいます。これが専門用語で言うテーブルロックという状態です。
大規模なウェブサイトでロックが発生すると、ユーザーが「注文ボタン」を押しても反応しなくなり、最悪の場合はエラー画面が表示されてしまいます。これを防ぐために、オンラインインデックス作成という技術が必要になるのです。
2. CONCURRENTLY(コンカレントリ)の仕組み
PostgreSQL(ポストグレスキューエル)などのデータベースで使える強力な武器が、CONCURRENTLY(コンカレントリ)というオプションです。これは「並行して(同時に)」という意味を持ちます。
このオプションを使ってインデックスを作ると、データベースは「通常業務(読み書き)」を続けながら、空いている時間やリソースを使って裏側で少しずつ目次を作ってくれます。図書館の例えなら、通常業務を続ける司書さんの横で、別のスタッフが少しずつ索引カードを書き溜めていくようなものです。時間は少しかかりますが、お客様をお待たせすることなく、安全に検索を速くすることができます。
3. Railsマイグレーションでの準備:トランザクションの解除
Railsで CONCURRENTLY を使ったインデックス作成を行うには、一つだけ特別な設定が必要です。それが disable_ddl_transaction!(ディーディーエル・トランザクションの解除)という命令です。
通常、Railsのマイグレーションは「全部成功するか、全部失敗するか」というセット(トランザクション)で動いています。しかし、CONCURRENTLY を使った作業は「時間がかかるので、セットにしないで単独で動かしてね」というルールがあります。この一行を書き忘れると、エラーが出て実行できませんので注意しましょう。
class AddIndexToLargeTable < ActiveRecord::Migration[7.0]
# この一行が最重要!トランザクションをオフにします。
disable_ddl_transaction!
def change
# ここに処理を書いていきます
end
end
4. 安全にインデックスを作成する書き方
準備ができたら、実際にインデックスを追加する命令を書きます。ここで algorithm: :concurrently というオプションを指定します。これが「裏側でやってね」という合図になります。
例えば、何百万件もある orders(注文)テーブルの customer_id にインデックスを貼る場合は以下のように記述します。この一行が、サイトを停止させるか、平穏無事に稼働させるかの分かれ道になります。大規模なスキーマ設計においては、これが「当たり前」の作法として求められます。
class AddIndexToOrdersCustomerId < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def change
# algorithm: :concurrently を指定して、オンラインのままインデックス作成
add_index :orders, :customer_id, algorithm: :concurrently
end
end
5. インデックス作成に失敗した時の「不完全な目次」
CONCURRENTLY は便利な反面、少しデリケートな部分もあります。作業中にデータベースに負荷がかかりすぎたり、別の要因で作業が中断されたりすると、INVALID(無効)なインデックスが残ってしまうことがあります。
これは、書きかけのまま放置された「ボロボロの索引カード」のようなものです。場所だけ取って、検索には全く役に立ちません。もし失敗した場合は、一度その中途半端なインデックスを remove_index で削除してから、もう一度やり直す必要があります。大規模テーブルを扱う際は、実行した後に「正しく目次が完成したか」を確認するまでが遠足……ではなく、エンジニアの仕事です。
# 失敗した時の確認(SQLなどで確認すると INVALID と表示されます)
# その場合は削除して再実行!
remove_index :orders, :customer_id, algorithm: :concurrently
6. 不要なインデックスを削除する時も CONCURRENTLY
実は、インデックスを作る時だけでなく、削除する時も CONCURRENTLY を使うのがマナーです。インデックスの削除も、大規模なテーブルではデータベースに負担をかけ、一瞬ロックを引き起こす可能性があるからです。
「もうこの目次は使わないから捨てよう」という時も、そっと裏側で作業を行うことで、ユーザーへの影響を最小限に抑えられます。これもまた、零ダウンタイム移行(サービスを止めない移行)を実現するための重要なテクニックの一つです。
class RemoveIndexFromOrders < ActiveRecord::Migration[7.0]
disable_ddl_transaction!
def change
# 削除する時も、他の作業を邪魔しないように実行!
remove_index :orders, column: :customer_id, algorithm: :concurrently
end
end
7. 作業時間を正しく見積もる大切さ
CONCURRENTLY を使った作業は、通常のインデックス作成よりも「時間がかかる」のが普通です。数百万件のデータがある場合、数分から数十分、場合によっては数時間かかることも珍しくありません。
プログラミング未経験の方は「パソコンなんだから一瞬で終わるのでは?」と思うかもしれませんが、大量の情報を整理整頓するには、それ相応の時間がかかります。作業を始める前に、テスト環境でどのくらいの時間がかかるかを計測しておくことが、本番環境でのトラブルを防ぐための知恵となります。焦らず、じっくりとデータベースに仕事をさせる心の余裕も大切ですね。
8. 大規模開発で愛されるエンジニアになるために
今回学んだ CONCURRENTLY や algorithm: :concurrently の知識は、ユーザー数が少ないうちは必要ありません。しかし、アプリが成長し、多くの人があなたの作ったサイトを訪れるようになった時、必ず必要になる技術です。
「機能を追加して便利にする」だけでなく、「サービスを止めずに改善し続ける」という視点を持つエンジニアは、現場で非常に重宝されます。まずはマイグレーションを書くときに、「このテーブルは大きいかな?」と一瞬立ち止まって考えることから始めてみましょう。その一歩が、高品質なシステム作りの第一歩です!