[groonga-dev,01788] Re: FW: mroonga適用テーブルへの「WHERE IN 多数」クエリでエラー

Back to archive index

Kimura A a.kim****@live*****
2013年 9月 18日 (水) 18:48:17 JST


木村です。
須藤さん励ましありがとうございます(ノ_;)

とりあえず使用した2つのシェルを貼ります(ファイル自体も添付してお送りします)。
いずれもCakePHPのシェルなので、もし意味が読み取りにくい部分がありましたらご質問ください。
コメントはなるべく細かく書き込んでおきました。

必要であれば一部の変数をベタ書きに修正するなどして、多少整理することもできます。
また、Insertシェルについてはデータ生成部分をもう少し素のMySQLクエリに近い形に書き直すことも可能です。

お手数をおかけしますが、ご確認どうぞよろしくお願いします。


○Insertシェル
/var/www/vhosts/my-domain.com/cake/app/Console/Command/InsertShell.php

<?php
App::uses('AppShell', 'Console/Command');

/**
 * mroonga「WHERE IN 多数」クエリのテスト用データ生成シェル
 *
 * @property Model $Model
 */
class InsertShell extends AppShell {

/**
 * オプションの準備
 *
 * @var array
 */
    public $optionParams = array(
        'force' => array(
            'short' => 'f',
            'help' => 'Remove "mroonga_errored" rock file and do continue.'
        ),
        'minutes' => array(
            'short' => 'm',
            'help' => 'Minutes to work. Default 5'
        )
    );

/**
 * オプションの許可
 */
    public function getOptionParser() {
        $parser = parent::getOptionParser();
        $parser->addOptions($this->optionParams);
        return $parser;
    }

/**
 * メイン処理
 *
 * @return void
 */
    public function main() {
        $database = 'webTest';                                                        // 接続先DB:「webTest」ならtestデータベース
        $fields = array('code', 'description', 'id', 'vendor_id', 'tag_pool');        // 生成対象カラム
        $limit = 100;                                                                // 1度にいくつのレコードをINSERTするか
        $minutes = empty($this->params['minutes']) ? 5 : $this->params['minutes'];    // 実行を許可する最大分数。cronによる動作時の重複回避用
        $sleep = 1;                                                                    // 一括INSERTクエリの連続実行時に何秒インターバルを置くか
        $headRoomSec = 10;                                                            // 実行許可分間のうち処理の重複回避のために何秒の余裕を残すか

        // モデルの準備
        $this->Model = ClassRegistry::init('Product');
        if ($database && $database !== 'default') {
            // 接続先DBの変更
            $this->Model->setDataSource($database);
        }

        // forceオプション対応
        if ((!empty($this->params['force']) || !$this->Model->find('first', array('fields' => array('id')))) && file_exists(TMP . 'mroonga_errored')) {
            // forceオプション有効もしくはproductsテーブルが空で、かつロックファイルが存在する場合:ロックファイルを消去して処理実行
            CakeLog::debug("mroonga_erroredファイルを削除して{$limit}件単位の一括INSERT処理({$minutes}分間)を開始します");
            @unlink(TMP . 'mroonga_errored');
        } else {
            // 通常は開始ログのみ出力
            CakeLog::debug("{$limit}件単位の一括INSERT処理({$minutes}分間)を開始します");
        }

        // ロックファイルの存在チェック
        if (file_exists(TMP . 'mroonga_errored')) {
            // Selectシェル側でエラー発生後はINSERT処理回避
            CakeLog::debug("mroonga_erroredファイルが存在するためINSERT処理を回避します");
            return;
        }

        // 時間超過を回避しつつ連続実行
        $startTS = time();                                            // 処理開始TS
        $timeLimitTS = $startTS + (60 * $minutes) - $headRoomSec;    // 想定するタイムリミットTS:$headRoomSec秒の余裕を確保
        $loopSecMax = 0;                                            // 1回のループに要した最大秒数を保持
        for ($i = 0; $i < ($minutes * 20); $i++) {
            $loopStart = time();
            if ($i) {
                sleep($sleep);
            }

            $this->insert($fields, $limit);
            $loopSec = time() - $loopStart;
            $loopSecMax = max(array($loopSecMax, $loopSec));

            // 最も長時間を要したループの所要時間を基準に、トータル処理時間が所定秒数を超過しないかチェック:Selectシェル側処理との重複回避
            $loopEndTS = time();
            if ($loopSecMax > ($timeLimitTS - $loopEndTS)) {
                $loopCount = $i + 1;
                $totalSec = $loopEndTS - $startTS;

                $maxId = $this->getMaxId();
                $dbNameData = $this->Model->query('SELECT database() as `db`;', false);
                $dbName = $dbNameData[0][0]['db'];

                CakeLog::debug("{$minutes}分間のタイムリミットが近いため処理を終了します。DB名:{$dbName}/最大products.id:{$maxId}/トータル動作秒数:{$totalSec}/ループ回数:{$loopCount}/ループ最大所要秒数:{$loopSecMax}/ヘッドルーム秒数:{$headRoomSec}");
                break;
            }
        }
    }

    public function getMaxId() {
        $record = $this->Model->find('first', array(
            'fields' => array('id'),
            'order' => 'Product.id DESC'
        ));
        return empty($record['Product']['id']) ? 0 : $record['Product']['id'];
    }

    public function insert($fields, $limit) {
        $data = array();
        $id = $this->getMaxId();
        $vendorId = 1;
        for ($i = 0; $i < $limit; $i++) {
            $record = array();
            $id++;
            foreach ($fields as $field) {
                switch ($field) {
                    case 'code':
                        $record[$field] = "code{$id}";
                        break;
                    case 'code_sub':
                        $record[$field] = "code_sub{$id}";
                        break;
                    case 'description':
                        $record[$field] = $this->getText();
                        break;
                    case 'id':
                        break;
                    case 'vendor_id':
                        $record[$field] = 1;
                        break;
                    case 'tag_pool':
                        $record[$field] = $this->getText();
                        break;
                    case 'title':
                        $record[$field] = "タイトル{$id}";
                }
            }
            $data[] = $record;
        }

        if (!$this->Model->saveAll($data, array('callbacks' => false, 'validate' => false))) {
            CakeLog::debug("レコードの新規保存に失敗しました@" . __FILE__ . ' - L.' . __LINE__);
        }
    }

    public  function getText($nLengthRequired = 1000) {
        if (!$nLengthRequired) {
            $nLengthRequired = rand(100, 300);
        }

        // 5文字程度で半角スペース区切りになった文字列データを生成するためスペースを混入させておく
        $sCharList = "abcde fghij klmno pqrst uvwxy zABCD EFGHI JKLMN OPQRS TUVWX YZ012 34567 89_-, .";
        mt_srand();
        $sRes = '';
        for($i = 0; $i < $nLengthRequired; $i++) {
            $sRes .= $sCharList{mt_rand(0, strlen($sCharList) - 1)};
        }

        return $sRes;
    }
}
?>


○Selectシェル
/var/www/vhosts/my-domain.com/cake/app/Console/Command/SelectShell.php

<?php
App::uses('AppShell', 'Console/Command');

/**
 * mroonga「WHERE IN 多数」クエリの検証用シェル
 *
 * @property Model $Model
 */
class SelectShell extends AppShell {

/**
 * メイン処理
 *
 * @return void
 */
    public function main() {
        $database = 'webTest';    // 接続先DB:「webTest」ならtestデータベース
        $field = 'code';        // WHERE IN構文の対象カラム
        $doCountUp = false;        // SELECT前にcount値のUPDATE処理を実行するか否か

        // モデルの準備
        $this->Model = ClassRegistry::init('Product');
        if ($database && $database !== 'default') {
            // 接続先DBの変更
            $this->Model->setDataSource($database);
        }

        // WHERE IN構文用の条件を実データテーブルから300件ランダム取得
        $records = $this->Model->find('all', array(
            'fields' => array($field),
            'limit' => 300,
            'order' => 'rand()'
        ));
        $vals = Hash::extract($records, "{n}.Product.{$field}");

        // SELECTクエリの構成
        $query = (
            "SELECT * " .
            "FROM `products` AS `Product` " .
            "WHERE `Product`.`" . $field . "` IN ('" . implode("', '", $vals) . "');"
        );

        // 【オプション】カウントアップ実行
        if ($doCountUp) {
            $countUpId = rand(1, 200000);
            $query = "UPDATE products SET count=count+1 WHERE id={$countUpId}; " . $query;
        }

        // SELECT実行
        $result = null;
        try {
            $result = $this->Model->query($query, false);
        } catch(Exception $e) {
            $result = false;
        }

        // DB名の確認(安心用)
        $dbNameData = $this->Model->query('SELECT database() as `db`;', false);
        $dbName = $dbNameData[0][0]['db'];
        $queryLen = strlen($query);

        // メッセージの構成
        $msg = "{$dbName}データベースにおける「";
        if ($doCountUp) {
            $msg .= "Product.id=={$countUpId}カウントアップ → ";
        }
        $msg .= "WHERE Product.{$field} IN 多数」クエリチェック";
        if (is_array($result)) {
            $msg = "【○】{$msg}成功";
        } else {
            $msg = "【×】{$msg}失敗";
            if (!file_exists(TMP . 'mroonga_errored')) {
                // Insertシェル停止用のロックファイルを作成
                touch(TMP . 'mroonga_errored');
                chmod(TMP . 'mroonga_errored', 0666);
            }

        }
        $msg = "{$msg}。クエリ長={$queryLen}字";

        // ログを出力
        CakeLog::debug($msg);
    }
}
 		 	   		  



groonga-dev メーリングリストの案内
Back to archive index