删除MySQL中的数百万行

问题描述 投票:62回答:11

我最近发现并修复了我正在处理的网站中的一个错误,导致表中数百万个重复的数据行即使没有它们也会非常大(仍然是数百万)。我可以很容易地找到这些重复的行,并可以运行单个删除查询来杀死它们。问题是尝试一次性删除这么多行会长时间锁定表,如果可能的话我想避免这种情况。我可以看到摆脱这些行的唯一方法,而不是取下网站(通过锁定表):

  1. 编写一个脚本,在循环中执行数千个较小的删除查询。这理论上会解决锁定表问题,因为其他查询将能够进入队列并在删除之间运行。但它仍然会在数据库上加载相当多的负载,并且需要很长时间才能运行。
  2. 重命名表并重新创建现有表(它现在将为空)。然后在重命名的表上进行清理。重命名新表,将旧表命名并将新行合并到重命名的表中。这需要采取相当多的步骤,但应该以最小的中断完成工作。这里唯一棘手的部分是所讨论的表格是一个报表,所以一旦它重新命名,而空的一个放在它的位置,所有历史报告都会消失,直到我把它放回原位。此外,由于存储的数据类型,合并过程可能会有点痛苦。总的来说,这是我现在可能的选择。

我只是想知道是否有其他人之前有这个问题,如果是这样,你如何处理它而不取下网站,并希望,如果有任何中断用户?如果我使用2号或类似的方法,我可以安排这些东西深夜运行并在第二天早上进行合并,并提前告知用户,这不是什么大不了的事。我只是想看看是否有人有更好或更简单的方法来进行清理。

mysql query-performance maintenance sql-delete
11个回答
127
投票
DELETE FROM `table`
WHERE (whatever criteria)
ORDER BY `id`
LIMIT 1000

洗涤,冲洗,重复直至零行受影响。也许在一个脚本中,在迭代之间休眠一两秒钟。


0
投票

我认为缓慢是由于MySQl的“聚集索引”,其中实际记录存储在主键索引中 - 按主键索引的顺序。这意味着通过主键访问记录非常快,因为它只需要一次磁盘提取,因为磁盘上的记录就在那里找到索引中正确的主键。

在没有聚簇索引的其他数据库中,索引本身不保存记录,而只是一个“偏移”或“位置”,指示记录在表文件中的位置,然后必须在该文件中进行第二次提取以检索实际数据。

您可以想象,当删除聚簇索引中的记录时,必须向下移动表中该记录以上的所有记录,以避免在索引中创建大量漏洞(这是我几年前至少记得的 - 以后的版本可能已经改变了这一点)。

知道上面我们发现在MySQL中真正加速删除的是以相反的顺序执行删除。这会产生最少量的记录移动,因为您首先从末尾删除记录,这意味着后续删除具有较少的要重定位的对象。


0
投票

我没有编写任何脚本来执行此操作,并且正确地执行它绝对需要脚本,但另一个选项是创建一个新的重复表并选择要保留的所有行。在此过程完成时,使用触发器使其保持最新状态。当它同步时(减去你要删除的行),重命名一个事务中的两个表,以便新的表取代旧的。放下旧桌子,瞧!

这(显然)需要大量额外的磁盘空间,并且可能会对您的I / O资源造成负担,但除此之外,可能会更快。

根据数据的性质或在紧急情况下,您可以重命名旧表并在其中创建一个新的空表,并在闲暇时选择“保留”行到新表中...


7
投票

我还建议在您的表中添加一些约束,以确保不再发生这种情况。一百万行,每次射击1000次,将完成1000次重复的脚本。如果脚本每3.6秒运行一次,您将在一小时内完成。别担心。您的客户不太可能注意到。


6
投票

以下内容一次删除1,000,000条记录。

 for i in `seq 1 1000`; do 
     mysql  -e "select id from table_name where (condition) order by id desc limit 1000 " | sed 's;/|;;g' | awk '{if(NR>1)print "delete from table_name where id = ",$1,";" }' | mysql; 
 done

你可以将它们组合在一起并删除table_name,其中IN(id1,id2,.. idN)我确定太难了


6
投票

我有一个用例在MySQL的25M +行表中删除1M +行。尝试了不同的方法,如批量删除(如上所述)。 我发现了最快的方法(将所需记录复制到新表中):

  1. 创建仅包含ID的临时表。

CREATE TABLE id_temp_table(temp_id int);

  1. 插入应删除的ID:

插入到id_temp_table(temp_id)中选择.....

  1. 创建新表table_new
  2. 将所有记录从表插入到table_new,而不使用id_temp_table中的不必要的行

insert into table_new .... where table_id NOT IN(从id_temp_table中选择distinct(temp_id));

  1. 重命名表

整个过程耗时约1小时。在我的用例中,简单删除100条记录上的批量需要10分钟。


3
投票

我使用来自优秀的mk-archiver实用程序包的Maatkit(一组用于MySQL管理的Perl脚本)Maatkit来自O'Reilly“High Performance MySQL”一书的作者Baron Schwartz。

目标是一个低影响,仅向前的工作,从表中剔除旧数据,而不会影响OLTP查询。您可以将数据插入到另一个表中,该表不必位于同一服务器上。您也可以以适合LOAD DATA INFILE的格式将其写入文件。或者你也不能做,在这种情况下,它只是一个增量DELETE。

它已经构建为以小批量存档您不需要的行,作为奖励,它可以将删除的行保存到文件中,以防您搞砸了选择要删除的行的查询。

无需安装,只需抓住http://www.maatkit.org/get/mk-archiver并在其上运行perldoc(或阅读网站)以获取文档。


1
投票

分批做,一次说2000行。承诺介于两者之间。百万行不是那么多,这将是快速的,除非你在表上有很多索引。


1
投票

根据mysql documentationTRUNCATE TABLEDELETE FROM的快速替代品。试试这个:

TRUNCATE TABLE table_name

我在50M行上尝试了这个,它在两分钟内完成。

注意:截断操作不是事务安全的;在活动事务或活动表锁定过程中尝试一个错误时发生错误


1
投票

对我们来说,DELETE WHERE %s ORDER BY %s LIMIT %d答案不是一个选项,因为WHERE标准很慢(非索引列),并且会击中master。

从读取副本中选择要删除的主键列表。使用这种格式导出:

00669163-4514-4B50-B6E9-50BA232CA5EB
00679DE5-7659-4CD4-A919-6426A2831F35

使用以下bash脚本来获取此输入并将其块化为DELETE语句[由于mapfile内置需要bash≥4]:

sql-chunker.sh(记得chmod +x我,并改变shebang指向你的bash 4可执行文件):

#!/usr/local/Cellar/bash/4.4.12/bin/bash

# Expected input format:
: <<!
00669163-4514-4B50-B6E9-50BA232CA5EB
00669DE5-7659-4CD4-A919-6426A2831F35
!

if [ -z "$1" ]
  then
    echo "No chunk size supplied. Invoke: ./sql-chunker.sh 1000 ids.txt"
fi

if [ -z "$2" ]
  then
    echo "No file supplied. Invoke: ./sql-chunker.sh 1000 ids.txt"
fi

function join_by {
    local d=$1
    shift
    echo -n "$1"
    shift
    printf "%s" "${@/#/$d}"
}

while mapfile -t -n "$1" ary && ((${#ary[@]})); do
    printf "DELETE FROM my_cool_table WHERE id IN ('%s');\n" `join_by "','" "${ary[@]}"`
done < "$2"

像这样调用:

./sql-chunker.sh 1000 ids.txt > batch_1000.sql

这将为您提供一个格式如此的输出文件(我使用批量大小为2):

DELETE FROM my_cool_table WHERE id IN ('006CC671-655A-432E-9164-D3C64191EDCE','006CD163-794A-4C3E-8206-D05D1A5EE01E');
DELETE FROM my_cool_table WHERE id IN ('006CD837-F1AD-4CCA-82A4-74356580CEBC','006CDA35-F132-4F2C-8054-0F1D6709388A');

然后像这样执行语句:

mysql --login-path=master billing < batch_1000.sql

对于那些不熟悉login-path的人来说,它只是一个快捷方式,无需在命令行输入密码即可登录。


1
投票

我遇到了类似的问题。我们有一个非常大的表,大小约为500 GB,没有分区,在primary_key列上只有一个索引。我们的主人是一台机器,128个核心和512 GAG RAM,我们也有多个奴隶。我们尝试了一些技术来解决行的大规模删除问题。我会在这里列出所有我们发现的最差到最好的 -

  1. 一次读取和删除一行。这是你能做的最糟糕的事情。所以,我们甚至没有试过这个。
  2. 使用primary_key列上的限制查询从数据库中获取第一个“X”行,然后在应用程序中检查要删除的行ID,并使用primary_key id列表触发单个删除查询。因此,每个'X'行有2个查询。现在,这种方法很好但是使用批处理作业在10分钟左右的时间内删除了大约500万行,因此MySQL数据库的从属设备滞后了105秒。在10分钟的活动中延迟105秒。所以,我们不得不停下来。
  3. 在这种技术中,我们在随后的批量提取和大小“X”的删除之间引入了50 ms的延迟。这解决了滞后问题,但我们现在每10分钟删除120-1.3百万行,而技术#2则减少500万行。
  4. 对数据库表进行分区,然后在不需要时删除整个分区。这是我们的最佳解决方案,但它需要预先分区的表。我们遵循了第3步,因为我们有一个非分区的非常旧的表,只有primary_key列的索引。创建分区会花费太多时间,我们处于危机模式。这里有一些与分区有关的链接我发现有用 - Official MySQL ReferenceOracle DB daily partitioning

所以,IMO,如果你能负担得起在你的桌子上创建分区的奢侈,那么选择#4选项,否则,你会遇到选项#3。

© www.soinside.com 2019 - 2024. All rights reserved.