19

約 1,200 万行の MYISAM テーブルを使用しています。メソッドを使用して、指定した日付より古いすべてのレコードを削除します。テーブルは日付フィールドでインデックス付けされます。コード内で実行すると、削除するレコードがない場合は約 13 秒、1 日のレコードがある場合は約 25 秒かかることがログに示されます。mysql クライアントで同じクエリを実行すると (コードの実行中に SHOW PROCESSLIST からクエリを取得)、レコードがない場合はまったく時間がかからず、1 日のレコードの場合は約 16 秒かかります。

実際の問題は、これを 1 日に 1 回実行すると削除するレコードがあると時間がかかることです。でも、何もすることがないときは、できるだけ早く終了したいと思います。

メソッドの抜粋:

    try {
        $smt = DB::getInstance()->getDbh()->prepare("DELETE FROM " . static::$table . " WHERE dateSent < :date");
        $smt->execute(array(':date' => $date));
        return true;
    } catch (\PDOException $e) {
        // Some logging here removed to ensure a clean test
    }

削除する行が 0 行の場合の結果をログに記録します。

    [debug] ScriptController::actionDeleteHistory() success in 12.82 seconds

削除する行が 0 行の場合の mysql クライアント:

    mysql> DELETE FROM user_history WHERE dateSent < '2013-05-03 13:41:55';
    Query OK, 0 rows affected (0.00 sec)

1 日で削除された場合の結果をログに記録します。

    [debug] ScriptController::actionDeleteHistory() success in 25.48 seconds

削除に 1 日かかる場合の mysql クライアント:

    mysql> DELETE FROM user_history WHERE dateSent < '2013-05-05 13:41:55';
    Query OK, 672260 rows affected (15.70 sec)

PDO が遅い理由はありますか?

乾杯。

コメントへの返信:

両方で同じクエリであるため、インデックスが取得されているか、取得されていないかのどちらかです。そしてそうです。

EXPLAIN SELECT * FROM user_history WHERE dateSent < '2013-05-05 13:41:55' 
1   SIMPLE  user_history range  date_sent   date_sent   4   NULL    4   Using where 

このテストでは、MySQL と Apache が同じサーバー上で実行されています。負荷の問題が発生している場合、mysql はコード内クエリで 13 秒間 100% に達します。mysql クライアント クエリでは、クエリが完了する前に登録する機会がありません。これが PHP/PDO が方程式に追加するものではないことはわかりませんが、私はすべてのアイデアを受け入れます。

:date は PDO プレースホルダーであり、フィールド名は dateSent であるため、mysql キーワードと競合することはありません。それでも、代わりに :dateSent を使用すると、依然として遅延が発生します。

また、プレースホルダーを使用せずにすでに試しましたが、これについて言及するのを怠っていました。ありがとう! これに沿って。PHP/PDO でも同じ遅延が発生します。

DB::getInstance()->getDbh()->query(DELETE FROM user_history WHERE dateSent < '2013-05-03 13:41:55')

また、mysql クライアントでプレースホルダーを使用しても遅延は発生しません。

PREPARE test from 'DELETE FROM user_history WHERE dateSent < ?';
SET @datesent='2013-05-05 13:41:55';
EXECUTE test USING @datesent;
Query OK, 0 rows affected (0.00 sec)

これは MYISAM テーブルなので、トランザクションは関係ありません。

$date の値は、コードの実行中に SHOW PROCESSLIST から取得された mysql クライアントで実行されたクエリに示されているように、削除がないか、または 1 日の削除をテストするために異なります。この場合、メソッドに渡されず、次から派生します。

    if (!isset($date)) {
        $date = date("Y-m-d H:i:s", strtotime(sprintf("-%d days", self::DELETE_BEFORE)));
    }

そして、この時点で、テーブル スキーマが疑問視される可能性があるため、次のようになります。

CREATE TABLE IF NOT EXISTS `user_history` (
  `userId` int(11) NOT NULL,
  `asin` varchar(10) COLLATE utf8_unicode_ci NOT NULL,
  `dateSent` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`userId`,`asin`),
  KEY `date_sent` (`dateSent`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

これは、かなりの規模の Web サイトであり、全体に多くの DB 呼び出しがあります。私は、サイトが危険なルーティングにかかっていることを示唆する他の点で、サイトのパフォーマンスの方法に証拠は見当たりません. 特に、SHOW PROCESSLIST でのこのクエリは、PHP/PDO で実行すると 13 秒までゆっくりと忍び寄りますが、mysql で実行するとまったく時間がかかりません (特に、レコードが削除されない場所を参照すると、13 秒かかります) PHP/PDO のみ)。

現在問題になっているのは、この特定の DELETE クエリだけです。しかし、このプロジェクトの他のどこにも、このような一括 DELETE ステートメントは他にありません。私の考えられる他のプロジェクトでもありません。したがって、問題は、大きなテーブルに対する PDO DELETE クエリに固有のものです。

「それがあなたの答えではありませんか?」- いいえ。問題は、mysql クライアントと比較して、PHP/PDO でこれに大幅に時間がかかるのはなぜですか。SHOW PROCESSLIST は、PHP/PDO で時間がかかるこのクエリのみを示します (レコードが削除されないため)。mysql クライアントではまったく時間がかかりません。それがポイントです。

try-catch ブロックを使用せずに PDO クエリを試しましたが、まだ遅延があります。


また、mysql_* 関数で試してみると、mysql クライアントを直接使用した場合と同じタイミングが示されます。そのため、指は現在 PDO を非常に強く指しています。PDO とやり取りするのは私のコードである可能性がありますが、予期しない遅延が発生するクエリは他にないため、可能性は低いと思われます。

方法:

    $conn = mysql_connect(****);
    mysql_select_db(****);

    $query = "DELETE FROM " . static::$table . " WHERE dateSent < '$date'";
    $result = mysql_query($query);

削除するレコードがない場合のログ:

Fri May 17 15:12:54 [verbose] UserHistory::deleteBefore() query: DELETE FROM user_history WHERE dateSent < '2013-05-03 15:12:54'
Fri May 17 15:12:54 [verbose] UserHistory::deleteBefore() result: 1
Fri May 17 15:12:54 [verbose] ScriptController::actionDeleteHistory() success in 0.01 seconds

削除される 1 日のレコードのログ:

Fri May 17 15:14:24 [verbose] UserHistory::deleteBefore() query: DELETE FROM user_history WHERE dateSent < '2013-05-07 15:14:08'
Fri May 17 15:14:24 [verbose] UserHistory::deleteBefore() result: 1
Fri May 17 15:14:24 [debug] ScriptController::apiReturn(): {"message":true}
Fri May 17 15:14:24 [verbose] ScriptController::actionDeleteHistory() success in 15.55 seconds

そして、メソッドで PDO 接続を作成し、それを使用して、DB シングルトンへの呼び出しを回避しようとしましたが、これもまた遅延が発生します。同じDBシングルトンをすべて使用する他のクエリには他の遅延はありませんが、試してみる価値はありますが、違いはまったく期待できませんでした:

    $connectString = sprintf('mysql:host=%s;dbname=%s', '****', '****');
    $dbh = new \PDO($connectString, '****', '****');
    $dbh->exec("SET CHARACTER SET utf8");
    $dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);

    $smt = $dbh->prepare("DELETE FROM " . static::$table . " WHERE dateSent < :date");
    $smt->execute(array(':date' => $date));

タイムロガーでメソッドを呼び出す:

    $startTimer = microtime(true);
    $deleted = $this->apiReturn(array('message' => UserHistory::deleteBefore()));
    $timeEnd = microtime(true) - $startTimer;
    Logger::write(LOG_VERBOSE, "ScriptController::actionDeleteHistory() success in " . number_format($timeEnd, 2) . " seconds");

DB::connect() に PDO/ATTR_EMULATE_PREPARES を追加しました。レコードをまったく削除しない場合でも、まだ遅延があります。これは以前に使用したことがありませんが、正しい形式のように見えます。

   $this->dbh->setAttribute(\PDO::ATTR_EMULATE_PREPARES, false);

現在の DB::connect() ですが、これに一般的な問題があった場合、すべてのクエリに影響するのでしょうか?

public function connect($host, $user, $pass, $name)
{
    $connectString = sprintf('mysql:host=%s;dbname=%s', $host, $name);
    $this->dbh = new \PDO($connectString, $user, $pass);
    $this->dbh->exec("SET CHARACTER SET utf8");
    $this->dbh->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
 }

インデックスはスキーマの上に示されています。レコードの削除後にインデックスを再構築することに直接関係している場合、mysql は PHP/PDO と同じ時間を要します。そうではありません。これが問題です。このクエリが遅いというわけではありません。時間がかかることが予想されます。PHP/PDO は、mysql クライアントで実行されるクエリや、PHP で mysql lib を使用するクエリよりも著しく遅いということです。


MYSQL_ATTR_USE_BUFFERED_QUERY を試行しましたが、それでも遅延が発生します


DB は標準的なシングルトン パターンです。DB::getInstance()->getDbh() は、上記の DB::connect() メソッドで作成された PDO 接続オブジェクトを返します (例: DB::dbh)。クエリが実行されるのと同じ方法で PDO 接続を作成するときにまだ遅延があるため、DB シングルトンが問題ではないことを証明したと思います (上記の 6 つの編集)。


原因はわかりましたが、なぜ今この瞬間にこれが起こっているのかわかりません。

正しい形式で 1,000 万行のランダムな行を含むテーブルを作成するテスト SQL と、問題のあるクエリを実行する PHP スクリプトを作成しました。また、PHP/PDO または mysql クライアントではまったく時間がかかりません。次に、DB 照合順序をデフォルトの latin1_swedish_ci から utf8_unicode_ci に変更します。PHP/PDO では 10 秒かかり、mysql クライアントではまったく時間がかかりません。それから latin1_swedish_ci に戻すと、PHP/PDO でまったく時間がかかりません。

多田!

これをDB接続から削除すると、どちらの照合でも正常に機能します。したがって、ここにはある種の問題があります。

 $dbh->exec("SET CHARACTER SET utf8");

もっと調べて、あとでフォローアップします。

4

2 に答える 2

4

そう...

この投稿では、欠陥がどこにあったかを説明します。

「SET CHARACTER SET utf8」は必要ですか?

基本的に、それは次の使用でした:

$this->dbh->exec("SET CHARACTER SET utf8");

これは DB::connect() でこれであるべきでした

$this->dbh->exec("SET NAMES utf8");

完全に私のせいです。

mysql サーバー側でクエリを変換して DB の照合順序に一致させる必要があるため、深刻な影響があったようです。上記の投稿は、私ができるよりもはるかに優れた詳細を提供します。

誰かが私の調査結果を確認する必要がある場合は、この一連の SQL クエリによってテスト DB がセットアップされ、自分で確認できるようになります。なんらかの理由でこれらを削除して再度追加する必要があったため、テスト データが入力された後、インデックスが正しく有効になっていることを確認してください。1000 万行を作成します。要点を証明するのに十分ではないかもしれません。

DROP DATABASE IF EXISTS pdo_test;
CREATE DATABASE IF NOT EXISTS pdo_test;
USE pdo_test;

CREATE TABLE IF NOT EXISTS test (
  `userId` int(11) NOT NULL,
  `asin` varchar(10) COLLATE utf8_unicode_ci NOT NULL,
  `dateSent` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`userId`,`asin`),
  KEY `date_sent` (`dateSent`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

drop procedure if exists load_test_data;

delimiter #
create procedure load_test_data()
    begin
        declare v_max int unsigned default 10000000;
        declare v_counter int unsigned default 0;

        while v_counter < v_max do
            INSERT INTO test (userId, asin, dateSent) VALUES (FLOOR(1 + RAND()*10000000), SUBSTRING(MD5(RAND()) FROM 1 FOR 10), NOW());
            set v_counter=v_counter+1;
        end while;
    end #
delimiter ;

ALTER TABLE test DISABLE KEYS;
call load_test_data();
ALTER TABLE test ENABLE KEYS;

# Tests - reconnect to mysql client after each one to reset previous CHARACTER SET

# Right collation, wrong charset - slow
SET CHARACTER SET utf8;
ALTER DATABASE pdo_test COLLATE='utf8_unicode_ci';
DELETE FROM test  WHERE dateSent < '2013-01-01 00:00:00';

# Wrong collation, no charset - fast
ALTER DATABASE pdo_test COLLATE='latin1_swedish_ci';
DELETE FROM test  WHERE dateSent < '2013-01-01 00:00:00';

# Right collation, right charset - fast
SET NAMES utf8;
ALTER DATABASE pdo_test COLLATE='utf8_unicode_ci';
DELETE FROM test  WHERE dateSent < '2013-01-01 00:00:00';
于 2013-05-17T13:22:07.427 に答える
-2

テーブルの分析と最適化を試みます。

http://dev.mysql.com/doc/refman/5.5/en/optimize-table.html

http://dev.mysql.com/doc/refman/5.5/en/analyze-table.html

于 2013-05-17T11:33:52.507 に答える