実際に運用していく段階を考える章。
ソリューション: なるべくスケーリング可能であるように布石を打っておく
- シャーディング
データを水平に(つまり一つのテーブルを)複数のDBに分割するのがシャーディング。
アプリケーションのスケーリングはマシンを増やせばどうにかなることが多いけど、DBのスケーリングは初めからそれを意図した構築をしておかないと途中から移行するのはかなりしんどい。
RailsにはOctopusという、比較的シンプルにシャーディングを行うgemがあるので、それを利用すると良い。ただし、そもそもシャーディング自体がかなり難度の高い行為なので、あまり無理はしないようにしよう(参考スライド)
- アセットファイルの扱い
ユーザーがコンテンツをuploadしたりするようなサービスの場合、クラウド上のスケーラブルなstorageを使うと後々楽という話。この本ではpaperclip×AWSS3の例が挙げられていた。この組み合わせは現代でも結構定石のようで、ググると解説エントリがちょこちょこ見つかる(paperclipのwikiにも事例としてある)。
自前鯖で持つのは色々こわいし、餅は餅屋ということで。
- PaaSの話
herokuとかそのへん。便利ですよね。
結論: 最初からスケーリングの意識を持っておこう。ただし、シャーディングはこわいのでよく考えて。
アンチパターン:ひどいSQL
ソリューション インデックスを張る
以下、だいたいの目安。
- primay key(デフォルトで貼ってある)
- 外部キー(has_manyとかに頻繁に使用される)
- ポリモーフィック関連(をまたぐクエリを発行するような際)
- ANDでtypeとその他のキーを条件に指定するようなケースが存在するとき、複合インデックスを貼っておくべきだ、という話。
- 複合インデックスを貼るときは、選択制の高いものから貼るということと、条件句のカラム指定の際に貼られているインデックスを意識することを忘れないように。
- 一意性のvalidateをしたいとき。ユニークindexを利用する。
- STI(シングルテーブル継承)時。typeカラム。
-
to_param
に渡されるもの(検索に利用される、という意味なので当然。(to_paramについて)) - ほか、検索条件に指定される(whereメソッドに渡される)カラム
逆に貼るかどうかよく考えたほうがいいもの
- 状態を表すStatusカラム、Booleanカラム、日付カラム
- cardinalityを考えよう
indexにもデメリットはある(挿入、更新)。利用シーンを想定した上で適切に貼ることが重要。
ヒントとなるツールとしては、limerick_rake(古すぎるので今使うのは現実的ではない)、MYSQLのEXPLAIN、Railsfootnotes、QueryReviewer(これもあまり古すぎて現実的ではない)などがあげられている。今だとar-proxyが使いやすいと思われる。
あと、new relic最強とも書いてある。お金があるならこれで。
結論: 適切にindexをはり、ボトルネッククエリを発見しよう
インデックスが実際に使われているかどうかはexplain
で調べることができる。ActiveRecordは直接クエリいじりにくいんだけど、親切なgemがあるのでこれを使うと良いかも。explainの解釈について、詳しくは各DBのレファレンス(MySQL)を見れば良いのだが、簡単にいうと、subtype(どんな種類のクエリか),type(どのようにアクセスするか,()),extra
を見れば良いようだ(解説)
ソリューション: 発行されるクエリについて自覚する
次の三つの違い。
@article.comments.count
@article.comments.length
@article.comments.size
答え:http://rubyist.g.hatena.ne.jp/rochefort/20090802/p1
のように、一見区別がつかないようなメソッドでも内部の挙動がかなり異なり、それがパフォーマンスのボトルネックにつながったりする。罠っぽい。
Rubyの配列いじるメソッドでごちゃごちゃやるようなことは、だいたいSQLでスッキリかけることが多い。select, where, joins
くらいとは仲良くなって、常識的なクエリを記述しよう。それでも生SQLを書くよりはだいぶとマシなはず。
比較例:
SQL
SELECT DISTINCT users.* FROM users INNER JOIN comments
ON comments.user_id = users.id INNER JOIN users users_comments
ON users_comments.id = comments.user_id INNER JOIN articles
ON articles.id = comments.article_id INNER JOIN comments comments_articles
ON comments_articles.article_id = articles.id WHERE (articles.id in (1) AND users.id != 1)
ActiveRecord
User.select("DISTINCT users.*").
joins(:comments => [:user, {:article => :comments}]).
where(["articles.id in ? AND users.id != ?",
self.article_ids, self.id])
結論: やっぱりSQLはわかろう
その他