19

我正在使用具有大约 1200 万行的 MISAM 表。一种方法用于删除所有早于指定日期的记录。该表在日期字段上建立索引。当在代码中运行时,日志显示如果没有要删除的记录大约需要 13 秒,如果有 1 天的记录大约需要 25 秒。当在 mysql 客户端中运行相同的查询时(在代码运行时从 SHOW PROCESSLIST 中获取查询),没有记录完全不需要时间,一天的记录大约需要 16 秒。

现实生活中的问题是,当每天运行一次有要删除的记录时,这会花费很长时间,因此更频繁地运行它似乎是合乎逻辑的。但我希望它在无事可做时尽快退出。

方法提取:

    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

mysql客户端删除0行时:

    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

mysql 客户端当 1 天导致删除时:

    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)

这是一个 MISAM 表,因此该表不涉及任何事务。

$date 的值不同以测试没有删除或一天的删除,如在 mysql 客户端上运行的查询所示,该查询取自代码运行时的 SHOW PROCESSLIST。在这种情况下,它不会传递给方法,而是来自:

    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;

这是一个体面的网站,整个网站都有很多数据库调用。我没有看到该网站在任何其他方面的表现方式表明它是由狡猾的路由造成的。尤其是当我在 PHP/PDO 中运行时,我看到 SHOW PROCESSLIST 上的这个查询缓慢上升到 13 秒,但在 mysql 中运行时根本不需要时间(特别是指不删除记录的地方需要 13 秒仅在 PHP/PDO 中)。

目前只有这个特定的 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

要删除的一天记录的日志:

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");

将 PDO/ATTR_EMULATE_PREPARES 添加到 DB::connect()。根本不删除任何记录时仍有延迟。我以前没有使用过这个,但它看起来像正确的格式:

   $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 库的查询慢。


MYSQL_ATTR_USE_BUFFERED_QUERY 试过了,但还是有延迟


DB 是标准的单例模式。DB::getInstance()->getDbh() 返回在上面所示的 DB::connect() 方法中创建的 PDO 连接对象,例如:DB::dbh。我相信我已经证明 DB 单例不是问题,因为在以与执行查询相同的方法创建 PDO 连接时仍然存在延迟(以上 6 次编辑)。


我已经找到了它的原因,但我不知道为什么现在会发生这种情况。

我创建了一个测试 SQL,它创建了一个包含 1000 万行以正确格式随机行的表,以及一个运行有问题的查询的 PHP 脚本。在 PHP/PDO 或 mysql 客户端中完全不需要时间。然后我将数据库排序规则从默认的 latin1_swedish_ci 更改为 utf8_unicode_ci,在 PHP/PDO 中需要 10 秒,而在 mysql 客户端中根本没有时间。然后我将它改回 latin1_swedish_ci 并且再次在 PHP/PDO 中完全不需要时间。

多田!

现在,如果我从数据库连接中删除它,它在任何一种排序规则中都可以正常工作。所以这里有一些问题:

 $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 服务器需要转换查询以匹配数据库的排序规则,这似乎产生了可怕的影响。上面的帖子提供了比我更好的细节。

如果有人需要确认我的发现,这一系列 SQL 查询将设置一个测试数据库并让您自己检查。只需确保在输入测试数据后正确启用索引,因为出于某种原因我不得不删除并重新添加这些索引。它创建了 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 回答