ruby on rails on mysql チューニング入門
DESCRIPTION
Rails 3 系+MySQL を利用しているサービス向けに 1. どのようにボトルネックを探すのか 2. どのような設計を行えばいいのか 3. Rails上でどのようなコードを書けばいいのか の3点に絞ってこのプレゼンをみてチューニングを行えるように資料作成を行いましたTRANSCRIPT
佐藤大資 (@eccyan)
Ruby on Railson MySQL
チューニング入門
Ruby on Rails を使えば簡単に誰でもWeb サービスを構築する事が出来ます。
Ruby on Rails を使えば簡単に誰でもWeb サービスを構築する事が出来ます。
しかし、 SQL を理解せずにActive Record を利用していると・・・
そこで今回は、
そこで今回は、
そこで今回は、
そこで今回は、
に絞ってチューニングの取り掛り方法をお伝えします。
ActiveRecord って何?
ActiveRecord って何?
ActiveRecord って何?• DB テーブルの 1 行が 1 つのクラス
ActiveRecord って何?
ActiveRecord って何?• DB テーブルの 1 行が 1 つのクラス• 色々な RDBMS を同じソースコードで(MySQL, PostgreSQL, SQLite, SQL Server, Sybase, and Oracle)
ActiveRecord って何?
ActiveRecord って何?• DB テーブルの 1 行が 1 つのクラス• 色々な RDBMS を同じソースコードで(MySQL, PostgreSQL, SQLite, SQL Server, Sybase, and Oracle)• SQL 再利用の仕組みがある
ActiveRecord って何?
ストアドプロシージャ?
ActiveRecord って何?
DELIMITER // DROP PROCEDURE IF EXISTS proc1// CREATE PROCEDURE proc1()BEGIN SELECT VERSION();END;//DELIMITER ;
ストアドプロシージャ?
ActiveRecord って何?
DELIMITER // DROP PROCEDURE IF EXISTS proc1// CREATE PROCEDURE proc1()BEGIN SELECT VERSION();END;//DELIMITER ;負荷大丈夫?
ストアドプロシージャ?• 処理速度は早いが、負荷集中する• Master-Slave 構成の場合、更新系の
負荷分散が出来ない• RDBMS 毎に構文やノウハウが変わる
ActiveRecord って何?
オレオレライブラリ?
ActiveRecord って何? /** * ページングクエリ (LIMIT 付き ) を実行する * * 注 : $sql は SELECT で始めてください。 * $sql に SELECT SQL_CALC_FOUND_ROWS を入れないでください。 * * ページングパラメータ ($paging) について * <pre> * page ページ番号 (1-) empty や 0 以下の場合は 1 が仮定される * pagesize ページサイズ empty や 0 以下の場合は最後まで * no_count true: カウント不要 , empty: カウント必要 * </pre> * * @param stdclass $db DB ハンドラ * @param string $sql SQL * @param string $params パラメータ * @param array $paging ページングパラメータ * @param string $key null: 結果は array of array * $key を指定すると結果をある列の配列で返す * @return array array('total' => 全件数 , 'list' => 結果 ) * ただし、 $pageing['no_count'] が true の場合は * 結果のみを返します。 */ public function doPagingQuery($db, $sql, $params, $paging, $key = null) { if (empty($paging['no_count'])) { $sql = preg_replace('/SELECT/i', 'SELECT SQL_CALC_FOUND_ROWS', $sql, 1); }
// ページング条件 $this->createLimitClause($limitClause, $params, $paging); $sql .= ' ' . $limitClause;
// 実行 $list = $db->query($sql, $params)->result_array(); if ($key) { foreach ($list as &$value) { $value = $value[$key]; } }
if (empty($paging['no_count'])) { // 全件数を取得する $totalResult = $db->query('SELECT FOUND_ROWS() AS total') ->row_array(); $total = $totalResult['total']; } }
オレオレライブラリ?
ActiveRecord って何? /** * ページングクエリ (LIMIT 付き ) を実行する * * 注 : $sql は SELECT で始めてください。 * $sql に SELECT SQL_CALC_FOUND_ROWS を入れないでください。 * * ページングパラメータ ($paging) について * <pre> * page ページ番号 (1-) empty や 0 以下の場合は 1 が仮定される * pagesize ページサイズ empty や 0 以下の場合は最後まで * no_count true: カウント不要 , empty: カウント必要 * </pre> * * @param stdclass $db DB ハンドラ * @param string $sql SQL * @param string $params パラメータ * @param array $paging ページングパラメータ * @param string $key null: 結果は array of array * $key を指定すると結果をある列の配列で返す * @return array array('total' => 全件数 , 'list' => 結果 ) * ただし、 $pageing['no_count'] が true の場合は * 結果のみを返します。 */ public function doPagingQuery($db, $sql, $params, $paging, $key = null) { if (empty($paging['no_count'])) { $sql = preg_replace('/SELECT/i', 'SELECT SQL_CALC_FOUND_ROWS', $sql, 1); }
// ページング条件 $this->createLimitClause($limitClause, $params, $paging); $sql .= ' ' . $limitClause;
// 実行 $list = $db->query($sql, $params)->result_array(); if ($key) { foreach ($list as &$value) { $value = $value[$key]; } }
if (empty($paging['no_count'])) { // 全件数を取得する $totalResult = $db->query('SELECT FOUND_ROWS() AS total') ->row_array(); $total = $totalResult['total']; } }
誰が保守するの?
オレオレライブラリ?• 属人性が高い• OSS に比べて枯れていない• プラグマブルで無く、代替出来ない
ActiveRecord って何?
AR で再利用可能なコードを書こう• Arel• Relation• Scope
ActiveRecord って何?
Scope
ActiveRecord って何?
scope :active, where(active: true) scope :inactive, where(active: false)
Scope
ActiveRecord って何?
scope :active, where(active: true) scope :inactive, where(active: false)
scope :adult_categories, lambda { # 大人向けカテゴリの ID は 1, 5, 6 active.where(category_id: [1, 5, 6]) }
負荷の少ないクエリとは?
負荷の少ないクエリとは?
負荷の少ないクエリとは?• 検索処理が軽い
負荷の少ないクエリとは?
負荷の少ないクエリとは?• 検索処理が軽い• 並び替え処理が軽い
負荷の少ないクエリとは?
負荷の少ないクエリとは?• 検索処理が軽い• 並び替え処理が軽い• 結合処理が軽い
負荷の少ないクエリとは?
負荷の少ないクエリとは?• 検索処理が軽い• 並び替え処理が軽い• 結合処理が軽い
EXPLAIN で調べよう
負荷の少ないクエリとは?
EXPLAIN• インデックスが必要か確認できる• 結合順序が最適か確認できる
負荷の少ないクエリとは?
EXPLAIN
負荷の少ないクエリとは?
mysql> EXPLAIN SELECT `answers`.`id` AS t0_r0, `answers`.`old_id` AS t0_r1, `answers`.`question_id` AS t0_r2, `answers`.`user_id` AS t0_r3, `answers`.`question_user_id` AS t0_r4, `answers`.`to_user_id` AS t0_r5, `answers`.`parent_id` AS t0_r6, `answers`.`content` AS t0_r7, `answers`.`ng_word` AS t0_r8, `answers`.`check` AS t0_r9, `answers`.`active` AS t0_r10, `answers`.`created_at` AS t0_r11, `answers`.`updated_at` AS t0_r12, `questions`.`id` AS t1_r0, `questions`.`old_id` AS t1_r1, `questions`.`old_random_key` AS t1_r2, `questions`.`parent_category_id` AS t1_r3, `questions`.`category_id` AS t1_r4, `questions`.`user_id` AS t1_r5, `questions`.`to_user_id` AS t1_r6, `questions`.`type_id` AS t1_r7, `questions`.`is_serious` AS t1_r8, `questions`.`content` AS t1_r9, `questions`.`ng_word` AS t1_r10, `questions`.`display` AS t1_r11, `questions`.`check` AS t1_r12, `questions`.`access_count` AS t1_r13, `questions`.`use_image` AS t1_r14, `questions`.`image_url` AS t1_r15, `questions`.`link_url` AS t1_r16, `questions`.`active` AS t1_r17, `questions`.`answer_last_updated_at` AS t1_r18, `questions`.`created_at` AS t1_r19, `questions`.`updated_at` AS t1_r20, `questions`.`last_answer_id` AS t1_r21 FROM `answers` INNER JOIN `users` ON `users`.`id` = `answers`.`user_id` INNER JOIN `questions` ON `questions`.`id` = `answers`.`question_id` WHERE `answers`.`active` = 1 AND (answers.user_id = 1) AND (answers.question_user_id != 1) AND (answers.to_user_id = 1 OR answers.to_user_id is NULL) AND (questions.active = 1) AND (questions.id IN ( SELECT temp_a.question_id FROM answers as temp_a WHERE temp_a.user_id = 1 )) AND (answers.ng_word IN (0, 1)) GROUP BY answers.question_id ORDER BY questions.answer_last_updated_at desc;
EXPLAIN
負荷の少ないクエリとは?
+----+--------------------+-----------+----------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------+---------+------------------------------------+------+----------------------------------------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+--------------------+-----------+----------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------+---------+------------------------------------+------+----------------------------------------------+| 1 | PRIMARY | users | const | PRIMARY | PRIMARY | 4 | const | 1 | Using index; Using temporary; Using filesort || 1 | PRIMARY | answers | ref | index_answers_on_question_id_and_user_id_and_created_at,index_answers_on_question_id_and_created_at,index_answers_on_user_id_and_created_at,index_answers_on_to_user_id_and_created_at,index_answers_on_question_user_id_and_created_at | index_answers_on_user_id_and_created_at | 4 | const | 196 | Using where || 1 | PRIMARY | questions | eq_ref | PRIMARY,index_questions_on_id | PRIMARY | 4 | rio_production.answers.question_id | 1 | Using where || 2 | DEPENDENT SUBQUERY | temp_a | index_subquery | index_answers_on_question_id_and_user_id_and_created_at,index_answers_on_question_id_and_created_at,index_answers_on_user_id_and_created_at | index_answers_on_question_id_and_user_id_and_created_at | 8 | func,const | 4 | Using index; Using where |+----+--------------------+-----------+----------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------------------------------------+---------+------------------------------------+------+----------------------------------------------+4 rows in set (0.01 sec)
select_type
負荷の少ないクエリとは?
SIMPLE キーまたは JOIN
PRIMARY 外部クエリを示すSUBQUERY 相関関係のないサブクエリ
DEPENDENT SUBQUERY
相関関係のあるサブクエリ
UNCACHEABLE SUBQUERY
実行毎に結果が変わるサブクエリ
DERIVED FROM 句のサブクエリ
select_type
負荷の少ないクエリとは?
SIMPLE キーまたは JOIN
PRIMARY 外部クエリを示すSUBQUERY 相関関係のないサブクエリ
DEPENDENT SUBQUERY
相関関係のあるサブクエリ
UNCACHEABLE SUBQUERY
実行毎に結果が変わるサブクエリ
DERIVED FROM 句のサブクエリ
サブクエリ• SQL 内部で更に SQL を発行し取得する事• 遅いのは相関関係のあるサブクエリ
負荷の少ないクエリとは?
相関サブクエリクエリとサブクエリが相互関係している
負荷の少ないクエリとは?
EXPLAINSELECT * FROM questions WHERE questions.id IN (SELECT answers.question_id FROM answers WHERE question_user_id = questions.user_id );
相関サブクエリクエリとサブクエリが相互関係している
負荷の少ないクエリとは?
EXPLAINSELECT * FROM questions WHERE questions.id IN (SELECT answers.question_id FROM answers WHERE question_user_id = questions.user_id );
相関サブクエリさて、このクエリは?
負荷の少ないクエリとは?
EXPLAINSELECT * FROM questions WHERE questions.id IN (SELECT question_id FROM answers WHERE question_user_id = 1 );
相関サブクエリさて、このクエリは?
負荷の少ないクエリとは?
EXPLAINSELECT * FROM questions WHERE questions.id IN (SELECT question_id FROM answers WHERE question_user_id = 1 );
| id | select_type || 1 | PRIMARY | | 2 | DEPENDENT SUBQUERY |
相関サブクエリさて、このクエリは?
負荷の少ないクエリとは?
EXPLAINSELECT * FROM questions WHERE questions.id IN (SELECT question_id FROM answers WHERE question_user_id = 1 );
| id | select_type || 1 | PRIMARY | | 2 | DEPENDENT SUBQUERY |
なんで!?
相関サブクエリさて、このクエリは?
負荷の少ないクエリとは?
EXPLAINSELECT * FROM questions WHERE questions.id IN (SELECT question_id FROM answers WHERE question_user_id = 1 );
相関サブクエリさて、このクエリは?
負荷の少ないクエリとは?
EXPLAINSELECT * FROM questions WHERE EXISTS (SELECT 1 FROM answers WHERE question_user_id = 1 AND answers.question_id = questions.id );
相関サブクエリさて、このクエリは?
負荷の少ないクエリとは?
EXPLAINSELECT * FROM questions WHERE EXISTS (SELECT 1 FROM answers WHERE question_user_id = 1 AND answers.question_id = questions.id );
サブクエリの問題点• 可読性が減る• アルゴリズムの変動リスク• インデックス等の環境による遅延リスク
負荷の少ないクエリとは?
type
負荷の少ないクエリとは?
const PRIMARY KEY/UNIQUE を利用するアクセスeq_ref JOIN で PRIMARY KEY/UNIQUE を利用するアクセス
ref PRIMARY KEY/UNIQUE 以外のインデックスで等価検索を利用するアクセス
range インデックスを利用する範囲検索。index インデックス全体をスキャンするアクセス
( フルインデックススキャン )ALL テーブル全体をスキャンするアクセス
( フルテーブルスキャン )
type
負荷の少ないクエリとは?
const PRIMARY KEY/UNIQUE を利用するアクセスeq_ref JOIN で PRIMARY KEY/UNIQUE を利用するアクセス
ref PRIMARY KEY/UNIQUE 以外のインデックスで等価検索を利用するアクセス
range インデックスを利用する範囲検索。index インデックス全体をスキャンするアクセス
( フルインデックススキャン )ALL テーブル全体をスキャンするアクセス
( フルテーブルスキャン )
フルインデックススキャン• Index condition pushdown を狙う高頻度の場合• 実行タイミングを変更–更新時に集計やソートを行う–キュー等を利用して非同期化
• 仕様の変更–件数に制限をかける–別テーブルやデータストアに保存
負荷の少ないクエリとは?
フルテーブルスキャン• インデックスを追加する• Index Condition Pushdown を狙う高頻度の場合• フルインデックススキャンと同じ
負荷の少ないクエリとは?
Index Condition Pushdown(ICP)• MySQL 5.6 から• マルチカラムインデックスを有効活用す
る
負荷の少ないクエリとは?
Index Condition Pushdown(ICP)• MySQL 5.6 から• マルチカラムインデックスを有効活用す
る
負荷の少ないクエリとは?
Index Condition Pushdown(ICP)ICP を使わない場合 (Sex = 1, 10 ≦ Age < 20)
負荷の少ないクエリとは?
Sex Age1 15
1 30
2 18
1 40
2 27
2 13
1 14
1 24
1 50
Id Sex Age
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Index Condition Pushdown(ICP)ICP を使わない場合 (Sex = 1, 10 ≦ Age < 20)
負荷の少ないクエリとは?
Sex Age1 15
1 30
2 18
1 40
2 27
2 13
1 14
1 24
1 50
Id Sex Age
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Index Condition Pushdown(ICP)ICP を使わない場合 (Sex = 1, 10 ≦ Age < 20)
負荷の少ないクエリとは?
Sex Age1 15
1 30
2 18
1 40
2 27
2 13
1 14
1 24
1 50
Id Sex Age
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Index Condition Pushdown(ICP)ICP を使う場合 (Sex = 1, 10 ≦ Age < 20)
負荷の少ないクエリとは?
Sex Age1 15
1 30
2 18
1 40
2 27
2 13
1 14
1 24
1 50
Id Sex Age
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Index Condition Pushdown(ICP)ICP を使う場合 (Sex = 1, 10 ≦ Age < 20)
負荷の少ないクエリとは?
Sex Age1 15
1 30
2 18
1 40
2 27
2 13
1 14
1 24
1 50
Id Sex Age
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Index Condition Pushdown(ICP)ICP を使う場合 (Sex = 1, 10 ≦ Age < 20)
負荷の少ないクエリとは?
Sex Age1 15
1 30
2 18
1 40
2 27
2 13
1 14
1 24
1 50
Id Sex Age
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Index Condition Pushdown(ICP)ICP を使う場合 (Sex = 1, 10 ≦ Age < 20)
負荷の少ないクエリとは?
Sex Age1 15
1 30
2 18
1 40
2 27
2 13
1 14
1 24
1 50
Id Sex Age
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Extra
負荷の少ないクエリとは?
Using where WHERE の検索条件がインデックスだけでは解決出来ない。
Using index インデックスだけで条件を解決できる。Using filesort クイックソートでソートを行っている。
Using temporary 実行にテンポラリテーブルが必要。
Extra
負荷の少ないクエリとは?
Using where WHERE の検索条件がインデックスだけでは解決出来ない。
Using index インデックスだけで条件を解決できる。Using filesort クイックソートでソートを行っている。
Using temporary 実行にテンポラリテーブルが必要。
Using filesort• インデックスを追加する• ソート条件を 1 テーブルに集中させる• ソート実行を早めにさせる
負荷の少ないクエリとは?
Using filesortこのクエリの場合は?
負荷の少ないクエリとは?
EXPLAIN SELECT * FROM users WHERE active = 1 ORDER BY type_id;
+----+-------------+-------+------+---------------+------+---------+------+--------+-----------------------------+| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |+----+-------------+-------+------+---------------+------+---------+------+--------+-----------------------------+| 1 | SIMPLE | users | ALL | NULL | NULL | NULL | NULL | 191660 | Using where; Using filesort |+----+-------------+-------+------+---------------+------+---------+------+--------+-----------------------------+1 row in set (0.00 sec)
Using filesortこのクエリの場合は?
負荷の少ないクエリとは?
mysql> SHOW CREATE TABLE users \G;*************************** 1. row *************************** Table: usersCreate Table: CREATE TABLE `users` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
・・・
PRIMARY KEY (`id`), KEY `index_users_on_old_id` (`old_id`), KEY `index_users_on_provider_and_uid` (`provider`,`uid`), KEY `index_users_on_old_random_key` (`old_random_key`)) ENGINE=InnoDB AUTO_INCREMENT=174849 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci1 row in set (0.00 sec)
ERROR:No query specified
Using filesortこのクエリの場合は?
負荷の少ないクエリとは?
EXPLAIN SELECT * FROM users WHERE active = 1 ORDER BY type_id;
Using temporary• GROUP BY / DISTINCT を見直す• 行数を考慮して JOIN 条件の見直す• サブクエリを検討する• テーブル / カラム追加を検討する
負荷の少ないクエリとは?
Using temporary• GROUP BY / DISTINCT を見直す• 行数を考慮して JOIN 条件の見直す• サブクエリを検討する• テーブル / カラム追加を検討する
負荷の少ないクエリとは?
高速化の仕組み• インデックス• パーティション
高速化の仕組み
インデックスとは?• 辞書の索引を付けること• B-tree を利用している• 正確には最下層にデータ保持する
B+tree
高速化の仕組み
B-tree とは ?定義• 根は葉であるか 2~m の子をもつ• 根・葉以外の節は m/2 以上の子をもつ• 根から葉までの深さが等しい
高速化の仕組み
B-tree とは ?探索方法• 最左値より小さければ最左部分木へ進む• 最左値より大きければ次の値と比較し、
小さければ最左の次の部分木へ進む• 上記を反復する
高速化の仕組み
B-tree とは ?
高速化の仕組み
10
B-tree とは ?
高速化の仕組み
10
5
B-tree とは ?
高速化の仕組み
5 10
2020
B-tree とは ?
高速化の仕組み
205
10
47
B-tree とは ?
高速化の仕組み
20 475
10
5050
B-tree とは ?
高速化の仕組み
505
10 47
20
7
B-tree とは ?
高速化の仕組み
505 7
10 47
20
32
B-tree とは ?
高速化の仕組み
9505 7
10 47
20 329
B-tree とは ?
50
10 47
20 32
5 9
7
高速化の仕組み
B-tree とは ?
50
10 47
20 32
5 9
7
高速化の仕組み
B-tree とは ?
50
10 47
20 32
5 9
7
高速化の仕組み
B-tree とは ?
高速化の仕組み
5 9 20 32 50
7
10
47
MySQL(InnoDB) のインデックス探索• リーフページが大量の場合は打ち切り
ルートから探索を行う• 9ページまで読み打ち切る (MySQL
5.6.4)• リーフページサイズは 16KB
高速化の仕組み
パーティションとは?• 水平分割(行による分割)
高速化の仕組み
リスク• 分割方法によっては速度が遅くなる• 既存のクエリに分割キー条件を
追加しなければならない場合がある• 分割キーを PK にする必要がある• パーティション変更はサービス停止が必
要
高速化の仕組み
リターン• メモリへの読み込みが早くなる• 上記に伴いディスクアクセスも少なくな
る• 可用性の高い設計になる
高速化の仕組み
テルミーの例新着が遅くなった• 元々 created_at で分割をしていた• クエリに時間による制限や
ソート以外の条件が多かった• 上記の理由で ID による分割に変更
高速化の仕組み
テルミーの例新着が遅くなった
高速化の仕組み
EXPLAIN PARTITIONSSELECT `questions`.* FROM `questions` WHERE `questions`.`active` = 1 ORDER BY created_atLIMIT 10 OFFSET 0 \G;
テルミーの例新着が遅くなった
高速化の仕組み
id: 1 select_type: SIMPLE table: questions partitions: p0,p1,p2,p3,…,p509,p510,p511 type: ALLpossible_keys: NULL key: NULL key_len: NULL ref: NULL rows: 1179993 Extra: Using where; Using filesort
テルミーの例新着が遅くなった• ID によるソートを行うように• 取得時に ID の範囲を指定するように
高速化の仕組み
テルミーの例新着が遅くなった
高速化の仕組み
EXPLAIN PARTITIONSSELECT `questions`.* FROM `questions` WHERE `questions`.`active` = 1 AND (id > 3) AND (id < 14) LIMIT 10 OFFSET 0 \G;
テルミーの例新着が遅くなった
高速化の仕組み
EXPLAIN PARTITIONSSELECT `questions`.* FROM `questions` WHERE `questions`.`active` = 1 AND (id > 3) AND (id < 14) LIMIT 10 OFFSET 0 \G;
id: 1 select_type: SIMPLE table: questions partitions: p4,p5,p6,p7,p8,p9,p10,p11,p12,p13 type: rangepossible_keys: PRIMARY,index_questions_on_id key: PRIMARY key_len: 4 ref: NULL rows: 9 Extra: Using where1 row in set (0.00 sec)
テルミーの例新着が遅くなった
高速化の仕組み
id: 1 select_type: SIMPLE table: questions partitions: p4,p5,p6,p7,p8,p9,p10,p11,p12,p13 type: rangepossible_keys: PRIMARY,index_questions_on_id key: PRIMARY key_len: 4 ref: NULL rows: 9 Extra: Using where1 row in set (0.00 sec)
Rails でのチューニング• ActiveRecord が生成する SQL を知る
Rails でのチューニング
Rails でのチューニング• ActiveRecord が生成する SQL を知る• Eager Loading を利用する
Rails でのチューニング
Rails でのチューニング• ActiveRecord が生成する SQL を知る• Eager Loading を利用する• 更新時コールバックでの集計• バックグラウンドでの集計
Rails でのチューニング
ActiveRecord が生成する SQL を知る
Rails でのチューニング
ActiveRecord が生成する SQL を知るto_sql
Rails でのチューニング
pry(main)> Question.uniq.to_sql
ActiveRecord が生成する SQL を知るto_sql
Rails でのチューニング
pry(main)> Question.uniq.to_sql=> "SELECT DISTINCT `questions`.* FROM `questions` "
ActiveRecord が生成する SQL を知るexplain
Rails でのチューニング
pry(main)> Question.uniq.explain
ActiveRecord が生成する SQL を知るexplain
Rails でのチューニング
pry(main)> Question.uniq.explain Question Load (19503.4ms) SELECT DISTINCT `questions`.* FROM `questions` EXPLAIN (9.4ms) EXPLAIN SELECT DISTINCT `questions`.* FROM `questions`=> "EXPLAIN for: SELECT DISTINCT `questions`.* FROM `questions` \n+----+-------------+-----------+------+---------------+------+---------+------+--------+-------+\n| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |\n+----+-------------+-----------+------+---------------+------+---------+------+--------+-------+\n| 1 | SIMPLE | questions | ALL | NULL | NULL | NULL | NULL | 195195 | |\n+----+-------------+-----------+------+---------------+------+---------+------+--------+-------+\n1 row in set (0.01 sec)\n"
ActiveRecord が生成する SQL を知る実装を行う前に pry 等で• 生成されるクエリ (to_sql)• 実行計画 (explain)を確認する。
Rails でのチューニング
ActiveRecord が生成する SQL を知るNewRelic• Transaction tracing• Slow SQLの設定をオンにすれば
Rails でのチューニング
ActiveRecord が生成する SQL を知るNewRelic
Rails でのチューニング
ActiveRecord が生成する SQL を知るrack-mini-profiler• https://github.com/MiniProfiler/rack-
mini-profiler
• 環境毎に gem を入れるだけで簡単
Rails でのチューニング
ActiveRecord が生成する SQL を知るrack-mini-profiler
Rails でのチューニング
ActiveRecord が生成する SQL を知るmysqldumpslow• スローログを合計時間で集計してくれる• ログがあればローカルで実行できる• gzip も読み込んでくれる• MySQL デフォルト
Rails でのチューニング
ActiveRecord が生成する SQL を知るmysqldumpslow
Rails でのチューニング
$ mysqldumpslow -t 5 -s t mysql-slow.log*
Reading mysql slow query log from mysql-slow.log
Count: 14591 Time=1.57s (22851s) Lock=0.00s (5s) Rows=10.0 (145910), rio_slave[rio_slave]@8hosts SELECT `questions`.* FROM `questions` WHERE `questions`.`display` = 'S' AND `questions`.`to_user_id` IS NULL AND `questions`.`active` = N AND (answer_last_updated_at != created_at) AND (questions.created_at >= 'S') AND (ng_word IN (N, N)) ORDER BY answer_last_updated_at desc LIMIT N OFFSET N
…
Eager Loading を利用する
Rails でのチューニング
Eager Loading を利用するEager Loading とは?• 「積極的に読み込む」• 先にデータを取得しておくこと• インスタンス変数による自動キャッシュ
Rails でのチューニング
Eager Loading を利用する
Rails でのチューニング
Question.limit(10).each do |q| p “#{q.user} posted #{q.content}”end
Eager Loading を利用する
Rails でのチューニング
Question.limit(10).each do |q| p “#{q.user} posted #{q.content}”end Question Load (9.8ms) SELECT `questions`.* FROM `questions` User Load (5.2ms) SELECT `users`.* FROM `users` WHERE User Load (4.1ms) SELECT `users`.* FROM `users` WHERE User Load (4.8ms) SELECT `users`.* FROM `users` WHERE User Load (7.9ms) SELECT `users`.* FROM `users` WHERE User Load (5.7ms) SELECT `users`.* FROM `users` WHERE User Load (5.4ms) SELECT `users`.* FROM `users` WHERE User Load (4.5ms) SELECT `users`.* FROM `users` WHERE User Load (4.4ms) SELECT `users`.* FROM `users` WHERE User Load (4.2ms) SELECT `users`.* FROM `users` WHERE User Load (4.4ms) SELECT `users`.* FROM `users` WHERE
Eager Loading を利用する
Rails でのチューニング
Question.includes(:user).limit(10).each do |q| p “#{q.user} posted #{q.content}”end Question Load (9.8ms) SELECT `questions`.* FROM `questions` User Load (5.2ms) SELECT `users`.* FROM `users` WHERE User Load (4.1ms) SELECT `users`.* FROM `users` WHERE User Load (4.8ms) SELECT `users`.* FROM `users` WHERE User Load (7.9ms) SELECT `users`.* FROM `users` WHERE User Load (5.7ms) SELECT `users`.* FROM `users` WHERE User Load (5.4ms) SELECT `users`.* FROM `users` WHERE User Load (4.5ms) SELECT `users`.* FROM `users` WHERE User Load (4.4ms) SELECT `users`.* FROM `users` WHERE User Load (4.2ms) SELECT `users`.* FROM `users` WHERE User Load (4.4ms) SELECT `users`.* FROM `users` WHERE
Eager Loading を利用する
Rails でのチューニング
Question.includes(:user).limit(10).each do |q| p “#{q.user} posted #{q.content}”end Question Load (6.9ms) SELECT `questions`.* FROM `questions` User Load (5.0ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN (724, 1402, 2277, 3154, 3696, 4180, 4551, 5375, 6090, 6890)
Eager Loading を利用する
Rails でのチューニング
Question.includes(:user).limit(10).each do |q| p “#{q.user} posted #{q.content}”end Question Load (6.9ms) SELECT `questions`.* FROM `questions` User Load (5.0ms) SELECT `users`.* FROM `users` WHERE `users`.`id` IN (724, 1402, 2277, 3154, 3696, 4180, 4551, 5375, 6090, 6890)
Eager Loading を利用する
Rails でのチューニング
# 指定リレーショナルオブジェクトを先読みQuestion.includes(:user) Question.includes(:user, :category)
# 指定リレーションオブジェクトのフィールドを先読みQuestion.includes(category: [:parent_category])Question.includes(category: [:parent_category, :answer_good_logs])
# 更に入れ子にも出来ます・・・
Eager Loading を利用する
Rails でのチューニング
# 指定リレーショナルオブジェクトを先読みQuestion.includes(:user) Question.includes(:user, :category)
# 指定リレーションオブジェクトのフィールドを先読みQuestion.includes(category: [:parent_category])Question.includes(category: [:parent_category, :answer_good_logs])
# 更に入れ子にも出来ます・・・
Eager Loading チューニングのフロー1. キャッシュを切る2. rack-mini-profiler で大量発行 SQL を探
す3. 対象のコントローラを探す4. Includes を追加する5. rack-mini-profiler で確認
Rails でのチューニング
Eager Loading チューニングのフロースローログに乗らない細かい SQL も大きな負荷削減になることが結構有ります
Rails でのチューニング
インスタンス変数によるキャッシュ
Rails でのチューニング
インスタンス変数によるキャッシュ1接続内に複数回 SQL が発行される場合
Rails でのチューニング
Question.first.tap do |q| 10.times { "answers count is #{q.answers.count}" }end Question Load (8.0ms) SELECT `questions`.* FROM `questions` (4.2ms) SELECT COUNT(*) FROM `answers` WHERE (3.6ms) SELECT COUNT(*) FROM `answers` WHERE (5.3ms) SELECT COUNT(*) FROM `answers` WHERE (3.8ms) SELECT COUNT(*) FROM `answers` WHERE (3.8ms) SELECT COUNT(*) FROM `answers` WHERE (5.7ms) SELECT COUNT(*) FROM `answers` WHERE (3.8ms) SELECT COUNT(*) FROM `answers` WHERE (3.4ms) SELECT COUNT(*) FROM `answers` WHERE (3.8ms) SELECT COUNT(*) FROM `answers` WHERE (5.4ms) SELECT COUNT(*) FROM `answers` WHERE
インスタンス変数によるキャッシュ1接続内に複数回 SQL が発行される場合
Rails でのチューニング
class Question < ActiveRecord::Base
・・・
def answers_count @answers_count ||= self.answers.count end
・・・
end
インスタンス変数によるキャッシュ1接続内に複数回 SQL が発行される場合
Rails でのチューニング
Question.first.tap do |q| 10.times { "answers count is #{q.answers_count}" }end Question Load (5.1ms) SELECT `questions`.* FROM `questions` (3.6ms) SELECT COUNT(*) FROM `answers` WHERE
更新時コールバックでの集計• カウント集計
• 最後に更新のあった解答
Rails でのチューニング
更新時コールバックでの集計• カウント集計–更新時に親へカウントアップ /ダウン
• 最後に更新のあった投稿–更新時に親へ保存
Rails でのチューニング
更新時コールバックでの集計どの様なテーブル設計の場合なの?
Rails でのチューニング
Users (Master)
name
…
Posts (Transaction)
user_id
content
…1:n
更新時コールバックでの集計どの様なテーブル設計の場合なの?
Rails でのチューニング
Users (Master)
name
…
Posts (Transaction)
user_id
content
…
UserPostStatus (Transaction)
user_id
last_post_id
posted_count
…
1:n
更新時コールバックでの集計どの様なテーブル設計の場合なの?
Rails でのチューニング
Users (Master)
name
…
Posts (Transaction)
user_id
content
…
UserPostStatus (Transaction)
user_id
last_post_id
posted_count
…
1:1
1:n
更新時コールバックでの集計どの様なテーブル設計の場合なの?
Rails でのチューニング
Users (Master)
name
…
Posts (Transaction)
user_id
content
…
UserPostStatus (Transaction)
user_id
last_post_id
posted_count
…
1:1 1:1
1:n
更新時コールバックでの集計どの様なテーブル設計の場合なの?
Rails でのチューニング
Users (Master)
name
…
Posts (Transaction)
user_id
content
…
UserPostStatus (Transaction)
user_id
last_post_id
posted_count
…都度カウントアップ /カウントダウン都度更新
1:1 1:1
1:n
更新時コールバックでの集計どの様なテーブル設計の場合なの?
Rails でのチューニング
Users (Transaction)
name
last_post_id
posted_count
…
1:1 or nPosts (Transaction)
user_id
content
…
更新時コールバックでの集計どの様に更新処理を行うのか?
Rails でのチューニング
class User < ActiveRecord::Base attr_accessible :last_post_id belongs_to :last_post, class_name: Post.to_s, foreign_key: :last_answer_id
def update_last_post(post = nil) new_last_post = post || Post.last_post(self.id).first self.update_column :last_post_id, new_last_post.try(:id) end
・・・
end
更新時コールバックでの集計どの様に更新処理を行うのか?
Rails でのチューニング
class User < ActiveRecord::Base attr_accessible :last_post_id belongs_to :last_post, class_name: Post.to_s, foreign_key: :last_answer_id
def update_last_post(post = nil) new_last_post = post || Post.last_post(self.id).first self.update_column :last_post_id, new_last_post.try(:id) end
・・・
end
更新時コールバックでの集計どの様に更新処理を行うのか?
Rails でのチューニング
class Post < ActiveRecord::Base
after_save { user.update_last_post(self) }
・・・
end
更新時コールバックでの集計注意点• 冗長性が高いものであること• 再集計が可能であること
Rails でのチューニング
総括• データストアの知識が重要• プロファイルでボトルネックを探せるか• メンテナンス性とトレードオフにならない
ように設計• データベース設計を変更する勇気を持とう
総括
参考文献漢のコンピュータ道http://nippondanji.blogspot.jp/2009/03/mysqlexplain.htmlhttp://nippondanji.blogspot.jp/2009/03/using-filesort.htmlhttp://nippondanji.blogspot.jp/2012/10/mysql-56.htmlMySQL Practive Wikihttp://www.mysqlpracticewiki.com/index.php/Extra_fieldSH2 の日記http://d.hatena.ne.jp/sh2/20111217十番目のムーサhttp://d.hatena.ne.jp/psappho/20111101/1320152348 MySQL SQL オプティマイザのコスト計算アルゴリズムhttp://dbstudy.info/files/20120310/mysql_costcalc.pdfMySQL Reference Manualshttp://dev.mysql.com/doc/
総括