94

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

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

我只是想知道以前是否有其他人遇到过这个问题,如果有,你是如何在不关闭网站的情况下处理它的,并且希望对用户的干扰最小?如果我采用 2 号或其他类似的方法,我可以安排这些东西在深夜运行,并在第二天一大早进行合并,并提前让用户知道,所以这不是什么大不了的事。我只是想看看是否有人对更好或更简单的清理方法有任何想法。

4

16 回答 16

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

清洗,冲洗,重复直到零行受到影响。也许在迭代之间休眠一到三秒的脚本中。

于 2009-08-23T16:40:51.390 回答
13

我有一个用例在 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 (select distinct(temp_id) from id_temp_table);

  1. 重命名表

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

于 2017-11-22T12:50:10.350 回答
10

以下内容一次删除 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 where IN (id1,id2,..idN) 我确定太难了

于 2016-04-13T18:57:24.073 回答
8

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

于 2009-08-23T16:50:42.900 回答
6

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

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

您可以想象,当删除聚集索引(如 MySQL 使用)中的记录时,必须向下移动索引(=表)中该记录之上的所有记录,以避免在索引中创建大量空洞(这就是我记得的至少在几年前——8.x 版可能已经改善了这个问题)。

有了上述“幕后”操作的知识,我们发现在 MySQL 5.x 中真正加快删除速度的是以相反的顺序执行删除。这会产生最少的记录移动量,因为您首先从末尾删除记录,这意味着后续删除需要重新定位的记录更少 - 逻辑对吗?!

于 2019-03-19T19:46:50.170 回答
4

这是推荐的做法:

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 高性能

于 2020-01-06T03:07:59.967 回答
3

我遇到了类似的问题。我们有一个非常大的表,大小约为 500 GB,没有分区,primary_key 列上只有一个索引。我们的主人是一台机器,有 128 个内核和 512 Gigs 的 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 卡住。

于 2018-11-03T14:31:37.167 回答
2

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

目标是一个低影响、只进的工作,从表中剔除旧数据,而不会对 OLTP 查询产生太大影响。您可以将数据插入到另一个表中,而不必在同一台服务器上。您还可以将其写入适合 LOAD DATA INFILE 格式的文件。或者你不能这样做,在这种情况下它只是一个增量删除。

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

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

于 2009-08-26T00:11:05.773 回答
2

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

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

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

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

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

于 2017-11-29T15:08:24.510 回答
2

我之前也有过同样的情况。数据库迁移期间存储了超过 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;
于 2021-01-27T13:39:52.717 回答
1

分批进行,一次说 2000 行。介于两者之间。一百万行并不多,这会很快,除非表上有很多索引。

于 2009-08-23T16:42:21.420 回答
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 万次删除,这对我的用例来说还可以。

于 2020-04-30T16:26:22.863 回答
0

在将事务表中的多条记录移动到存档表后,我遇到了类似的问题。

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

我使用“archive_temp”存储在内存中创建的 id 的临时表,没有任何索引。

因此,在从原始事务表中删除记录时,例如 DELETE from tat where id in (select id from archive_temp); 用于返回错误“与服务器的连接丢失”的查询

创建该临时表后,我按如下方式在该临时表上创建了索引: ALTER TABLE archive_tempADD INDEX( id);

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

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

于 2020-11-18T09:28:49.113 回答
0

这会在几秒钟内查询 Delete a BIG TABLES:

创建表<my_table_temp> LIKE <my_table> ;

将表<my_table>重命名为 < my_table_delete >

将表<my_table_temp>重命名为 < my_table >

删除表<my_table_delete> ;

于 2022-01-25T11:49:19.707 回答
-3

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

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

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

于 2019-04-29T13:21:47.750 回答
-9

根据mysql 文档TRUNCATE TABLE是一个快速的替代方案DELETE FROM。试试这个:

截断表表名

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

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

于 2016-03-06T15:20:12.350 回答