isuconのために
TTPしたいこと
Gotanda.pm #2
who am i
- @kfly8
- Application engineer
	- like Perl, JavaScript
- develop games
 

今日の話は 自称isuconファンの話です
まだの方 5秒待ちます
唐突ですが
Question
「isuconで100万円をとる銀の弾丸はある?」
Answer
「@kazeburoさん、@fujiwaraさんと同じチームになる?!」
...
!!
まねしよう!!
TTP
徹底的にパkる
先輩の知恵を拝借させてもらう
5位以内でも賞金が出るらしい
制限時間内に基本的なことが出来たら、
もしかしたら、、、
もしかしたら!!
まず初めに感謝したいこと
感謝(2)
- ウェブアプリケーションのプロファイリングの仕方
- Explain
- SQLアンチパターン
isuconによって多くの素敵な知見が
得られています
ありがとうございます!!
アジェンダ
- 事前準備
- 当日準備
- 今日やらないこと
- TTPしたい知見
事前準備
- [x] 参加申し込み
- [ ] レギュレーション良く読む- http://isucon.net/archives/39979344.html
- 採点方式(減点式?加点式?) (制限 1秒以内に返せばいい?)
- 環境 (AWS, OSなど)
 
参加申し込みはさっきしたはず...
当日準備
- [ ] リポジトリ管理- appicationと設定ファイルをgit管理にする
 
- [ ] 公開鍵登録
- [ ] hosts登録
- [ ] ベンチマークツール- 仕様、アクセスの傾向
 
- [ ] プロファイリング- fujiwaraさんの発表
- アクセスの傾向、重いページ、クエリ実行計画、etc...
 
今日やらないこと
- middlewareの調整
- ネットワークの調整
- nginx luaとか
- golangとか
TTPしたい知見
- [ ] SELECT *やめる
- [ ] N+1問題
- [ ] add index / drop index
- [ ] covering index
- [ ] count
- [ ] Cache
TTPしたい知見
- 
SELECT *やめる
- N+1問題 - for { query } -> join? IN句? Perlで組み立て
 
- add index / drop index
- covering index - IN句, subquery
 
- count - counter table / MAX(id)
 
- Cache - memcached, redis / Query cache, Template cache
 
もうこれで瞬間的にコードが頭に浮かぶなら
TTPできている!!たぶん。
SELECT * やめる
- BEFORESELECT * FROM memos WHERE ...
- 
AFTER SELECT id, content, created_at FROM memos WHERE ...
- 
不要なトラフィック発生しないようにする 
ORM 使ってても、同様
- schemaから自動で設定してくれているので、明示的に利用したい時使う
my $memos = $teng->search(memos => { ... }, {
    columns => [qw/id content created_at/],
});https://github.com/cho45/p5-Teng/blob/master/lib/Teng.pm#L486
- パフォーマンスだけじゃなく、運用上もやだ
SQLアンチパターン 暗黙の列
- 運用時にADD COLUMNした時に、* だとアプリケーションの挙動に影響でる
大きなデータカラムは
抜粋できないか考える
SELECT id, SUBSTRING_INDEX(content, "\n", 1) AS title, created_at FROM memos WHERE ...- 参照頻度が高いなら事前に必要最低限のカラムを用意ALTER TABLE memos ADD COLUNN title ..;
N+1問題
for my $memo (@$memos) {
    $memo->{username} = $self->dbh->select_one(
        'SELECT username FROM users WHERE id=?',
        $memo->{user},
    );
}- 実装しやすい(良い意味でも悪い意味でも)
- Nの大きさに線形比例して、性能劣化
対策(1) JOIN
    SELECT
        memos.id, memos.content, memos.created_at,
        users.username
    FROM
        memos, users
    WHERE
        memos.user_id = users.id
        ...対策(2) 非正規化
- memosに必要なデータを入れておく非正規化
- usernameの更新に注意
- こそ泥チューニング
    INSERT memos (content, username) VALUES ($content, $user.username);
    SELECT
        id, content, created_at,
        username
    FROM
        memos
    WHERE
        ...対策(3) Perlで組み立て
    my $memos = $self->dbh->select_all(
        sprintf("SELECT id, content, username, created_at FROM memos WHERE is_private=0 ORDER BY created_at DESC, id DESC LIMIT 100 OFFSET %d", $page * 100)
    );    my @user_ids = uniq map { $_->{user} } @$memos;
    my %user_id2username = @{$self->dbh->selectcol_arrayref(
        qq/SELECT id, username FROM users WHERE id IN ( @{[join ',', ('?')x@user_ids]} )/,
        {Columns => [1,2]},
        @user_ids,
    )};
    # -> ( 1 => 'username1', 2 => 'username2' )- 組データの、selectcol_arrayref のColumns => [1,2] からの array -> hash は(bk) idiom
- documentに載ってる
余談 selectall_hashrefも好きだ!
my $user_id2data = $self->dbh->selectall_hashref(
    qq/SELECT id, username, created_at FROM users WHERE id IN ( @{[join ',', ('?')x@user_ids]} )/,
    'id',
    {},
    @user_ids,
);
# ->
{
    1 => { id => 1, username => 'username1', created_at => '2014-09-17 19:00:00' },
    2 => { id => 2, username => 'username1', created_at => '2014-09-17 19:00:00' },
}add index
drop index
実行計画をカジュアルに見て、明らかに変なのをあぶり出す
use DBIx::QueryLog
DBIx::QueryLog->explain(1);# フルテーブルスキャーン!
tail -f access.log | grep -B 4 ALL 
# rowsもあわせてみたい
tail -f access.log | grep -B 4 filesort
tail -f access.log | grep -B 4 temporary[2014-09-17T01:06:13] [DBIx::Sunny::db] [0.003505] SELECT * FROM memos WHERE is_private=0 ORDER BY created_at DESC, id DESC LIMIT 100 at /Users/kfly8/lab/isucon3/qualifier/webapp/perl/local/lib/perl5/DBIx/Sunny.pm line 142
.--------------------------------------------------------------------------------------------------------------.
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra                       |
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+
|  1 | SIMPLE      | memos | ALL  | NULL          | NULL | NULL    | NULL |  202 | Using where; Using filesort |もっとくわしく
- アプリケーションでの占有時間を確認
- 一発のクエリが軽くても、頻度次第でよろしくない
- pt-query-digest && tcpdump とかクエリの解析ツールを使う
Explain して想定通りにindexが使われているか、都度確認
わすれがち、やりがちなこと
必要だったインデックスが
コードの変化でゴミになることもある
関連
SQLアンチパターン インデックスショットガン
- 雑に言うと「インデックスガンガン貼ってこうぜ!」っていうアンチパターン
covering index
- データが入っているleaf nodeにアクセスしないように、クエリの実行計画を立てる
- InnoDBがクラスタインデックスの構造だから預かれる恩恵
- kazeburoさんの資料 49pageから参照
- これ以上ないくらい分かりやすい!!!!!!!
isucon3 qualifier での例
よくないクエリ
mysql> EXPLAIN SELECT * FROM memos WHERE is_private = 0 ORDER BY created_at LIMIT 100 OFFSET 10000;
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra                       |
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+
|  1 | SIMPLE      | memos | ALL  | NULL          | NULL | NULL    | NULL |  202 | Using where; Using filesort |
+----+-------------+-------+------+---------------+------+---------+------+------+-----------------------------+元々のスキーマ
CREATE TABLE `memos` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user` int(11) NOT NULL,
  `content` text,
  `is_private` tinyint(4) NOT NULL DEFAULT '0',
  `created_at` datetime NOT NULL,
  `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;ADD INDEX
ALTER TABLE memos ADD INDEX is_private_created_at_idx (is_private, created_at);filesortしないで済むようになった
mysql> EXPLAIN SELECT * FROM memos WHERE is_private = 0 ORDER BY created_at LIMIT 100 OFFSET 10000;
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys             | key                       | key_len | ref   | rows | Extra       |
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+-------------+
|  1 | SIMPLE      | memos | ref  | is_private_created_at_idx | is_private_created_at_idx | 1       | const |   52 | Using where |
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+-------------+
1 row in set (0.00 sec)- is_private_created_at_idx INDEX で参照できた is_private,created_at,id 以外のデータにアクセスしないようにする
- = Covering Index
mysql> EXPLAIN SELECT id,is_private,created_at FROM memos WHERE is_private = 0 ORDER BY created_at LIMIT 100 OFFSET 10000;
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys             | key                       | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | memos | ref  | is_private_created_at_idx | is_private_created_at_idx | 1       | const |   52 | Using where; Using index |
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+--------------------------+idだけの指定でも、もちろんusing index
mysql> EXPLAIN SELECT id FROM memos WHERE is_private = 0 ORDER BY created_at LIMIT 100 OFFSET 10000;
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+--------------------------+
| id | select_type | table | type | possible_keys             | key                       | key_len | ref   | rows | Extra                    |
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+--------------------------+
|  1 | SIMPLE      | memos | ref  | is_private_created_at_idx | is_private_created_at_idx | 1       | const |   52 | Using where; Using index |
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+--------------------------+例えば、userとか使っているINDEX以外のカラム足しちゃうとダメ(Using indexが消えている)
mysql> EXPLAIN SELECT id,user FROM memos WHERE is_private = 0 ORDER BY created_at LIMIT 100 OFFSET 10000;
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+-------------+
| id | select_type | table | type | possible_keys             | key                       | key_len | ref   | rows | Extra       |
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+-------------+
|  1 | SIMPLE      | memos | ref  | is_private_created_at_idx | is_private_created_at_idx | 1       | const |   52 | Using where |
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+-------------+- 逆に言うと、userデータがどうしても必要なら、INDEXは大きくなるが、わざとINDEXに加えるというのもありえるが、、
- けど、今回の例では、他にも必要なデータがある
IN句???
mysql> EXPLAIN SELECT memos.id, memos.content FROM memos WHERE id IN (SELECT id FROM memos WHERE is_private = 0 ORDER BY created_at LIMIT 100 OFFSET 10000);
ERROR 1235 (42000): This version of MySQL doesn't yet support 'LIMIT & IN/ALL/ANY/SOME subquery'ぬーん
PerlでIN句
my @public_memos = $self->dbh->select_all(q/
    SELECT id FROM memos WHERE is_private = 0 ORDER BY created_at, id DESC LIMIT 100
/);
my $memos = $self->dbh->select_all(q/
    SELECT id, title, created_at FROM memos WHERE id IN (?)
/, \@public_memos);- 分かりやすい
- 複数台構成になったときにも都合いい
けどまだTTPできてない
- SELF JOIN
mysql> EXPLAIN SELECT memos.id, memos.content FROM memos, (SELECT id FROM memos WHERE is_private = 0 ORDER BY created_at LIMIT 100 OFFSET 10000) AS t WHERE memos.id = t.id;
+----+-------------+------------+--------+---------------------------+---------------------------+---------+-------+------+--------------------------+
| id | select_type | table      | type   | possible_keys             | key                       | key_len | ref   | rows | Extra                    |
+----+-------------+------------+--------+---------------------------+---------------------------+---------+-------+------+--------------------------+
|  1 | PRIMARY     | <derived2> | ALL    | NULL                      | NULL                      | NULL    | NULL  |   52 | NULL                     |
|  1 | PRIMARY     | memos      | eq_ref | PRIMARY                   | PRIMARY                   | 4       | t.id  |    1 | NULL                     |
|  2 | DERIVED     | memos      | ref    | is_private_created_at_idx | is_private_created_at_idx | 1       | const |   52 | Using where; Using index |
+----+-------------+------------+--------+---------------------------+---------------------------+---------+-------+------+--------------------------+- 分かりやすい
- 複数台構成になったときにも都合いい
- 分かりやすい
余談
SQL::Maker, SQL::Abstract
- スカラリファレンスを値とすると、SQLとして展開してくれる
SQL::Maker::Select->new->add_select(\'COUNT(*) AS cnt')SQL::Maker subquery example(1)
my $builder = SQL::Maker->new(driver => 'mysql');
my $stmt = $builder->new_select
         ->add_select('id')->add_select('title')->add_select('created_at')
         ->add_from('memos')
         ->add_from(\q/(SELECT id FROM memos LIMIT 100) AS t/) # !!
         ->add_where('memos.id' => 't.id');SQL::Maker subquery example(2)
my $builder  = SQL::Maker->new(driver => 'mysql');
my $sub_stmt = $builder->new_select
            ->add_select('id')
            ->add_from('memos')
            ->limit(100);
my $stmt = $builder->new_select
         ->add_select('id')->add_select('title')->add_select('created_at')
         ->add_from('memos')
         ->add_from($sub_stmt, 't'/) # !!
         ->add_where('memos.id' => 't.id');SQL::Maker subquery example(3)
use SQL::Maker;
SQL::Maker->load_plugin('JoinSelect'); # !!!!
my $builder = SQL::Maker->new(driver => 'mysql', new_line => ' ');
my ($sql, @binds) = $builder->join_select(
    user_item => [
        \q/(SELECT user_id FROM user_item) AS t/ => {'user_item.user_id' => 't.user_id' },
    ],
    ['*'],
    {
        'user_item.user_id' => 1,
    },
);
is $sql,
    'SELECT * FROM `user_item` INNER JOIN (SELECT user_id FROM user_item) AS t ON `user_item`.`user_id` = `t`.`user_id` WHERE (`user_item`.`user_id` = ?)';
is $binds[0], 1;余談(2)
- WHERE句なら、add_where_rawのが良さそう
- https://github.com/tokuhirom/SQL-Maker/blob/master/lib/SQL/Maker/Select.pm#L282
count
# フルテーブルスキャーン
[2014-09-17T11:48:43] [DBIx::Sunny::db] [0.000952] SELECT count(*) FROM memos WHERE is_private=0 at /Users/kfly8/lab/isucon3/qualifier/webapp/perl/local/lib/perl5/DBIx/Sunny.pm line 126
.----------------------------------------------------------------------------------------------.
| id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+-------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | memos | ALL  | NULL          | NULL | NULL    | NULL |  202 | Using where |
'----+-------------+-------+------+---------------+------+---------+------+------+-------------'`counter table
Cache
- Query Cache
- Memcached
- Redis
- etc..
生存時間に注意しつつ、
効率的なキャッシュを作る...
cacheの知見じゃなくて、
伝えたいこと
ファンとして伝えたい
isuconの名シーン...
2012
isucon2
fujiwara組
- template のcache
- busy loopを用意して、そこでfurl
- furl先でredisにcache
- 調整の仕方もすごい
- isuconの制限時間内におさめてすごい
ほんとすごい
まとめ(1)
- 
SELECT *やめる
- N+1問題 -> for { query } -> join? IN句? Perlで組み立て
- add index / drop index
- covering index -> IN句の実装速いけど、サブクエリだって実装簡単
- count -> counter table / Max(id)
- Cache -> memcached, redis / Query cache, Template cache
まとめ(2)
isuconたのしい
まとめ(3)
これだけ話してRDB使ってない
NoSQLアプリとかありえますよね?!
ご清聴ありがとうございました。
株式会社モバイルファクトリー は、
開発好きなエンジニアを求めています。
 
