SlideShare a Scribd company logo
Ruby on Rails
on MySQL
チューニング入門

佐藤大資 (@eccyan)
Ruby on Rails を使えば簡単に誰でも
Web サービスを構築する事が出来ます。
Ruby on Rails を使えば簡単に誰でも
Web サービスを構築する事が出来ます。
しかし、SQLを理解せずに
Active Record を利用していると・・・
Ruby on Rails on MySQL チューニング入門
そこで今回は、
そこで今回は、
そこで今回は、
そこで今回は、

に絞ってチューニングの
取り掛り方法をお伝えします。
ActiveRecordって何?

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って何?

ストアドプロシージャ?
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 ;
ActiveRecordって何?

ストアドプロシージャ?
• 処理速度は早いが、負荷集中する
• Master-Slave 構成の場合、更新系の
負荷分散が出来ない
• RDBMS毎に構文やノウハウが変わる
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'];
}
ActiveRecordって何?

オレオレライブラリ?
• 属人性が高い
• OSSに比べて枯れていない
• プラグマブルで無く、代替出来ない
ActiveRecordって何?

ARで再利用可能なコードを書こう
• Arel
• Relation
• Scope
ActiveRecordって何?

Scope
scope :active, where(active: true)
scope :inactive, where(active: false)
ActiveRecordって何?

Scope
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を発行し取得する事
• 遅いのは相関関係のあるサブクエリ
負荷の少ないクエリとは?

相関サブクエリ
クエリとサブクエリが相互関係している
EXPLAIN
SELECT * FROM questions
WHERE questions.id IN
(SELECT answers.question_id FROM answers
WHERE question_user_id = questions.user_id
);
負荷の少ないクエリとは?

相関サブクエリ
クエリとサブクエリが相互関係している
EXPLAIN
SELECT * FROM questions
WHERE questions.id IN
(SELECT answers.question_id FROM answers
WHERE question_user_id = questions.user_id
);
負荷の少ないクエリとは?

相関サブクエリ
さて、このクエリは?
EXPLAIN
SELECT * FROM questions
WHERE questions.id IN
(SELECT question_id FROM answers
WHERE question_user_id = 1
);
負荷の少ないクエリとは?

相関サブクエリ
さて、このクエリは?
EXPLAIN
SELECT * FROM questions
WHERE questions.id IN
(SELECT question_id FROM answers
WHERE question_user_id = 1
);
| id | select_type
|
| 1 | PRIMARY
|
| 2 | DEPENDENT SUBQUERY |
負荷の少ないクエリとは?

相関サブクエリ
さて、このクエリは?
EXPLAIN
SELECT * FROM questions
WHERE questions.id IN
(SELECT question_id FROM answers
WHERE question_user_id = 1
);
| id | select_type
|
| 1 | PRIMARY
|
| 2 | DEPENDENT SUBQUERY |
負荷の少ないクエリとは?

相関サブクエリ
さて、このクエリは?
EXPLAIN
SELECT * FROM questions
WHERE questions.id IN
(SELECT question_id FROM answers
WHERE question_user_id = 1
);
負荷の少ないクエリとは?

相関サブクエリ
さて、このクエリは?
EXPLAIN
SELECT * FROM questions
WHERE EXISTS
(SELECT 1 FROM answers
WHERE question_user_id = 1
AND answers.question_id = questions.id
);
負荷の少ないクエリとは?

相関サブクエリ
さて、このクエリは?
EXPLAIN
SELECT * 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

Age

Id

1

15

2

1

30

2

18

1

40

2

27

2

13

1

3
4
5
6

7
8
9
10
11

1

14

1

24

13

1

50

15

12

14

16

Sex

Age
負荷の少ないクエリとは?

Index Condition Pushdown(ICP)
ICPを使わない場合(Sex = 1, 10 ≦ Age < 20)
Sex

Age

Id

1

15

2

1

30

2

18

1

40

2

27

2

13

1

3
4
5
6

7
8
9
10
11

1

14

1

24

13

1

50

15

12

14

16

Sex

Age
負荷の少ないクエリとは?

Index Condition Pushdown(ICP)
ICPを使わない場合(Sex = 1, 10 ≦ Age < 20)
Sex

Age

Id

1

15

2

1

30

2

18

1

40

2

27

2

13

1

3
4
5
6

7
8
9
10
11

1

14

1

24

13

1

50

15

12

14

16

Sex

Age
負荷の少ないクエリとは?

Index Condition Pushdown(ICP)
ICPを使う場合(Sex = 1, 10 ≦ Age < 20)
Sex

Age

Id

1

15

2

1

30

2

18

1

40

2

27

2

13

1

3
4
5
6

7
8
9
10
11

1

14

1

24

13

1

50

15

12

14

16

Sex

Age
負荷の少ないクエリとは?

Index Condition Pushdown(ICP)
ICPを使う場合(Sex = 1, 10 ≦ Age < 20)
Sex

Age

Id

1

15

2

1

30

2

18

1

40

2

27

2

13

1

3
4
5
6

7
8
9
10
11

1

14

1

24

13

1

50

15

12

14

16

Sex

Age
負荷の少ないクエリとは?

Index Condition Pushdown(ICP)
ICPを使う場合(Sex = 1, 10 ≦ Age < 20)
Sex

Age

Id

1

15

2

1

30

2

18

1

40

2

27

2

13

1

3
4
5
6

7
8
9
10
11

1

14

1

24

13

1

50

15

12

14

16

Sex

Age
負荷の少ないクエリとは?

Index Condition Pushdown(ICP)
ICPを使う場合(Sex = 1, 10 ≦ Age < 20)
Sex

Age

Id

1

15

2

1

30

2

18

1

40

2

27

2

13

1

3
4
5
6

7
8
9
10
11

1

14

1

24

13

1

50

15

12

14

16

Sex

Age
負荷の少ないクエリとは?

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: users
Create 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_ci
1 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とは?

5

10
高速化の仕組み

B-treeとは?

20

5

10

20
高速化の仕組み

B-treeとは?
10

47
5

20
高速化の仕組み

B-treeとは?
10

50
5

20

47

50
高速化の仕組み

B-treeとは?
10

7
5

47

20

50
高速化の仕組み

B-treeとは?
10

32
5

7

47

20

50
高速化の仕組み

B-treeとは?
10

9
5

7

9

47

20

32

50
高速化の仕組み

B-treeとは?
10

7

5

9

47

20

32

50
高速化の仕組み

B-treeとは?
10

7

5

9

47

20

32

50
高速化の仕組み

B-treeとは?
10

7

5

9

47

20

32

50
高速化の仕組み

B-treeとは?
10

7

5

9

47

20

32

50
高速化の仕組み

MySQL(InnoDB)のインデックス探索
• リーフページが大量の場合は打ち切り
ルートから探索を行う
• 9ページまで読み打ち切る(MySQL 5.6.4)
• リーフページサイズは16KB
高速化の仕組み

パーティションとは?
• 水平分割(行による分割)
高速化の仕組み

リスク
• 分割方法によっては速度が遅くなる
• 既存のクエリに分割キー条件を
追加しなければならない場合がある
• 分割キーをPKにする必要がある
• パーティション変更はサービス停止が必
要
高速化の仕組み

リターン
• メモリへの読み込みが早くなる
• 上記に伴いディスクアクセスも少なくな
る
• 可用性の高い設計になる
高速化の仕組み

テルミーの例
新着が遅くなった
• 元々 created_at で分割をしていた
• クエリに時間による制限や
ソート以外の条件が多かった
• 上記の理由でIDによる分割に変更
高速化の仕組み

テルミーの例
新着が遅くなった
EXPLAIN PARTITIONS
SELECT `questions`.* FROM `questions`
WHERE `questions`.`active` = 1
ORDER BY created_at
LIMIT 10 OFFSET 0 G;
高速化の仕組み

テルミーの例
新着が遅くなった
id: 1
select_type: SIMPLE
table: questions
partitions: p0,p1,p2,p3,…,p509,p510,p511
type: ALL
possible_keys: NULL
key: NULL
key_len: NULL
ref: NULL
rows: 1179993
Extra: Using where; Using filesort
高速化の仕組み

テルミーの例
新着が遅くなった
• IDによるソートを行うように
• 取得時にIDの範囲を指定するように
高速化の仕組み

テルミーの例
新着が遅くなった
EXPLAIN PARTITIONS
SELECT `questions`.* FROM `questions`
WHERE `questions`.`active` = 1
AND (id > 3) AND (id < 14)
LIMIT 10 OFFSET 0 G;
高速化の仕組み

テルミーの例
新着が遅くなった
EXPLAIN PARTITIONS
SELECT `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: range
possible_keys: PRIMARY,index_questions_on_id
key: PRIMARY
key_len: 4
ref: NULL
rows: 9
Extra: Using where
1 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: range
possible_keys: PRIMARY,index_questions_on_id
key: PRIMARY
key_len: 4
ref: NULL
rows: 9
Extra: Using where
1 row in set (0.00 sec)
Railsでのチューニング

Railsでのチューニング
• ActiveRecordが生成するSQLを知る
Railsでのチューニング

Railsでのチューニング
• ActiveRecordが生成するSQLを知る
• Eager Loadingを利用する
Railsでのチューニング

Railsでのチューニング
• ActiveRecordが生成するSQLを知る
• Eager Loadingを利用する
• 更新時コールバックでの集計
• バックグラウンドでの集計
Railsでのチューニング

ActiveRecordが生成するSQLを知る
Railsでのチューニング

ActiveRecordが生成するSQLを知る
to_sql
pry(main)> Question.uniq.to_sql
Railsでのチューニング

ActiveRecordが生成するSQLを知る
to_sql
pry(main)> Question.uniq.to_sql
=> "SELECT DISTINCT `questions`.* FROM `questions` "
Railsでのチューニング

ActiveRecordが生成するSQLを知る
explain
pry(main)> Question.uniq.explain
Railsでのチューニング

ActiveRecordが生成するSQLを知る
explain
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"
Railsでのチューニング

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-miniprofiler
• 環境毎にgemを入れるだけで簡単
Railsでのチューニング

ActiveRecordが生成するSQLを知る
rack-mini-profiler
Railsでのチューニング

ActiveRecordが生成するSQLを知る
mysqldumpslow
• スローログを合計時間で集計してくれる
• ログがあればローカルで実行できる
• gzipも読み込んでくれる
• MySQL デフォルト
Railsでのチューニング

ActiveRecordが生成するSQLを知る
mysqldumpslow
$ 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
…
Railsでのチューニング

Eager Loadingを利用する
Railsでのチューニング

Eager Loadingを利用する
Eager Loading とは?
• 「積極的に読み込む」
• 先にデータを取得しておくこと
• インスタンス変数による自動キャッシュ
Railsでのチューニング

Eager Loadingを利用する
Question.limit(10).each do |q|
p “#{q.user} posted #{q.content}”
end
Railsでのチューニング

Eager Loadingを利用する
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
Railsでのチューニング

Eager Loadingを利用する
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
Railsでのチューニング

Eager Loadingを利用する
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)
Railsでのチューニング

Eager Loadingを利用する
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)
Railsでのチューニング

Eager Loadingを利用する
# 指定リレーショナルオブジェクトを先読み
Question.includes(:user)
Question.includes(:user, :category)
# 指定リレーションオブジェクトのフィールドを先読み
Question.includes(category: [:parent_category])
Question.includes(category:
[:parent_category, :answer_good_logs])

# 更に入れ子にも出来ます
・・・
Railsでのチューニング

Eager Loadingを利用する
# 指定リレーショナルオブジェクトを先読み
Question.includes(:user)
Question.includes(:user, :category)
# 指定リレーションオブジェクトのフィールドを先読み
Question.includes(category: [:parent_category])
Question.includes(category:
[:parent_category, :answer_good_logs])

# 更に入れ子にも出来ます
・・・
Railsでのチューニング

Eager Loadingチューニングのフロー
1. キャッシュを切る
2. rack-mini-profilerで大量発行SQLを探す
3. 対象のコントローラを探す
4. Includes を追加する
5. rack-mini-profilerで確認
Railsでのチューニング

Eager Loadingチューニングのフロー
スローログに乗らない細かいSQLも
大きな負荷削減になることが結構有ります
Railsでのチューニング

インスタンス変数によるキャッシュ
Railsでのチューニング

インスタンス変数によるキャッシュ
1接続内に複数回SQLが発行される場合
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
Railsでのチューニング

インスタンス変数によるキャッシュ
1接続内に複数回SQLが発行される場合
class Question < ActiveRecord::Base
・・・
def answers_count
@answers_count ||= self.answers.count
end
・・・
end
Railsでのチューニング

インスタンス変数によるキャッシュ
1接続内に複数回SQLが発行される場合
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)

Posts (Transaction)

name

user_id

email
…

1:n

content
…
Railsでのチューニング

更新時コールバックでの集計
どの様なテーブル設計の場合なの?
Users (Master)

Posts (Transaction)

name

user_id

email

1:n

…

content
…

UserPostStatus (Transaction)
user_id
last_post_id
posted_count
…
Railsでのチューニング

更新時コールバックでの集計
どの様なテーブル設計の場合なの?
Users (Master)

Posts (Transaction)

name

user_id

email

1:n

…

content
…

UserPostStatus (Transaction)
1:1

user_id
last_post_id
posted_count
…
Railsでのチューニング

更新時コールバックでの集計
どの様なテーブル設計の場合なの?
Users (Master)

Posts (Transaction)

name

user_id

email

1:n

…

content
…

UserPostStatus (Transaction)
1:1

user_id
last_post_id
posted_count
…

1:1
Railsでのチューニング

更新時コールバックでの集計
どの様なテーブル設計の場合なの?
Users (Master)

Posts (Transaction)

name

user_id

email

1:n

…

content
…

UserPostStatus (Transaction)
1:1

user_id

1:1

last_post_id

都度更新

posted_count

都度カウントアップ/カウントダウン

…
Railsでのチューニング

更新時コールバックでの集計
どの様なテーブル設計の場合なの?
Users (Transaction)
name

Posts (Transaction)
1:1 or n

user_id

email

content

last_post_id

…

posted_count
…
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.html
http://nippondanji.blogspot.jp/2009/03/using-filesort.html
http://nippondanji.blogspot.jp/2012/10/mysql-56.html
MySQL Practive Wiki
http://www.mysqlpracticewiki.com/index.php/Extra_field
SH2の日記
http://d.hatena.ne.jp/sh2/20111217
十番目のムーサ
http://d.hatena.ne.jp/psappho/20111101/1320152348
MySQL SQLオプティマイザのコスト計算アルゴリズム
http://dbstudy.info/files/20120310/mysql_costcalc.pdf
MySQL Reference Manuals
http://dev.mysql.com/doc/

More Related Content

Ruby on Rails on MySQL チューニング入門