【问题标题】:How can I improve DELETE FROM performance on large InnoDB tables?如何提高大型 InnoDB 表的 DELETE FROM 性能?
【发布时间】:2013-01-11 18:16:07
【问题描述】:

我有一个相当大的 InnoDB 表,其中包含大约 1000 万行(并且不断增加,预计会变成该大小的 20 倍)。每行不是那么大(平均为 131 B),但有时我不得不删除其中的一大块,这需要很长时间。这是表结构:

 CREATE TABLE `problematic_table` (
    `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
    `taxid` int(10) unsigned NOT NULL,
    `blastdb_path` varchar(255) NOT NULL,
    `query` char(32) NOT NULL,
    `target` int(10) unsigned NOT NULL,
    `score` double NOT NULL,
    `evalue` varchar(100) NOT NULL,
    `log_evalue` double NOT NULL DEFAULT '-999',
    `start` int(10) unsigned DEFAULT NULL,
    `end` int(10) unsigned DEFAULT NULL,
    PRIMARY KEY (`id`),
    KEY `taxid` (`taxid`),
    KEY `query` (`query`),
    KEY `target` (`target`),
    KEY `log_evalue` (`log_evalue`)
) ENGINE=InnoDB AUTO_INCREMENT=7888676 DEFAULT CHARSET=latin1;

从表中删除大块的查询就像这样:

DELETE FROM problematic_table WHERE problematic_table.taxid = '57';

这样的查询只用了将近一个小时就完成了。我可以想象索引重写开销使这些查询非常慢。

我正在开发一个将在预先存在的数据库上运行的应用程序。我很可能无法控制服务器变量,除非我强制更改它们(我不希望这样做),所以恐怕更改这些的建议没有什么价值。

我尝试INSERT ... SELECT那些我不想删除到临时表中的行并只是删除其余的行,但是随着删除与保留的比例转向保留,这不再是一个有用的解决方案。

这是一张将来可能会频繁出现INSERTs 和SELECTs 的表格,但不会出现UPDATEs。基本上,它是一个需要不时删除部分内容的日志记录和引用表。

我可以通过限制索引的长度来改进此表上的索引吗?在交易期间切换到支持DISABLE KEYS 的 MyISAM 会有所帮助吗?我还能尝试什么来提高DELETE 的性能?

编辑:这样的删除之一是大约一百万行。

【问题讨论】:

  • 它可能没有您需要的那么多帮助,但可以改进数据类型。 id 列真的需要是BIGINT 吗? INT 是大小的一半,最高可达 40 亿,远高于您预计的最大值。 query 列可能占用了大部分空间。如果不总是 32 个字符,请将其设为 VARCHARquery 上的索引也可能是有限的,如果你真的不需要它。
  • @G-Nugget:感谢您的建议。我将考虑将id 转换为INT。是的,query 列需要 32 个字符,因为这些是 SHA-256 哈希值。不过,我可能应该限制它的索引。大多数时候,此列用于标识行;如果我将它用作PRIMARY KEY 会有帮助吗(id 并没有真正用于查找记录)?
  • 也许这是一个类似的问题,见stackoverflow.com/a/13742306/1741542
  • @inhan:将BIGINT 转换为INT 没有任何明显的区别。
  • 我的“大删除”论文:mysql.rjweb.org/doc.php/deletebig

标签: mysql performance innodb


【解决方案1】:

我有一个类似的场景,一个有 200 万行的表和一个删除语句,它应该删除大约 10 万行 - 这样做大约需要 10 分钟。

检查配置后,我发现 MySQL Server 以默认 innodb_buffer_pool_size = 8 MB (!) 运行。

innodb_buffer_pool_size = 1.5GB 重启后,同样的场景需要 10 秒。

所以看起来“重新排序表”是否适合 buffer_pool 似乎存在依赖关系。

【讨论】:

  • 这是增加缓冲池大小的好习惯吗?
  • 当然。我们有一个大型 Magento 数据库正在运行,innodb_buffer_pool_size 设置为 5G,是该服务器上整个 RAM 大小的四分之一。大多数其他 MySQL 变量对性能的影响没有这个变量那么大,所以把你所有的 RAM 字节都扔在那里。
【解决方案2】:

我有一个包含大约 2 亿行的 InnoDB 表,我确实遇到了同样的问题。 删除行需要很长时间。

表上有一个主键、一个唯一键和多个复合索引。

当以较小的块删除时,它进行得非常快,所以我决定创建一个存储过程,它可以在多次迭代中简单地删除行,但有限制。 有点像 Jan Larsen 的回答,但不需要单独的表格。

这使得在几分钟内删除大量数据(大约 500K 行)成为可能。

似乎 InnoDB 为了能够回滚错误更改而必须进行的事务太大,因此无法放入内存,这导致删除执行非常糟糕。

程序:

CREATE DEFINER=`root`@`%` PROCEDURE `delete_rows`()
BEGIN
    declare v_max int unsigned default 100;
    declare v_counter int unsigned default 1;

        while v_counter < v_max do
            DELETE from items where a = 'A' AND b = 'B' AND c = 'C' LIMIT 10000;
            set v_counter=v_counter+1;
        end while;
END

然后调用它:

CALL delete_rows();

where语句匹配以a,b,c-columns开头的复合索引,我认为这很重要,这样MySQL就不必进行全表扫描来匹配行。

【讨论】:

    【解决方案3】:

    我通过使用存储过程解决了类似的问题,从而将性能提高了数千倍。

    我的表有 33M 行和多个索引,我想删除 10K 行。我的数据库在 Azure 中,无法控制 innodb_buffer_pool_size。

    为简单起见,我创建了一个表 tmp_id,其中只有一个主要的 id 字段:

    CREATE TABLE `tmp_id` (
        `id` bigint(20) NOT NULL DEFAULT '0',
        PRIMARY KEY (`id`)
    )
    

    我在tmp_id 中选择了我想删除的一组ID,然后运行delete from my_table where id in (select id from tmp_id); 这在12 小时内没有完成,所以我尝试在tmp_id 中只使用一个ID,花了25 分钟。执行delete from my_table where id = 1234 在几毫秒内完成,所以我决定尝试在一个过程中执行此操作:

    CREATE PROCEDURE `delete_ids_in_tmp`()
    BEGIN
        declare finished integer default 0;
        declare v_id bigint(20);
        declare cur1 cursor for select id from tmp_id;
        declare continue handler for not found set finished=1;    
        open cur1;
        igmLoop: loop
            fetch cur1 into v_id;
            if finished = 1 then leave igmLoop; end if;
            delete from problematic_table where id = v_id;
        end loop igmLoop;
        close cur1;
    END
    

    现在call delete_ids_in_tmp(); 在不到一分钟的时间内删除了所有 10K 行。

    【讨论】:

    【解决方案4】:

    此解决方案在完成后可以提供更好的性能,但实施过程可能需要一些时间。

    可以添加一个新的BIT 列,默认为TRUE 用于“活动”,FALSE 用于“非活动”。如果这还不够状态,您可以使用 TINYINT 和 256 个可能的值。

    添加此新列可能需要很长时间,但一旦完成,您的更新应该会快得多,只要您在 PRIMARY 上进行操作,就像您在删除时所做的那样,并且不对这个新列编制索引.

    InnoDB 在你这么大的表上花费这么长时间DELETE 的原因是因为集群索引。它会根据您的PRIMARY,首先是找到的UNIQUE,或者如果找不到PRIMARYUNIQUE,它可以确定为适当的替代品,因此当删除一行时,它会对您的表进行物理排序现在在磁盘上对整个表进行物理重新排序,以提高速度和碎片整理。所以不是DELETE 花了这么长时间;这是删除该行后的物理重新排序。

    当您创建一个固定宽度的列并对其进行更新而不是删除时,无需在您的大表中进行物理重新排序,因为行和表本身占用的空间是恒定的。

    在非工作时间,单个DELETE 可用于删除不必要的行。此操作仍然会很慢,但总体上比删除单个行要快得多。

    【讨论】:

    • 感谢您的回答以及对这种巨大减速原因的解释。您对“状态”列的想法不错,但会导致许多不再使用的旧数据。空间或停机时间不是问题,但我为什么要把这些垃圾留在我的数据库中? :) 如果该数据库中没有可变长度列,即如果我将 varchar 列更改为 char,它会对 InnoDB 有帮助吗?
    • 接受晚了,但总比没有好;)
    • INT 列长度与存储无关。 INT(1) 仍然是 4 个字节,您可以将 SIGNED 的最大值 2147483647 和 UNSIGNED 的最大值 4294967295 存储在哪里。
    • 您能提供此声明的引用吗?这似乎是最不可能的。如果 DELETE 真的做到了这一切,就不会发生其他任何事情了。
    • “在磁盘上对整个表进行物理重新排序以提高速度和碎片整理”——这是错误的。 InnoDB 行删除首先进入“更改缓冲区”;这实际上延迟了删除的工作。当删除发生时,从它所在的一个块中删除一行,然后考虑将该块与其邻居合并。没有大规模重组。
    【解决方案5】:
        DELETE FROM problematic_table WHERE problematic_table.taxid = '57';
    

    删除引号,由于taxid 是整数并且在引号中传递值使其成为字符串,由于整数和字符串之间的比较,它不会选择索引。

        DELETE FROM problematic_table WHERE problematic_table.taxid = 57;
    

    【讨论】:

    【解决方案6】:

    MySQL 优化器无处不在,它会在某些环境而不是其他环境上运行全表扫描。使用上面的一些想法,我的最终解决方案是将 PK id 字段存储在临时表中,并通过到临时表的内部连接删除它们。这似乎强制使用 PK 索引并阻止全表扫描。

    CREATE TEMPORARY TABLE d_table (id bigint);
    INSERT INTO d_table SELECT id FROM my_table WHERE tax_id = 333; -- Select PK id into temp table
    DELETE mt.* FROM my_table mt INNER JOIN d_table dt ON mt.id=dt.id; -- Delete by PK via JOIN to temp table to prevent full table scan
    DROP TEMPORARY TABLE d_table;
    

    【讨论】:

      猜你喜欢
      • 2012-12-21
      • 2018-07-11
      • 1970-01-01
      • 2012-08-24
      • 1970-01-01
      • 2010-10-09
      • 2013-03-28
      • 2013-06-30
      • 2021-08-01
      相关资源
      最近更新 更多