2010/06/28 | Written by
rust | under
Blog
と言うわけで、Rack化してみました。
darashi's jpmobile at rack – GitHub
Github上のrackブランチがそれにあたります。
以下で実験中です。ちゃんと絵文字と漢字コードが変換されたときはちょっとうれしかったりしました。
http://rust-stnard.appspot.com/
使い方
特に難しいことはなく、以下のような感じで動作させることができます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| # -*- coding: utf-8 -*-
require 'rubygems'
require 'sinatra'
$LOAD_PATH << './lib/jpmobile/lib'
require 'jpmobile'
require 'jpmobile/rack'
require 'pp'
use Jpmobile::Rack::MobileCarrier
use Jpmobile::Rack::ParamsFilter
use Jpmobile::Rack::Filter
get '/' do
erb :index
end |
1 2 3 4 5 6 7 8
| <html>
<head>
<title>そうでもない</title>
</head>
<body>
言わずもがな
</body>
</html> |
Encoding on Ruby 1.9
またパラメータのEncodingも変換しています。Rack::Request#paramsが必ずASCII-8BITになるので、それを無理矢理UTF-8に戻しています。
そして次は
Rails 3.0 on Ruby 1.9 で動作する jpmobile と言うのが目標です。と言うかRubyKaigi 2010までには何とかしないとダメなので、7月初旬にはリリースできるようにしたいと思っています。Rails 3.0対応のパッチとか送ってもらっているのですが、Rack化でいろいろ変わったのと、見直したいところも多々あるので、しばしお待ちください。すんません…
Tags:
jpmobile,
Rails,
Rails 3.0,
Sinatra
2010/06/16 | Written by
rust | under
Blog
Extending Index for Innodb tables can hurt performance in a surprising way
多くのキーを使うクエリーのときに、よく最適化するときにインデックスを拡張する。通常は問題なく、インデックスの長さが劇的に増加しない限り、インデックスを使うクエリーは新しいインデックスの先頭を使うことができる。では、そうではない場合を見てみよう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| CREATE TABLE `idxitest` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`a` INT(11) NOT NULL,
`b` INT(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`)
) ENGINE=InnoDB AUTO_INCREMENT=6029313 DEFAULT CHARSET=latin1
mysql> SELECT COUNT(*) FROM idxitest WHERE a=5 AND b=5;
+----------+
| COUNT(*) |
+----------+
| 60434 |
+----------+
1 ROW IN SET (0.69 sec)
mysql> EXPLAIN SELECT COUNT(*) FROM idxitest WHERE a=5 AND b=5;
+----+-------------+----------+------+---------------+------+---------+-------+--------+-------------+
| id | select_type | TABLE | TYPE | possible_keys | KEY | key_len | REF | ROWS | Extra |
+----+-------------+----------+------+---------------+------+---------+-------+--------+-------------+
| 1 | SIMPLE | idxitest | REF | a | a | 4 | const | 707820 | USING WHERE |
+----+-------------+----------+------+---------------+------+---------+-------+--------+-------------+
1 ROW IN SET (0.00 sec) |
さて、カラム(a)から(a, b)にインデックスを拡張することで、高速化して他に影響を与えない最適化ができる。いいかな?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| mysql> ALTER TABLE idxitest DROP KEY a,ADD KEY(a,b);
Query OK, 0 ROWS affected (24.84 sec)
Records: 0 Duplicates: 0 Warnings: 0
mysql> SELECT COUNT(*) FROM idxitest WHERE a=5 AND b=5;
+----------+
| COUNT(*) |
+----------+
| 60434 |
+----------+
1 ROW IN SET (0.02 sec)
mysql> EXPLAIN SELECT COUNT(*) FROM idxitest WHERE a=5 AND b=5;
+----+-------------+----------+------+---------------+------+---------+-------------+--------+-------------+
| id | select_type | TABLE | TYPE | possible_keys | KEY | key_len | REF | ROWS | Extra |
+----+-------------+----------+------+---------------+------+---------+-------------+--------+-------------+
| 1 | SIMPLE | idxitest | REF | a | a | 8 | const,const | 120640 | USING INDEX |
+----+-------------+----------+------+---------------+------+---------+-------------+--------+-------------+
1 ROW IN SET (0.00 sec) |
素晴らしい。クエリーは30倍も速くなった。スキャンする行が減っただけではなく、インデックスですべてまかなえるようになった。これだとデータにアクセスする必要がない。
でも喜ぶには早すぎる。ほとんど気がついていないようなそれまでは高速に実行されていたクエリーが他にもあった。しかしそれが遅くなるのだ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| # 古いスキーマで実行
mysql> SELECT * FROM idxitest WHERE a=100 ORDER BY id DESC LIMIT 1;
+---------+-----+---+
| id | a | b |
+---------+-----+---+
| 3000000 | 100 | 7 |
+---------+-----+---+
1 ROW IN SET (0.00 sec)
mysql> EXPLAIN SELECT * FROM idxitest WHERE a=100 ORDER BY id DESC LIMIT 1;
+----+-------------+----------+------+---------------+------+---------+-------+--------+-------------+
| id | select_type | TABLE | TYPE | possible_keys | KEY | key_len | REF | ROWS | Extra |
+----+-------------+----------+------+---------------+------+---------+-------+--------+-------------+
| 1 | SIMPLE | idxitest | REF | a | a | 4 | const | 126074 | USING WHERE |
+----+-------------+----------+------+---------------+------+---------+-------+--------+-------------+
1 ROW IN SET (0.00 sec)
# 新しいスキーマで実行(インデックスを拡張)
mysql> SELECT * FROM idxitest WHERE a=100 ORDER BY id DESC LIMIT 1;
+---------+-----+---+
| id | a | b |
+---------+-----+---+
| 3000000 | 100 | 7 |
+---------+-----+---+
1 ROW IN SET (1.01 sec)
mysql> EXPLAIN SELECT * FROM idxitest WHERE a=100 ORDER BY id DESC LIMIT 1;
+----+-------------+----------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | TABLE | TYPE | possible_keys | KEY | key_len | REF | ROWS | Extra |
+----+-------------+----------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | idxitest | INDEX | a | PRIMARY | 4 | NULL | 36 | USING WHERE |
+----+-------------+----------+-------+---------------+---------+---------+------+------+-------------+
1 ROW IN SET (0.00 sec)
# 実行計画は以下のようになることもある
mysql> EXPLAIN SELECT * FROM idxitest WHERE a=100 ORDER BY id DESC LIMIT 1;
+----+-------------+----------+------+---------------+------+---------+-------+------+------------------------------------------+
| id | select_type | TABLE | TYPE | possible_keys | KEY | key_len | REF | ROWS | Extra |
+----+-------------+----------+------+---------------+------+---------+-------+------+------------------------------------------+
| 1 | SIMPLE | idxitest | REF | a | a | 4 | const | 1 | USING WHERE; USING INDEX; USING filesort |
+----+-------------+----------+------+---------------+------+---------+-------+------+------------------------------------------+
1 ROW IN SET (0.01 sec) |
なぜこんなに遅くなってしまうのか。理由はInnoDBの特性に有利な実行計画にある。つまり、インデックスが各キーの値に対する主キーでソートされているということ。なのでインデックス(a)があってidが主キーであるとき、実際のインデックスは(a, id)となり、拡張されたインデックスが(a, b)であるときには実際のインデックスは(a, b, id)になるということ。なので、元にインデックスで(a, id)を使っていたようなクエリーでは、新しいインデックスを使うことができない。
じゃあどうすれば?こんなときは「冗長」なインデックスを(a)と(a, b)に同時に張ることができる。だが、これをちゃんと動くと仮定する仮定しがちだが、しばしば動作しない。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| CREATE TABLE `idxitest` (
`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`a` INT(11) NOT NULL,
`b` INT(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`),
KEY `a_2` (`a`,`b`)
) ENGINE=InnoDB AUTO_INCREMENT=6029313 DEFAULT CHARSET=latin1
mysql> SELECT * FROM idxitest WHERE a=100 ORDER BY id DESC LIMIT 1;
+---------+-----+---+
| id | a | b |
+---------+-----+---+
| 3000000 | 100 | 7 |
+---------+-----+---+
1 ROW IN SET (1.03 sec)
mysql> EXPLAIN SELECT * FROM idxitest WHERE a=100 ORDER BY id DESC LIMIT 1;
+----+-------------+----------+-------+---------------+---------+---------+------+------+-------------+
| id | select_type | TABLE | TYPE | possible_keys | KEY | key_len | REF | ROWS | Extra |
+----+-------------+----------+-------+---------------+---------+---------+------+------+-------------+
| 1 | SIMPLE | idxitest | INDEX | a,a_2 | PRIMARY | 4 | NULL | 2247 | USING WHERE |
+----+-------------+----------+-------+---------------+---------+---------+------+------+-------------+
1 ROW IN SET (0.00 sec) |
MySQLのオプティマイザーは(a)と(a, b)のインデックスの利用を考えた後、最後にどちらも使わずにフルインデックススキャンすることにしたようだ。(a)のインデックスを使えば1行だけでいいことが保証されるのに、2247行もスキャンするような計画を見積もったこのケースでは、オプティマイザーの誤動作のように見える。
正しい計画をMySQLオプティマイザーに使わせるためには、FORCE INDEX(a)を使う必要がある。
これは冗長なインデックスを調べるツールであるmk-duplicate-key-checkerの結果から、インデックスを変更するときには十分気をつけなければならないことを意味する。インデックスの中で主キーによってデータがソートされているInnoDBに依存するようなクエリーがあるのなら、変更が重大な影響を及ぼすことになるからだ。
オプティマイザーの振る舞いはMySQLのバージョンによって異なる。今回は5.1.45で行ったが、5.0でも同じ振る舞いだったことを記しておく。
Tags:
MySQL
いつものように気になったので意訳
Joining on range? Wrong! | MySQL Performance Blog
これら書く問題というのはMySQLの最初の方にある問題なんだが、気をつけてクエリーを解析しないと知らないまま終わってしまうことだ。phpDay.it でこの落とし穴についてデモしてきたが、ブログの記事にするのもいいとおもったんだ。
問題をデモするための典型的な例として、売上げクエリーを考えよう。データは次の3つのテーブルに入っている。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| CREATE TABLE `products` (
`prd_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`prd_name` VARCHAR(32) NOT NULL,
PRIMARY KEY (`prd_id`),
KEY `name` (`prd_name`)
)
CREATE TABLE `tags` (
`tag_prd_id` INT(10) UNSIGNED NOT NULL,
`tag_name` VARCHAR(32) NOT NULL,
PRIMARY KEY (`tag_name`, `tag_prd_id`)
)
CREATE TABLE `items_ordered` (
`itm_id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`itm_prd_id` INT(10) UNSIGNED NOT NULL,
`itm_order_timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`itm_id`),
KEY `itm_prd_id__and__itm_order_timestamp` (`itm_prd_id`,`itm_order_timestamp`)
) |
そして十分な量のデータを入れた。
そして次のクエリーを考える。昨日にどれだけ液晶テレビが売れたのかを調べるクエリーだ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| SELECT COUNT(1)
FROM tags t
JOIN products p
ON p.prd_id = t.tag_prd_id
JOIN items_ordered i
ON i.itm_prd_id = p.prd_id
WHERE t.tag_name = 'lcd'
AND i.itm_order_timestamp>= '2010-05-16 00:00:00'
AND i.itm_order_timestamp <'2010-05-17 00:00:00'
+----------+
| COUNT(1) |
+----------+
| 4103 |
+----------+ |
ちゃんとできたかな!
データ構造を見る限り、うまくいったように見える。tagsテーブルにはインデックスtag_nameがあり、items_orderedには(itm_prd_id, itm_order_timestamp)などが結合時に使われている。ではさらに詳細に見てみるとどうだろう。
1 2 3 4 5 6 7 8 9 10 11 12
| SHOW STATUS LIKE 'Handler_read%';
+-----------------------+--------+
| Variable_name | VALUE |
+-----------------------+--------+
| Handler_read_first | 0 |
| Handler_read_key | 3 |
| Handler_read_next | 118181 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+--------+ |
ちょっとよくないように見える。結果が4103行なのに、120000行もスキャンしている。必要なカラムにはちゃんとインデックスつけているのに!EXPLAINはどうなっているのか。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| *************************** 1. ROW ***************************
id: 1
select_type: SIMPLE
TABLE: t
TYPE: REF
possible_keys: PRIMARY
KEY: PRIMARY
key_len: 98
REF: const
ROWS: 1
Extra: USING WHERE; USING INDEX
*************************** 2. ROW ***************************
id: 1
select_type: SIMPLE
TABLE: p
TYPE: eq_ref
possible_keys: PRIMARY
KEY: PRIMARY
key_len: 4
REF: example_db.t.tag_prd_id
ROWS: 1
Extra: USING INDEX
*************************** 3. ROW ***************************
id: 1
select_type: SIMPLE
TABLE: i
TYPE: REF
possible_keys: itm_prd_id__and__itm_order_timestamp
KEY: itm_prd_id__and__itm_order_timestamp
key_len: 4
REF: example_db.p.prd_id
ROWS: 10325
Extra: USING WHERE; USING INDEX |
ここでちょっと思い出してみる。
1 2 3
| `itm_prd_id` INT(10) UNSIGNED NOT NULL
`itm_order_timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
KEY `itm_prd_id__and__itm_order_timestamp` (`itm_prd_id`,`itm_order_timestamp`) |
EXPLAINの3行目のkey_lenは4バイトなのだが、(itm_prd_id, itm_order_timestamp)は4+4で8バイトあるはずだ。と言うことは、1つのカラムしか見ていないことになる。
これをどう理解すべきか。データベースはlcdとタグの付いたソートされたデータを全て読み込む。それはやく120000行で、そのあと日付で絞り込まれる。なんと非効率な方法だ!MySQLは製品IDと日付範囲をインデックスでマッチして関連する行を読み込むだけな単純な最適化もできない。
これは結合するときだけ影響する。範囲条件を最初のテーブル(そしてそれのみ)に限定すれば、期待通りに動く。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| EXPLAIN
SELECT COUNT(1)
FROM items_ordered i
WHERE i.itm_prd_id = 5
AND i.itm_order_timestamp>= '2010-05-16 00:00:00'
AND i.itm_order_timestamp <'2010-05-17 00:00:00'
*************************** 1. ROW ***************************
id: 1
select_type: SIMPLE
TABLE: i
TYPE: range
possible_keys: itm_prd_id__and__itm_order_timestamp
KEY: itm_prd_id__and__itm_order_timestamp
key_len: 8
REF: NULL
ROWS: 1306
Extra: USING WHERE; USING INDEX |
この場合、結合しないのでMySQLはrefとは表示せず、key_lenも8バイトとなっている。つまりちゃんとインデックスが利用されていると言うことだ。
この問題に対しては多くの回避策があるが、基本的に状況に依存する。本質的には、結合から範囲条件を外すことになる。今回の場合は、日付(DATA)のカラムを追加して、それで絞り込むことにすればよい。
1 2
| ALTER TABLE items_ordered ADD itm_order_date DATE NOT NULL, ADD INDEX itm_prd_id__and__itm_order_date (itm_prd_id, itm_order_date);
UPDATE items_ordered SET itm_order_date = DATE(itm_order_timestamp); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| EXPLAIN
SELECT COUNT(1)
FROM tags t
JOIN products p
ON p.prd_id = t.tag_prd_id
JOIN items_ordered i
ON i.itm_prd_id = p.prd_id
WHERE t.tag_name = 'lcd'
AND i.itm_order_date = '2010-05-16'
*************************** 1. ROW ***************************
id: 1
select_type: SIMPLE
TABLE: t
TYPE: REF
possible_keys: PRIMARY
KEY: PRIMARY
key_len: 98
REF: const
ROWS: 1
Extra: USING WHERE; USING INDEX
*************************** 2. ROW ***************************
id: 1
select_type: SIMPLE
TABLE: p
TYPE: eq_ref
possible_keys: PRIMARY
KEY: PRIMARY
key_len: 4
REF: example_db.t.tag_prd_id
ROWS: 1
Extra: USING INDEX
*************************** 3. ROW ***************************
id: 1
select_type: SIMPLE
TABLE: i
TYPE: REF
possible_keys: itm_prd_id__and__itm_order_timestamp,itm_prd_id__and__itm_order_date
KEY: itm_prd_id__and__itm_order_date
key_len: 7
REF: example_db.p.prd_id,const
ROWS: 206494
Extra: USING WHERE; USING INDEX |
このクエリーはitm_prd_id__and__itm_order_dateで7バイトとなっているので、(itm_prd_id, itm_order_date)をちゃんと利用していることが分かる。またrefでも2つのカラムを結合で利用していることが分かる。
1 2 3 4 5 6 7 8 9 10 11
| SHOW STATUS LIKE 'Handler_read%';
+-----------------------+-------+
| Variable_name | VALUE |
+-----------------------+-------+
| Handler_read_first | 0 |
| Handler_read_key | 3 |
| Handler_read_next | 4104 |
| Handler_read_prev | 0 |
| Handler_read_rnd | 0 |
| Handler_read_rnd_next | 0 |
+-----------------------+-------+ |
非常によくなったことが分かるだろう。
しかし、クエリーが異なれば、解決方法も異なることは覚えておいて欲しい。
この問題に対するバグレポートがいくつかある(例えば #8569, #19548)が、MySQLからの回答では6.0かそれ以降のバージョンで解決されると言うことだ。または「それは解説されている振る舞いである」と言うことらしい。でも実際には仕様ではなく致命的なバグであり、修正が必要だ。
Tags:
MySQL
2010/06/11 | Written by
rust | under
Blog

ラビリンス
- アーティスト:TVサントラ
- レーベル:ソニーレコード( 1999-05-21 )
というわけで、買ってみました。ただラビリンスの方はいまだDVD化されていないようなので、とりあえずサントラだけでも。でもこの頃のドラマって面白いですね。妻に教えてもらっていろいろ見てますが、なんか勢いがある感じ。早くDVD化されないかな、ラビリンス。
No tags for this post.
2010/06/08 | Written by
rust | under
Blog
実は前回のはうまく使えていません。というのも、どうもinline patchの方が23.2では良くないらしく、頻繁に落ちるからです。という訳なので、typesterさんのfull screen patchだけを当てていたのですが、今度はIME経由のShiftキー押下の記号が全部半角で入るという悲しいことに。なので、これまたtypesterさんのfix-shiftmodifier-with-ime patchを当てたものを使ってみることにします。
1 2 3 4 5 6 7 8 9 10 11
| % wget http://ftp.gnu.org/pub/gnu/emacs/emacs-23.2.tar.gz
% wget http://github.com/downloads/typester/emacs/feature-fullscreen.patch
% wget http://github.com/downloads/typester/emacs/fix-shiftmodifier-with-ime.patch
% tar zxvf emacs-23.2.tar.gz
% cd emacs-23.1
% patch -p1 < ../feature-fullscreen.patch
% patch -p1 < ../fix-shiftmodifier-with-ime.patch
% ./configure --with-ns --without-x
% make bootstrap
% make install
% open nextstep/Emacs.app |
基本的に前回と同じです。これで全角括弧とかが入力できるようになりました。
Tags:
Emacs,
Mac OS X
2010/06/07 | Written by
rust | under
Blog
Math bookというところに、大学初等向けの数学の入門テキストがありました。というわけで、誰か読書会とかやらんかね。場所さえあれば、大学初等数学の講師経験もあるので、解説しますよ。というわけで、場所募集中。
Tags:
数学,
物理,
読書会