Gotanda.pm #2
今日の話は 自称isuconファンの話です
まだの方 5秒待ちます
唐突ですが
Question
「isuconで100万円をとる銀の弾丸はある?」
Answer
「@kazeburoさん、@fujiwaraさんと同じチームになる?!」
...
!!
まねしよう!!
TTP
徹底的にパkる
先輩の知恵を拝借させてもらう
5位以内でも賞金が出るらしい
制限時間内に基本的なことが出来たら、
もしかしたら、、、
もしかしたら!!
まず初めに感謝したいこと
isuconによって多くの素敵な知見が
得られています
ありがとうございます!!
参加申し込みはさっきしたはず...
SELECT *
やめるSELECT *
やめるfor { query } -> join? IN句? Perlで組み立て
もうこれで瞬間的にコードが頭に浮かぶなら
TTPできている!!たぶん。
SELECT *
やめる SELECT * FROM memos WHERE ...
AFTER
SELECT id, content, created_at FROM memos WHERE ...
不要なトラフィック発生しないようにする
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 ..;
for my $memo (@$memos) {
$memo->{username} = $self->dbh->select_one(
'SELECT username FROM users WHERE id=?',
$memo->{user},
);
}
SELECT
memos.id, memos.content, memos.created_at,
users.username
FROM
memos, users
WHERE
memos.user_id = users.id
...
INSERT memos (content, username) VALUES ($content, $user.username);
SELECT
id, content, created_at,
username
FROM
memos
WHERE
...
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' )
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' },
}
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 |
Explain して想定通りにindexが使われているか、都度確認
わすれがち、やりがちなこと
必要だったインデックスが
コードの変化でゴミになることもある
SQLアンチパターン インデックスショットガン
よくないクエリ
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)
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 |
+----+-------------+-------+------+---------------------------+---------------------------+---------+-------+------+-------------+
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できてない
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::Select->new->add_select(\'COUNT(*) AS cnt')
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');
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');
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;
# フルテーブルスキャーン
[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 |
'----+-------------+-------+------+---------------+------+---------+------+------+-------------'`
生存時間に注意しつつ、
効率的なキャッシュを作る...
cacheの知見じゃなくて、
伝えたいこと
ファンとして伝えたい
isuconの名シーン...
2012
isucon2
fujiwara組
ほんとすごい
SELECT *
やめるisuconたのしい
これだけ話してRDB使ってない
NoSQLアプリとかありえますよね?!
株式会社モバイルファクトリー は、
開発好きなエンジニアを求めています。