繁体   English   中英

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

[英]PDO DELETE unexpectedly slow when working with millions of rows

我正在使用一个大约有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在同一服务器上运行。 如果你遇到了一个加载问题,那么mysql在代码内查询的13秒内确实达到了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查询。

“那不是你的答案吗?” - 不。问题是为什么与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() - > 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");

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

所以...

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

是否需要“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';

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM