【问题标题】:SQL query execution time dramatically increasingSQL 查询执行时间急剧增加
【发布时间】:2017-08-31 03:08:48
【问题描述】:

我的问题解释如下。

这是我现在在我的服务器上运行的 PHP 代码:

$limit = 10000;
$annee = '2017';

//Counting the lines I need to delete
$sql = " SELECT COUNT(*) FROM historisation.cdr_".$annee." a 
         INNER JOIN transatel.cdr_transatel_v2 b ON a.id_cdr = b.id_cdr ";
$t = $db_transatel->selectAll($sql);

//The number of lines I have to delete
$i = $t[0][0];

do {

    if ($i < $limit) {
        $limit = $i;
    }
    //The problem is comming from that delete
    $selectFromHistoryAndDelete = " DELETE FROM transatel.cdr_transatel_v2
                                    WHERE id_cdr IN (
                                      SELECT a.id_cdr FROM historisation.cdr_".$annee." a
                                      INNER JOIN (SELECT id_cdr FROM historisation.cdr_transatel_v2) b ON a.id_cdr = b.id_cdr
                                    )
                                    LIMIT " . $limit;
    $delete = $db_transatel->exec($selectFromHistoryAndDelete, $params);

    $i = $i - $limit;

} while ($i > 0);

The execution of the query.

如图所示,在前 195 个循环中,执行时间在 13 到 17 秒之间。 第 195 次循环增加到 73 秒,第 196 次循环增加到 1305 秒。

现在查询运行了 2000 秒。

查询正在删除测试表中没有人正确使用的行。

我将第 10,000 行删除 10,000 行,以便查询快速且不会使服务器过载。

我想知道为什么执行时间会这样增加,但我认为最后会更快,因为我认为内部连接会更快,因为它们在表中的行数更少。

有人有想法吗?

编辑:表格引擎是 MyISAM。

【问题讨论】:

  • 回滚段。我认为 DB 在某处保留已删除的行以处理可能的回滚。尝试在每次删除后提交。
  • 遗憾的是我已经知道了,但是我的表在 MyIASM 中的表中,并且它不支持回滚和提交。
  • 当表很大时,IN 太慢了。使用 JOIN 查找要删除的行要快得多。
  • 这是因为存储如何与 MyISAM 一起工作。您需要在删除后运行OPTIMIZE TABLE 以回收空闲表空间。实际上,表空间中存在间隙,这些间隙也在处理中。另一种可能的解决方案是切换到 PARTITION 方法。见:stackoverflow.com/a/41252116/1144627
  • 一般的想法是加入两个表,如果 ID 为 IN,则其中一个列将具有值,而其他行将在该列中具有 NULL 值。在这种情况下,很容易删除 value IS NOT NULL 的所有行。

标签: php mysql sql


【解决方案1】:

根据您的最新评论,内部联接是多余的,因为您要从包含您要加入的值的表中删除。本质上,您必须处理 b.id_cdr = a.id_cdr 两次,因为在 cdr_2017 上比较的值的数量不会被内部连接更改,只是查询要删除的值的数量。

至于增量慢的原因,是因为你手动执行了和SELECT cdr_id FROM cdr_2017 LIMIT 10000 OFFSET x一样的功能。

也就是说,您的查询必须对cdr_2017 执行全表扫描以确定要删除的id 值。当您删除这些值时,SQL 优化器必须进一步通过 cdr_2017 表来检索这些值。

导致

DELETE FROM IN(1,2,3,...10000)
DELETE FROM IN(1,2,3,...20000)
...
DELETE FROM IN(1,2,3,...1000000)

假设cdr_id 是增量主键,要解决此问题,您可以使用从cdr_2017 检索到的最后一个索引来过滤所选值。 这会更快,因为不再需要全表扫描来验证连接的记录,因为您现在在查询的两边都使用了索引值。

$sql = " SELECT COUNT(a.cdr_id) FROM historisation.cdr_".$annee." a 
         INNER JOIN transatel.cdr_transatel_v2 b ON a.id_cdr = b.id_cdr ";
$t = $db_transatel->selectAll($sql);

//The number of lines I have to delete
$i = $t[0][0];

//set starting index
$previous = 0;

do {

    if ($i < $limit) {
        $limit = $i;
    }

    $selectFromHistoryAndDelete = 'DELETE d
FROM transatel.cdr_transatel_v2 AS d
JOIN (
   SELECT @previous := cdr_id AS cdr_id
   FROM historisation.cdr_2017 
   WHERE cdr_id > ' . $previous . '
   ORDER BY cdr_id
   LIMIT 10000
) AS a
ON a.cdr_id = d.cdr_id';

    $db_transatel->exec($selectFromHistoryAndDelete, $params);

    //retrieve last id selected in cdr_2017 to use in next iteration
    $v = $db_transatel->selectAll('SELECT @previous'); //prefer fetchColumn
    $previous = $v[0][0];

    $i = $i - $limit;

} while ($i > 0);

//optionally reclaim table-space
$db_transatel->exec('OPTIMIZE TABLE transatel.cdr_transatel_v2', $params);

您还可以重构以使用cdr_id &gt; $previous AND cdr_id &lt; $last 来删除 order by limit 子句,这也应该会提高性能。

虽然我想指出,在此操作期间,MyISAM 数据库引擎会执行 cdr_transatel_v2 上的表锁定。由于 MySQL 处理并发会话和查询的方式,以这种方式批量删除并没有太大的好处,并且实际上只适用于 InnoDB 和事务。特别是在使用带有 FastCGI 的 PHP 时,而不是 Apache mod_php。由于不在 cdr_transatel_v2 上的其他查询仍将被执行,并且 cdr_transatel_v2 上的写操作仍将排队。如果使用 mod_php,我会将限制减少到 1,000 记录以减少排队时间。 欲了解更多信息,请参阅https://dev.mysql.com/doc/refman/5.7/en/internal-locking.html#internal-table-level-locking


替代方法。

考虑到需要删除的记录数量很多,当删除的记录超过保留的记录时,使用INSERT而不是DELETE进行反转操作会更有利。

#ensure the storage table doesn't exist already
DROP TABLE IF EXISTS cdr_transatel_temp;

#duplicate the structure of the original table
CREATE TABLE transatel.cdr_transatel_temp 
LIKE transatel.cdr_transatel_v2;

#copy the records that are not to be deleted from the original table
INSERT transatel.cdr_transatel_temp
SELECT * 
FROM transatel.cdr_transatel_v2 AS d
LEFT JOIN historisation.cdr_2017 AS b
ON b.cdr_id = d.cdr_id
WHERE b.cdr_id IS NULL;

#replace the original table with the storage table
RENAME TABLE transatel.cdr_transatel_v2 to transatel.backup, 
    transatel.cdr_transatel_temp to cdr_transatel_v2;

#remove the original table
DROP TABLE transatel.backup;

【讨论】:

  • 感谢您花时间思考并回答。如果我能找到足够的时间,我今天肯定会尝试第一个解决方案。 @Vladatr 谈到了 IN 的表演,所以我在没有它的情况下进行了查询。现在删除在 ~0.07 秒内运行。我会尝试看看您的查询是否更快。再次感谢您的帮助!
猜你喜欢
  • 1970-01-01
  • 2014-09-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多