【问题标题】:Deleting millions of rows in MySQL删除 MySQL 中的数百万行
【发布时间】:2010-11-22 01:59:25
【问题描述】:

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

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

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

【问题讨论】:

  • 最近通过存储过程在不到一个小时的时间内删除了生产系统中大约7000万条记录,查看这个页面,它可能对其他人也有帮助rathishkumar.in/2017/12/…
  • 在下面查看我的答案,我解释了为什么在 MySQL 中反向删除比正向删除快多个数量级。在您阅读我的解释之前,这听起来很不直观。

标签: mysql query-performance maintenance sql-delete


【解决方案1】:
DELETE FROM `table`
WHERE (whatever criteria)
ORDER BY `id`
LIMIT 1000

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

【讨论】:

  • 如果你使用 DELETE 和 LIMIT,你真的应该使用 ORDER BY 来使查询具有确定性;不这样做会产生奇怪的影响(包括在某些情况下破坏复制)
  • 请注意one can't combine DELETE ... JOINORDER BYLIMIT
  • 我仍然怀疑数据透视表是否不是最好的方法,但是,我做了一个程序,只是为了保持理智:hastebin.com/nabejehure.pas
  • 这是一个实现这种方法的简单 Python 脚本:gist.github.com/tsauerwein/ffb159d1ab95d7fd91ef43b9609c471d
  • 为什么我们必须在迭代之间休眠?
【解决方案2】:

我有一个用例是在 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 与 id_temp_table 不同(temp_id));

  1. 重命名表

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

【讨论】:

  • 对于第 4 步,您可以左连接以使用索引: insert into table_new ... select ... from table left join id_temp_table t on t.temp_id = table.id where t.temp_id is NULL ;
【解决方案3】:

以下内容一次删除 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) 我确定太难了

【讨论】:

  • 这是唯一适用于 100GB 表的解决方案。限制为 1000 的选择只需几毫秒,但使用相同查询删除仅 1000 条记录需要一个小时,尽管 SSD 已就位。以这种方式删除仍然很慢,但至少每秒一千行而不是一小时。
  • 一次性删除 1 M 记录会杀死你的服务器
  • 我一次可以删除 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 客户端中运行了这些查询,但可能连接超时。
【解决方案4】:

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

【讨论】:

    【解决方案5】:

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

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

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

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

    【讨论】:

    • 我真的很喜欢这种想法!我喜欢它在视觉上很有意义,就像孩子可以理解的玩具一样。
    • 这对我来说真的很重要。在一个有 5M 行的表中删除 10K 行最初需要 5 分钟。然后我将 ORDER BY id DESC LIMIT 10000 添加到删除语句中,只用了 1 秒。后来我一次将大小增加到1M。整个过程耗时 10 分钟。
    • @GaniSimsek 我总是很高兴听到其他人从我的一些“这太疯狂了,它可能会奏效”的想法中受益的案例:)
    【解决方案6】:

    这是推荐的做法:

    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】:

      我遇到了类似的问题。我们有一个非常大的表,大小约为 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 列上建立索引。创建分区会花费太多时间,而且我们处于危机模式。以下是一些与分区相关的链接,我发现它们很有帮助 - Official MySQL ReferenceOracle DB daily partitioning

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

      【讨论】:

        【解决方案8】:

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

        目标是低影响的、仅向前的 蚕食旧数据的工作 不影响 OLTP 查询的表 很多。您可以将数据插入另一个 表,不必在同一个表上 服务器。您也可以将其写入 适合 LOAD 格式的文件 数据文件。或者你也不能做,在 在这种情况下,它只是一个增量 删除。

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

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

        【讨论】:

          【解决方案9】:

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

          【讨论】:

          • 以这种方式删除行的 shell 脚本是否安全?
          【解决方案10】:

          我之前也遇到过同样的情况。数据库迁移期间存储了超过 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】:

            分批进行,一次可以说 2000 行。介于两者之间。一百万行并不多,而且速度很快,除非表上有很多索引。

            【讨论】:

              【解决方案12】:

              我有一个真正加载的基础,需要一直删除一些旧条目。 一些删除查询开始挂起,所以我需要杀死它们, 如果删除太多,整个基地就会变得无响应,所以我需要限制并行运行。 因此,我创建了一个 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 万次,这对我的用例来说还可以。

              【讨论】:

                【解决方案13】:

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

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

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

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

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

                此后,无论要从事务表中删除多少条记录,我的删除查询都会在不到几秒的时间内执行。

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

                【讨论】:

                  【解决方案14】:

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

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

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

                  【讨论】:

                    【解决方案15】:

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

                    TRUNCATE TABLE 表名

                    我在 50M 行上进行了尝试,并在两分钟内完成。

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

                    【讨论】:

                    • 这肯定会删除行。不过,我很确定 OP 想要有选择性。
                    猜你喜欢
                    • 2021-08-03
                    • 2019-10-22
                    • 2019-07-12
                    • 2019-02-15
                    • 1970-01-01
                    • 2021-01-05
                    • 2020-12-15
                    • 2012-01-07
                    • 2015-06-02
                    相关资源
                    最近更新 更多