VMをつくり直すときに役に立ったサイト

プログラムテスト環境として利用していたVMのハードディスク容量が足りなくなったので、新たに作り直しました。前回は勉強のためとソースからコンパイルして作ったんですが、今回はyumを使った方法を試してみることに。

途中うまく動かない箇所があったんですが、yumリポジトリを変えて再度構築しなおしたら、意図通りに動作する環境ができあがりました。そのときに参考にしたサイトを忘れないようにメモです。

いくつかのサイトを参考にしながら作成しましたが、うまくいったのはこのサイトの方法で構築したときでした。
【VMware Player上のCentOSに開発環境を構築 #2CommentsAdd Star】
http://rewish.org/php_mysql/vm_player_config_2

Gitのインストールの際に参考にしたサイト
【centos5.3でyumを使ってgitをインストールする】
http://d.hatena.ne.jp/uk_oasis/20090807/1249633626

ついでに調べた、Gitのクライアント。
【tortoiseGit】 tortoiseSVNのgit版。Win専用。
http://code.google.com/p/tortoisegit/

【SmartGit】 Win、Mac、Linuxに対応
http://www.syntevo.com/smartgit/index.html
Gitだけじゃなくて、CSV、SVNのクライアントも。慣れたら同じ使い勝手で全部いけるとしたら一番いいのかも。

【Gitti】 Mac専用。画面がいい感じ。
http://www.gittiapp.com/

MySQL複合INDEXの威力

予想以上の効果がでたので記事に残します。
DBの情報を元に統計を行うSQLとプログラムの性能改善で、複合INDEXを試してみました。
MySQLは、4.2です。

対象となったSQLは複数ありますが、そのうちほとんどは4つのテーブルをJOINしています。取得条件を変えながら情報を取得し、最後に目的別に合算するっていうことをやっています。4つのテーブルのうち3つはデータ数200万オーバー、1つは100万オーバー。

最初は、こんな状態

+----+-------------+-------+--------+----------------------------------------------+
| id | select_type | table | type   | Extra                                        |
+----+-------------+-------+--------+----------------------------------------------+
|  1 | SIMPLE      | tb    | range  | Using where; Using temporary; Using filesort |
|  1 | SIMPLE      | tc    | ref    | Using where                                  |
|  1 | SIMPLE      | tp    | ref    | Using where                                  |
|  1 | SIMPLE      | td    | eq_ref | Using where                                  |
+----+-------------+-------+--------+----------------------------------------------+

まずは、一番最初に検索されている tb に複合インデックス(検索条件カラム、取得カラム全部、合計11カラム)を設定してみるも結果変わらず。カラムを検索条件だけにしてみたり、更に減らしたりと試すも結果変わらず。

とりあえず、最初に設定した複合インデックスを設定して、tcにも複合インデックス(全8カラム)を設定してみる。

すると変化が現れた。

+----+-------------+-------+--------+----------------------------------------------------------+
| id | select_type | table | type   | Extra                                                    |
+----+-------------+-------+--------+----------------------------------------------------------+
|  1 | SIMPLE      | tc    | ref    | Using where; Using index Using temporary; Using filesort |
|  1 | SIMPLE      | tb    | ref    | Using where                                              |
|  1 | SIMPLE      | tp    | ref    | Using where                                              |
|  1 | SIMPLE      | td    | eq_ref | Using where                                              |
+----+-------------+-------+--------+----------------------------------------------------------+

tc に設定したINDEXが利用されてuse indexが出てきたのはわかるが、テーブルをJOINする順番が変わっている!!気になって、tb に設定した複合INDEXを削除すると、またtb からJOINされるようになった、possible key(利用可能index)にはいて、key(利用index)にはでてこないけど、なんらかの影響は及ぼしているようだ。

td tpにも同じ要領で検索対象、取得対象のカラムに複合INDEXを設定する。その結果、td tpにもuse indexが表示されるようになった。

+----+-------------+-------+------+----------------------------------------------------------+
| id | select_type | table | type | Extra                                                    |
+----+-------------+-------+------+----------------------------------------------------------+
|  1 | SIMPLE      | tc    | ref  | Using where; Using index Using temporary; Using filesort |
|  1 | SIMPLE      | tb    | ref  | Using where                                              |
|  1 | SIMPLE      | tp    | ref  | Using where; Using index                                 |
|  1 | SIMPLE      | td    | ref  | Using where; Using index                                 |
+----+-------------+-------+------+----------------------------------------------------------+

だが、肝心のusing temporary、using filesortが消えない。using filesortって、今回のSQLにはgroup by は入っているけど、order byは入っていない。いくつかサイトを見たけど、order by には言及してあるものが多かった。そこで、order by をgroup by に読み替えて、実験してみることにした。
http://www.mysqlpracticewiki.com/index.php/Extra_field
上記のサイトと、リンク先を読んでいて今回のケースに当てはまりそうなものは、最初にJOINされるテーブルではないカラムで、group by を掛けていること。tbのカラムでかけていた。
幸いtc tbは1対1対応のテーブルでで、group by をtc のカラムでかけても取得結果は変わらないので、試してみることに。

その結果がこれ

+----+-------------+-------+------+--------------------------+
| id | select_type | table | type | Extra                    |
+----+-------------+-------+------+--------------------------+
|  1 | SIMPLE      | tc    | ref  | Using where; Using index |
|  1 | SIMPLE      | tb    | ref  | Using where              |
|  1 | SIMPLE      | tp    | ref  | Using where; Using index |
|  1 | SIMPLE      | td    | ref  | Using where; Using index |
+----+-------------+-------+------+--------------------------+

消えた!やった!
ってことで、実行されるSQL(20ちょいくらい)にexplainをかけてみた。サブクエリ以外でexplainをとってみると、ほとんどのものでusing temporary、using filesortが消えていた。消えなかったのは、取得したいデータの量的にどうしようもなさそうなもの(データの半分以上を一旦取得するもの)くらい。

これで統計プログラムを実行したところ、実行時間が40分オーバーから5分前後へ!
かなり早くなった。予想以上の結果にびっくりしたので、メモ!

複合INDEXについては、以下のサイトも参考にさせていただきました。
http://d.hatena.ne.jp/k_yamamot/20100831/1283262245
http://txqz.net/blog/2006/12/13/0943
http://dev.seesaa.net/article/238633.html

ZendでDbのMeta情報をキャッシュする

なんのこっちゃ?って感じですよね。僕もそう思います。

ZendFrameworkを利用する場合、Zend_Db_Table(Row、Rowsetも)を利用することが多いと思います。Zend_Db_Table_Abstractを継承して、テーブル名と他テーブルとのリレーションを定義するだけで、DBの登録、更新、削除ができてしまう優れものです。個人的にはこれだけ、ZendFrameworkを使ってみようってきっかけになりました。

これらがテーブル名とリレーションだけしか定義してないのに、存在しないカラム名を指定して処理しようとすると、”そんなカラムはないよ!”ってエラーを出してくれます。賢いです。これができるのって、テーブル関連の処理をするたびに”describe”が実行されて、テーブルの情報を取得しているからなのです。気がきいてます。

ただ1点、”describe”が実行される量が多すぎるんです。これが処理が遅くなる原因になることがあります。ここででてくるのがDbのMeta情報のキャッシュ。DbのMeta情報というのは、”describe”の実行結果です。なので、これをキャッシュとして保持しておくと、”describe”をしなくても、情報が手に入るため余計なSQLをDBに実行しなくてすみます。

たかが”describe”ですが、実行回数がほんとに多いので、だいぶ変わります。
Zend_Db_Table使うときは、セットで設定するくらいでいいのではと思っています。

Bootstrap.phpの中にこんな感じで書いておくと使えます。

    protected function _initDbCache()
    {
        $frontendOptions = array('automatic_serialization' => true);
        $backendOptions = array('cache_dir' => APPLICATION_PATH . 'cacheディレクトリへの相対パス');
        $cache = Zend_Cache::factory('Core', 'File', $frontendOptions, $backendOptions);
        Pb_Db_Table_Abstract::setDefaultMetadataCache($cache);
    }

UNIONの落とし穴

UNIONを利用してselectの結果を結合する場合、カラム名はUNION文の一番最初に書かれたSQL文のものが優先される。

その後のSQLの取得結果は、カラム名を見て同じものを同じ列に並べてくれるわけではなく、取得項目の順番のみで判別され、結合される。

例えば、1つめのselect結果が、
col1, col2, col3, col4
という順番で取得され、2つめのselect結果が
col1, col3, col2, col4
という順番で取得されていた場合、2つめのselect結果のcol3は、col2として、col2はcol3としてUNIONの取得結果が表示される。

項目名とは意味の異なるデータが表示されるので、まったく使えないデータとなる。

自分でSQL文を直接書いている場合には考えられない現象だが、フレームワークを用いてSQL文を生成している場合は、この落とし穴にはまりやすい。

その場合は、多少性能が落ちるかもしれないが、各取得結果をテーブルとみなしたサブクエリを用いて、取得項目の並びを指定して取得しなおすという方法が有効でした。

Mysqlのサブクエリとgroup、orderの優先度

Mysqlを使ってシステムを組んでいると、いろんな条件でデータを取得したい場面がでてくる。その中で、使用頻度が高いのがgroup by、order by、サブクエリの組み合わせ。

group byをすると、指定したキーで一番最初に現れたものが残り、後から現れたものは削除される。order by と併用した場合は、group by した結果に対して、order byが適用される。

order by を適用した後に、group by を適用したい場合は、サブクエリを用いてorder by を実行するSQLをテーブルとみなして、group by をかけるという方法になる。

例えば、mixiのようなSNSで各ユーザの最新の日記を取得したい場合は、日記テーブルを作成日で、降順にソートするSQLをテーブルとみなして、ユーザIDでgroup by をかけるというサブクエリを利用すれば可能。

group by と order by だと group by が優先されるというのは、結構覚えておくと役にたつことが多かったです。

MYSQLのUNIONとUNION ALLの違い

今やっているプログラムで使わざるを得なくなったUNION構文。
UNIONとUNION ALL何が違うんだろう?と調べてみました。

SQLをわかっている方向けに説明すると、行全体にdistinctをかけるのがUNIONで、かけないのがUNION ALL。

つまりUNIONで連結した場合、全てのカラムの値が同じ行が複数あった場合、1行を残してすべてまとめられます。

----------------------
| col_a | col_b |
----------------------
|  0  |  a  |
----------------------

こんな取得結果をUNIONとUNION ALLにて連結した場合。

----------------------
| col_a | col_b |
----------------------
|  0  |  a  |
----------------------

これがUNION

----------------------
| col_a | col_b |
----------------------
|  0  |  a  |
----------------------
|  0  |  a  |
----------------------

これがUNION ALL

Macにapacheとかインストール

先日、OSクリーンインストールすることになったMacの開発環境を作り直してる途中です。前は、ネットで調べながらapache、php、mysqlをソースからインストールしたんですが、今回は楽そうな記事を見つけたので、macportでインストール。

参考にさせていただきた記事はこちらです。
http://www.serendip.ws/archives/3234

とりあえず、mysqlコマンドでDbにつながること、ブラウザでページが見えることを確認したところまで。php経由で、DBに接続できたら家でまたプログラムを作れるようになります。会社でVM使っているからか、VM Fusionが欲しくなったけど、何も考えなかったことにしました。

Zend_Db_Tableを利用して複数DBサーバを参照する

DBサーバの負荷分散のために、select文を発行する先のDBを複数用意して運用することがあると思います。マスター(更新&参照)1台に、スレーブ(参照)1台とか。

今回、マスター1台、スレーブ1台の構成で、selectクエリを振り分けるプログラムを作りました。Zend_Db_TableのfetchRow、fetchAll等が呼ばれる度に、クエリを実行するDBサーバを決定します。

まずは、DB接続先の設定をiniファイルに定義します。
接続先情報は、ダミーです。

database.adapter         = Pdo_Mysql
database.params.host     = master.test.com
database.params.username = dbuser
database.params.password = hogehoge
database.params.dbname   = master

# 参照DBhost
database.reference.host  = slave1

次に、Zend_Db_Table_Abstractと各テーブルクラスの間に入れるクラス。
Zend_Db_Table_Abstractクラスの拡張クラスです。

abstract class My_Db_Table_Abstract extends Zend_Db_Table_Abstract
{
    public function fetchAll($where=null, $order=null, $count=null, $offset=null)
    {
        // 2回に1回はmasterに接続
        if ((rand() % 2) == 0) {
            return parent::fetchAll($where, $order, $count, $offset);
        }
        
        // 2回に1回はslaveに接続
        $adapter = $this->getAdapter();
        $this->_setAdapter($this->getReferenceAdapter());
        $rows = parent::fetchAll($where, $order, $count, $offset);
        $this->_setAdapter($adapter);
        
        return $rows;
    }

    public function fetchRow($where=null, $order=null)
    {
        // 2回に1回はmasterに接続
        if ((rand() % 2) == 0) {
            return parent::fetchRow($where, $order);
        }
        
        // 2回に1回はslaveに接続
        $adapter = $this->getAdapter();
        $this->_setAdapter($this->getReferenceAdapter());
        $row = parent::fetchRow($where, $order);
        $this->_setAdapter($adapter);
        
        return $row;
    }
    
    public function getReferenceAdapter()
    {
        if (Zend_Registry::isRegistered('Ref_Db_Adapter')) {
            return Zend_Registry::get('Ref_Db_Adapter');
        }
        
        $config = new Zend_Config_Ini('↑で作ったconfigファイル名');
        $params = $config->database->params->toArray();
        $params['host'] = $config->database->reference->host;
        $db = Zend_Db::factory($config->database->adapter, $params);
        $db->setFetchMode(Zend_Db::FETCH_ASSOC);
        Zend_Registry::set('Ref_Db_Adapter', $db);
        
        return $db;
    }
}

テーブルクラスの定義は以下のように

class Users extends My_Db_Table_Abstract
{
    protected $_name    = 'users';
    protected $_primary = 'user_id';
}

これで、fetchRow、fetchAllを使う際は、masterとslaveにクエリが分散されます

MySQLのViewを使った感想

100万件以上のデータを持つDBで、Viewテーブルを使える機会があったので使ってみました。結果としては残念なことに。Viewテーブルを利用するだけでは性能改善につながりません。

Viewテーブルについて調べているときに、「MySQLのViewテーブルは裏側でcreateしたときのSQL発行するだけだから、単独では性能改善につながらない」という内容の記事を見たんですが、まさにその通りの結果になりました。ViewテーブルをcreateしたときのSQLでの検索性能と、Viewテーブルを使って同様のデータを取得しようとしたときの性能はほぼ一緒。改善された様子はありませんでした。

DBにOracleを使っていたときは、性能改善の手段として、まず変数のbind化やストアドプロシージャと供にViewテーブルを使って性能改善をしていた記憶があるので、Viewテーブルに対して検索をかけたときは単一テーブルに対して検索かけた程度の性能が期待できるのかと勘違いしていました。OracleではSQL発行の窓口としてのViewテーブルではなく、実際にデータを持っていたんでしょうか。(もう5年近く前の経験なので、今は全く変わっているかもしれません。Oracle)

Zend_Db_Tableでの勘違い

Zend Framework: Documentation

Zend_Db_Table で UPDATE や DELETE の連鎖操作をエミュレートする場合は、 配列 $_dependentTables を親テーブルで宣言し、 従属しているテーブルをそこで指定します。

各従属テーブルのクラス内で、配列 $_referenceMap を宣言します。これは、参照の “ルール” を定義する連想配列となります。 参照ルールとは、リレーションの親テーブルが何になるのか、 従属テーブルのどのカラムと親テーブルのどのカラムが対応するのかを示すものです。

引用した1つめの文章を読み飛ばしてしまっていたため、find~を使うためには、$_referenceMapと$_dependentTablesは、対を成す形式で記述するものだと勘違いしていました。onUpdateとかonDeleteをZendを使って実現しなければ、$_dependentTablesは不要ってことですね。

これだけのことなんですが、かなり「はっ!!」としました。
作っているプログラムの行が少なくなることは見やすくなっていいことです。