处理数百万行时,PDO DELETE意外地变慢

时间:2013-05-17 07:07:12

标签: php mysql pdo

我正在处理一个大约有1200万行的MYISAM表。方法用于删除早于指定日期的所有记录。该表在日期字段上编制索引。当在代码中运行时,日志显示当没有要删除的记录时这需要大约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正在同一台服务器上运行。如果您遇到加载问题,那么在代码内查询的13秒内,mysql确实达到了100%。在mysql客户端查询中,它在查询完成之前没有机会在顶部注册。我不知道这不是PHP / PDO如何添加到等式中的,但我对所有想法都持开放态度。

:date是PDO占位符,fieldname是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的值不同于测试没有删除或一天的删除,如在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;

这是一个体面的网站,其中包含大量的数据库调用。我认为该网站在任何其他方面的表现方式都没有任何证据表明它可以归结为狡猾的路由。特别是当我在SHOW PROCESSLIST上看到这个查询时,在PHP / PDO中运行时慢慢爬上13秒,但是在mysql中运行时根本不需要时间(特别是指没有记录需要删除的时间需要13秒)仅限PHP / PDO。)

目前只有这个特殊的DELETE查询才有问题。但是我在这个项目的任何其他地方或者我能想到的任何其他项目中都没有这样的批量DELETE语句。因此,问题特别针对大桌子上的PDO DELETE查询。

&#34;那么你的答案是不是?&#34; - 不。问题是为什么与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 lib的查询。


MYSQL_ATTR_USE_BUFFERED_QUERY尝试了,但仍有延迟


DB是标准的单例模式。 DB :: getInstance() - &gt; 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");

我会研究更多,然后再跟进。

2 个答案:

答案 0 :(得分:4)

因此...

这篇文章解释了这个漏洞的位置。

Is "SET CHARACTER SET utf8" necessary?

基本上,它是使用:

$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';

答案 1 :(得分:-2)