删除MySQL中的数百万行

时间:2009-08-23 16:39:33

标签: mysql query-performance maintenance sql-delete

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

  1. 编写一个脚本,在循环中执行数千个较小的删除查询。这理论上会解决锁定表问题,因为其他查询将能够进入队列并在删除之间运行。但是它仍会在数据库上加载相当多的负载,并且需要很长时间才能运行。
  2. 重命名表并重新创建现有表(现在它将为空)。然后在重命名的表上进行清理。重命名新表,将旧表命名并将新行合并到重命名的表中。这需要采取相当多的步骤,但应该以最小的中断完成工作。这里唯一棘手的部分是所讨论的表格是一个报表,所以一旦它重新命名,而空的一个放在它的位置,所有历史报告都会消失,直到我把它放回原位。此外,由于存储的数据类型,合并过程可能会有点痛苦。总的来说,这是我现在可能的选择。
  3. 我只是想知道是否有其他人之前遇到过这个问题,如果是这样的话,你是如何处理它而不占用网站的,并且希望尽可能减少对用户的干扰?如果我使用2号或类似的方法,我可以安排这些东西深夜运行并在第二天早上进行合并,并提前告知用户,这不是什么大不了的事。我只是想看看是否有人想要更好或更简单的方法进行清理。

16 个答案:

答案 0 :(得分:126)

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

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

答案 1 :(得分:7)

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

答案 2 :(得分: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)我确定太难了

答案 3 :(得分:6)

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

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

    CREATE TABLE id_temp_table(temp_id int);

    1. 插入应删除的ID:
    2.   

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

      1. 创建新表table_new

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

      3.   

        插入table_new ....其中table_id NOT IN(选择   来自id_temp_table的distinct(temp_id);

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

答案 4 :(得分:3)

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

  

目标是低影响力,仅限前锋   工作蚕食旧数据   表不影响OLTP查询   许多。您可以将数据插入另一个   表,不必相同   服务器。你也可以写它   文件格式适合LOAD   数据信息。或者你也不能   在哪种情况下,它只是一个增量   DELETE

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

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

答案 5 :(得分:2)

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

TRUNCATE TABLE table_name

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

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

答案 6 :(得分:2)

这是推荐的做法:

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 High Performance

答案 7 :(得分:1)

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

答案 8 :(得分:1)

对我们来说,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的人来说,这只是在命令行中输入密码而无需登录的快捷方式。

答案 9 :(得分:1)

我遇到了类似的问题。我们有一个非常大的表,大小约为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分钟删除120-130万行,而技术2中则删除了500万行。
  4. 对数据库表进行分区,然后在不需要时删除整个分区。这是我们拥有的最好的解决方案,但它需要一个预先分区的表。我们遵循步骤3,因为我们有一个未分区的非常旧的表,只在primary_key列上建立了索引。创建分区会花费太多时间,而我们处于危机模式。以下是一些与分区相关的链接,我发现它们很有帮助-Official MySQL ReferenceOracle DB daily partitioning

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

答案 10 :(得分:1)

我之前也遇到过同样的情况。数据库迁移过程中存储了超过 4500 万条重复数据。是的,它发生了。 :)

我所做的是:

  • 创建了一个只过滤唯一的临时表
  • 截断原始表
  • 从临时表插入回原始表。
  • 在确保数据正确后,我删除了临时表。

总的来说,我猜大概需要 2.5 分钟。

示例:

CREATE TABLE mytable_temp AS SELECT * FROM my_original_table WHERE my_condition;
TRUNCATE TABLE my_original_table;
INSERT INTO my_original_table  SELECT * FROM mytable_temp;

答案 11 :(得分:0)

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

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

您可以想象一下,在删除聚集索引中的记录时,表中该记录上方的所有记录都必须向下移动,以避免在索引中创建大量漏洞(这至少是我几年前记得的) -更高版本可能对此进行了更改。)

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

答案 12 :(得分:0)

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

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

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

答案 13 :(得分: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万个数据,这对我的用例来说还可以。

答案 14 :(得分:0)

在将多个记录移到归档表后从事务表中删除时,我遇到了类似的问题。

我曾经使用临时表来标识要删除的记录。

我使用'archive_temp'来存储在内存中创建的ID且没有任何索引的临时表。

因此,当从原始交易表中删除记录时,例如 从id所在的tat中删除(从archive_temp中选择id); 查询用于返回错误“与服务器的连接丢失”

创建临时表后,我在该临时表上创建了索引,如下所示: 更改表archive_temp添加索引(id);

此后,无论要从事务表中删除的记录数如何,我的删除查询都在不到几秒钟的时间内执行。

因此,最好检查一下索引。 希望这会有所帮助。

答案 15 :(得分:-1)

创建一个包含以下内容的bat文件

cd C:\Program Files\MySQL\MySQL Server 8.0\bin for /L %%N IN (1, 1, 1000) DO ( mysql -u user -ppassword -e "Delete from database.tablename where id > 5000 order by id desc limit 1000" ) Pause