在MySQL中删除数百万行


77

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

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

我只是想知道以前是否有人遇到过这个问题,如果是的话,您如何在不关闭站点的情况下进行处理,希望对用户的干扰最小(如果有的话)?如果我使用2号或其他类似的方法,我可以将这些内容安排在深夜运行,并在第二天清晨进行合并,并且只需要提前通知用户即可,所以这不是什么大问题。我只是想看看是否有人对更好或更轻松的清理方式有任何想法。



通过存储过程约70万条记录,在生产系统最近删除了不到一个小时,看看这个页面,它可以帮助其他人也rathishkumar.in/2017/12/...
Rathish

Answers:


153
DELETE FROM `table`
WHERE (whatever criteria)
ORDER BY `id`
LIMIT 1000

洗涤,漂洗,重复直到影响零行。也许在脚本之间,在两次迭代之间休眠一到两秒。


7
吻总是赢。
电子版

53
如果将DELETE和LIMIT一起使用,则应真正使用ORDER BY来使查询具有确定性。不这样做会产生奇怪的影响(在某些情况下包括破坏复制)
MarkR

是啊,没错。每编辑一次。
混乱

6
请注意,不能 DELETE ... JOINORDER BY结合使用LIMIT
主教

3
我仍然怀疑枢轴表是否不是最好的方法,但是我还是制定了一个程序,只是为了保持理智:hastebin.com/nabejehure.pas
Diogo Medeiros

9

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

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

创建表id_temp_table(temp_id int);

  1. 插入应删除的ID:

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

  1. 创建新表table_new

  2. 将所有记录从表插入到table_new,而id_temp_table中没有不必要的行

插入table_new...。where table_id NOT IN(从id_temp_table中选择与众不同的(temp_id));

  1. 重命名表

整个过程大约需要1个小时。在我的用例中,简单删除100条记录上的批处理花费了10分钟。


对于第4步,您可以左联接以使用索引:插入table_new ...从表左联接中选择... t_temp_id = table.id上的id_temp_table t,其中t.temp_id为NULL;
Softlion

8

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


7

以下将删除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)肯定没有太多困难


1
这是唯一适用于100GB表的解决方案。限制为1000的Select仅需几毫秒,但是尽管有SSD,但使用相同查询进行的删除只花了一个小时的1000条记录。删除这种方法仍然很慢,但至少每秒至少有一千行,而不是一小时。
user2693017 '17


我能够一次删除100,000条记录(DELETE FROM table WHERE id <= 100000,然后删除200000,依此类推)。每批次花费30秒至1分钟。但是,当我以前尝试一次删除1,300,000时,查询运行了至少30分钟,然后失败了,ERROR 2013 (HY000): Lost connection to MySQL server during query.因为我在与服务器相同的虚拟机上的MySQL客户端中运行了这些查询,但是连接可能超时了。
Buttle Butkus

3

我将使用来自出色的Maatkit实用程序包(一堆用于MySQL管理的Perl脚本)中的mk-archiver

目标是一种低影响的只进作业,将旧数据从表中删除,而又不会对OLTP查询产生太大影响。您可以将数据插入另一个表,该表不必位于同一服务器上。您也可以将其以适合于LOAD DATA INFILE的格式写入文件中。或者您既不能执行任何操作,在这种情况下,它只是增量的DELETE。

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

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


3

建议的做法是:

rows_affected = 0
do {
 rows_affected = do_query(
   "DELETE FROM messages WHERE created < DATE_SUB(NOW(),INTERVAL 3 MONTH)
   LIMIT 10000"
 )
} while rows_affected > 0

一次删除10,000行通常是一个足够大的任务,可以使每个查询高效,而又足够短的任务,可以最大程度地减少对服务器的影响4(事务存储引擎可能会从较小的事务中受益)。在DELETE语句之间添加一些睡眠时间以随时间推移分散负载并减少持有锁的时间量也是一个好主意。

参考MySQL高性能


2

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

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

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,它只是无需在命令行中输入密码即可登录的快捷方式。


2

我遇到了类似的问题。我们有一个非常大的表,大小约为500 GB,没有分区,并且primary_key列上只有一个索引。我们的主服务器是一台庞大的机器,有128个内核和512 Gig的RAM,我们也有多个从服务器。我们尝试了一些技术来解决大规模删除行的问题。我将在这里列出我们发现的从最坏到最好的所有内容,

  1. 一次获取和删除一行。这是您绝对可以做的。因此,我们甚至都没有尝试过。
  2. 使用primary_key列上的限制查询从数据库中获取前“ X”行,然后在应用程序中检查要删除的行ID,并使用primary_key ID列表触发一个删除查询。因此,每个“ X”行2个查询。现在,这种方法很好,但是使用批处理作业在10分钟左右的时间内删除了大约500万行,这导致MySQL数据库的从属服务器滞后了105秒。10分钟活动中有105秒的延迟。因此,我们不得不停下来。
  3. 在这项技术中,我们在后续的批量抓取与删除大小分别为“ X”的代码之间引入了50毫秒的延迟。这解决了滞后问题,但是我们现在每10分钟删除1.2-130万行,而技术2中则删除了500万行。
  4. 对数据库表进行分区,然后在不需要时删除整个分区。这是我们拥有的最好的解决方案,但它需要一个预先分区的表。我们遵循步骤3,因为我们有一个未分区的非常旧的表,只在primary_key列上建立了索引。创建分区会花费太多时间,而我们处于危机模式。这里有一些与分区有关的链接,我发现它们很有帮助-官方MySQL参考Oracle DB日常分区

因此,IMO,如果您有足够的能力在表中创建分区,请选择选项#4,否则,您将选择选项#3。


1

批量执行一次,例如说2000行。中间提交。一百万行不是很多,这将很快,除非表上有很多索引。


0

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

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

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

知道以上内容后,我们发现在MySQL中真正加快了删除速度,是按照相反的顺序执行删除操作。这将产生最少数量的记录移动,因为您从头开始就是删除记录,这意味着后续的删除操作具有较少的重定位对象。


0

我有一个真正的基础,需要一直删除一些较旧的条目。一些删除查询开始挂起,因此我需要杀死它们,如果删除太多,整个数据库将变得无响应,因此我需要限制并行运行。因此,我cron job开始每分钟创建一个运行此脚本的脚本:

#!/bin/bash

#######################
#
i_size=1000
max_delete_queries=10
sleep_interval=15
min_operations=8
max_query_time=1000

USER="user"
PASS="super_secret_password"

log_max_size=1000000
log_file="/var/tmp/clean_up.log"
#
#######################

touch $log_file
log_file_size=`stat -c%s "$log_file"`
if (( $log_file_size > $log_max_size ))
then
    rm -f "$log_file"
fi 

delete_queries=`mysql -u user -p$PASS -e  "SELECT * FROM information_schema.processlist WHERE Command = 'Query' AND INFO LIKE 'DELETE FROM big.table WHERE result_timestamp %';"| grep Query|wc -l`

## -- here the hanging DELETE queries will be stopped
mysql-u $USER -p$PASS -e "SELECT ID FROM information_schema.processlist WHERE Command = 'Query' AND INFO LIKE 'DELETE FROM big.table WHERE result_timestamp %'and TIME>$max_query_time;" |grep -v ID| while read -r id ; do
    echo "delete query stopped on `date`" >>  $log_file
    mysql -u $USER -p$PASS -e "KILL $id;"
done

if (( $delete_queries > $max_delete_queries ))
then
  sleep $sleep_interval

  delete_queries=`mysql-u $USER -p$PASS -e  "SELECT * FROM information_schema.processlist WHERE Command = 'Query' AND INFO LIKE 'DELETE FROM big.table WHERE result_timestamp %';"| grep Query|wc -l`

  if (( $delete_queries > $max_delete_queries ))
  then

      sleep $sleep_interval

      delete_queries=`mysql -u $USER -p$PASS -e  "SELECT * FROM information_schema.processlist WHERE Command = 'Query' AND INFO LIKE 'DELETE FROM big.table WHERE result_timestamp %';"| grep Query|wc -l`

      # -- if there are too many delete queries after the second wait
      #  the table will be cleaned up by the next cron job
      if (( $delete_queries > $max_delete_queries ))
        then
            echo "clean-up skipped on `date`" >> $log_file
            exit 1
        fi
  fi

fi

running_operations=`mysql-u $USER -p$PASS -p -e "SELECT * FROM INFORMATION_SCHEMA.PROCESSLIST WHERE COMMAND != 'Sleep';"| wc -l`

if (( $running_operations < $min_operations ))
then
    # -- if the database is not too busy this bigger batch can be processed
    batch_size=$(($i_size * 5))
else 
    batch_size=$i_size
fi

echo "starting clean-up on `date`" >>  $log_file

mysql-u $USER -p$PASS -e 'DELETE FROM big.table WHERE result_timestamp < UNIX_TIMESTAMP(DATE_SUB(NOW(), INTERVAL 31 DAY))*1000 limit '"$batch_size"';'

if [ $? -eq 0 ]; then
    # -- if the sql command exited normally the exit code will be 0
    echo "delete finished successfully on `date`" >>  $log_file
else
    echo "delete failed on `date`" >>  $log_file
fi

这样,我每天可以删除大约200万个,对于我的用例来说还可以。


-3

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

(显然)这需要大量额外的磁盘空间,并且可能会占用您的I / O资源,但否则可能会更快。

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


-4

根据mysql文档,它TRUNCATE TABLE是的快速替代品DELETE FROM。试试这个:

TRUNCATE TABLE table_name

我在5000万行中尝试了此操作,并在两分钟内完成了操作。

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


25
这肯定会删除行。我很确定OP希望有选择性。
亚当·罗
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.