【问题标题】:Delete huge amounts of data from huge table从巨大的表中删除大量数据
【发布时间】:2013-10-27 06:00:35
【问题描述】:

我有两张桌子。我们称它们为 KEY 和 VALUE。
KEY 很小,大约有 1.000.000 条记录。
VALUE 很大,比如 1.000.000.000 条记录。

在它们之间有一个连接,因此每个 KEY 可能有许多 VALUES。不是外键,意思基本一样。

DDL 看起来像这样

create table KEY (
 key_id int,
 primary key (key_id)
);

create table VALUE (
 key_id int,
 value_id int,
 primary key (key_id, value_id)
);

现在,我的问题。 VALUE 中的所有 key_id 大约有一半已从 KEY 中删除,我需要在两个表仍处于高负载状态时有序地删除它们。

这很容易做到

delete v 
  from VALUE v
  left join KEY k using (key_id)
 where k.key_id is null;

但是,由于不允许在多表删除时使用limit,因此我不喜欢这种方法。这样的删除需要几个小时才能运行,因此无法限制删除。

另一种方法是创建游标来查找所有丢失的key_id,并有限制地将它们一个一个删除。这似乎很慢而且有点倒退。

还有其他选择吗?一些有用的技巧?

【问题讨论】:

  • 有时WHERE NOT EXISTSLEFT JOIN [...] IS NULL 快,但在这种情况下不确定 (stackoverflow.com/questions/6777910/…)。希望它会有所帮助!
  • 你的意思是你已经删除了key,现在要删除VALUE中的孤儿记录,是吗?
  • 好吧,至少那是完全相同的问题:)

标签: mysql


【解决方案1】:

任何尝试在一个事务中删除这么多数据的解决方案都会使回滚段不堪重负并导致很多性能问题。

pt-archiver 是一个很好的帮助工具。它尽可能高效地对中等大小的行执行增量操作。 pt-archiver 可以根据选项复制、移动或删除行。

文档包含删除孤立行的示例,这正是您的场景:

pt-archiver --source h=host,D=db,t=VALUE --purge \
  --where 'NOT EXISTS(SELECT * FROM `KEY` WHERE key_id=`VALUE`.key_id)' \
  --limit 1000 --commit-each

执行此操作将花费更长的时间来删除数据,但它不会使用太多资源,并且不会中断现有数据库的服务。我已经成功使用它清除了数亿行过时的数据。

pt-archiverPercona Toolkit for MySQL 的一部分,Percona Toolkit for MySQL 是一组免费 (GPL) 脚本,可帮助执行 MySQL 和兼容数据库的常见任务。

【讨论】:

    【解决方案2】:

    直接来自MySQL documentation

    如果您要从一个大表中删除许多行,您可能会超出 InnoDB 表的锁定表大小。为了避免这个问题,或者干脆 为了最大限度地减少表保持锁定的时间,以下 策略(根本不使用 DELETE)可能会有所帮助:

    选择不删除的行到与原表结构相同的空表中:

    INSERT INTO t_copy SELECT * FROM t WHERE ... ;
    

    使用 RENAME TABLE 以原子方式将原始表移开,并将副本重命名为原始名称:

    RENAME TABLE t TO t_old, t_copy TO t;
    

    删除原表:

    DROP TABLE t_old;
    

    在重命名 TABLE 时没有其他会话可以访问所涉及的表 执行,因此重命名操作不受并发限制 问题。请参阅第 12.1.9 节,“重命名表语法”。

    所以在你的情况下你可以这样做

    INSERT INTO value_copy SELECT * FROM VALUE WHERE key_id IN
        (SELECT key_id FROM `KEY`);
    
    RENAME TABLE value TO value_old, value_copy TO value;
    
    DROP TABLE value_old;
    

    根据他们写的here RENAME 操作很快,记录数不影响它。

    【讨论】:

    • 问题在于将十亿条记录插入附近的表并提交事务需要一段时间。否则,如果后者处于高负载并正在更新,您将无法将复制表与原始表同步。
    • 我只是做了一个小测试来确定。并且 MySQL 将复制所有 INSERTs 一个 preform 所有将在执行此 INSERT ... SELECT 查询期间进行的更新。
    • 所有应该留在表中的行,即不被删除的行,都在不断更新,这种方法会迫使我阻止写入,直到它完成,因为这个事务将持有读锁(并阻止写入)很长一段时间。
    • 它不会,在第一个查询完成时您将在两个表中拥有完全相同的数据(您要在新表中删除的行除外)。创建新表时将考虑第一个表上的所有更新/插入,并且读锁不会阻止您写入。因此,您可能会失去数据完整性的唯一时间是执行第一个和第二个查询之间的时间。但是,如果您将其作为最小化时间的程序,那应该没问题。
    • 如果在这组语句的第一次插入之后插入了更多行,这些新行即使满足 where 子句也永远不会被保留。在插入副本之前,必须锁定整个表。
    【解决方案3】:

    这个有限制怎么办?

    delete x 
      from `VALUE` x
      join (select key_id, value_id
              from `VALUE` v
              left join `KEY` k using (key_id)
             where k.key_id is null
             limit 1000) y
        on x.key_id = y.key_id AND x.value_id = y.value_id;
    

    【讨论】:

    • 这似乎有效。它比我希望的要慢,但它非常简单,根据我的测量,它可能已经足够快了。
    • 这会损害性能,因为 1) 加入两个大表可能会很慢; 2)嵌套的 SELECT 必须扫描超过 1000 x N 次行才能找到“第一个”要删除的 1000 行; 3) 最后 999 行将是最慢的,因为它将运行两次全索引扫描而不会提前退出; 4) VALUE 中删除的行在表中的位置可能非常随机,IO 不太可能是连续的
    • 你是对的,它似乎仍然比在所有行上进行游标更快,因为不必检索数据。另外,KEY 并不大,只有一百万行左右。
    • 您是否尝试过比较此连接的性能与创建临时表的性能?就我而言,它的速度要快得多,而且不会过于复杂。
    • 分块删除数据减少了整体时间,此外,更改少量配置参数也提高了性能rathishkumar.in/2017/12/…,但在生产服务器上实施时需要一些预防措施。
    【解决方案4】:

    首先,检查您的数据。找到具有太多值以“快速”删除的键。然后找出一天中系统负载最小的时间。在此期间执行“坏”键的删除。其余的,开始一个一个地删除它们,删除之间有一些停机时间,这样您在执行此操作时就不会对数据库施加太大压力。

    【讨论】:

      【解决方案5】:

      可能不是通过 key_id 将整组行分成小部分:

      delete v 
        from VALUE v
        left join KEY k using (key_id)
       where k.key_id is null and v.key_id > 0 and v.key_id < 100000;
      

      然后删除 key_id 为 100000..200000 等的行。

      【讨论】:

      • 没有什么说 key_id 100001 不会有 100 万个 value_id 与之关联,这对于一次删除来说太多了。
      • 答案在 MySQL 上是最好的,它具有最少的索引查找、表扫描和磁盘 IO 访问。如果你的表负载很重,你可以闯入较小的事务以防止从属滞后和锁阻塞。
      【解决方案6】:

      您可以尝试在单独的事务批次中删除。 这适用于 MSSQL,但应该类似。

      declare @i INT
      declare @step INT
      set @i = 0
      set @step = 100000
      
      while (@i< (select max(VALUE.key_id) from VALUE))
      BEGIN
        BEGIN TRANSACTION
        delete from VALUE where
          VALUE.key_id between @i and @i+@step and
          not exists(select 1 from KEY where KEY.key_id = VALUE.key_id and KEY.key_id between @i and @i+@step)
      
        set @i = (@i+@step)
        COMMIT TRANSACTION
      END
      

      【讨论】:

        【解决方案7】:

        创建一个临时表!

        drop table if exists batch_to_delete;
        create temporary table batch_to_delete as
        select v.* from `VALUE` v
        left join `KEY` k on k.key_id = v.key_id
        where k.key_id is null
        limit 10000; -- tailor batch size to your taste
        
        -- optional but may help for large batch size
        create index batch_to_delete_ix_key on batch_to_delete(key_id); 
        create index batch_to_delete_ix_value on batch_to_delete(value_id);
        
        -- do the actual delete
        delete v from `VALUE` v
        join batch_to_delete d on d.key_id = v.key_id and d.value_id = v.value_id;
        

        【讨论】:

          【解决方案8】:

          对我来说,这是一种我希望在日志文件中看到其进度的任务。我会避免在纯 SQL 中解决这个问题,我会在 Python 或其他类似语言中使用一些脚本。另一件让我烦恼的事情是,表之间的大量 WHERE IS NOT NULL 的 LEFT JOIN 可能会导致不需要的锁,所以我也会避免 JOIN。

          这是一些伪代码:

          max_key = select_db('SELECT MAX(key) FROM VALUE')
          while max_key > 0:
              cur_range = range(max_key, max_key-100, -1)
              good_keys = select_db('SELECT key FROM KEY WHERE key IN (%s)' % cur_range)
              keys_to_del = set(cur_range) - set(good_keys)
              while 1:
                  deleted_count = update_db('DELETE FROM VALUE WHERE key IN (%s) LIMIT 1000' % keys_to_del)
                  db_commit
                  log_something
                  if not deleted_count:
                      break
              max_key -= 100
          

          这不会对系统的其余部分造成太大影响,但可能需要很长时间。另一个问题是在删除所有这些行后优化表,但这是另一回事。

          【讨论】:

            【解决方案9】:

            如果目标列被正确索引,这应该很快,

            DELETE FROM `VALUE`
            WHERE NOT EXISTS(SELECT 1 FROM `key` k WHERE k.key_id = `VALUE`.key_id)
            -- ORDER BY key_id, value_id -- order by PK is good idea, but check the performance first.
            LIMIT 1000
            

            将限制从 10 更改为 10000 以获得可接受的性能,然后重新运行几次。

            还要记住,这种批量删除将对每一行执行锁定和备份.. 将每行的执行时间乘以数倍...

            有一些高级方法可以防止这种情况,但最简单的解决方法 只是围绕这个查询进行交易。

            【讨论】:

              【解决方案10】:

              您是否有具有相同数据的 SLAVE 或开发/测试环境?

              如果您担心具有 100 万个 value_ids 的特定键,第一步是找出您的数据分布

              SELECT v.key_id, COUNT(IFNULL(k.key_id,1)) AS cnt 
              FROM `value` v  LEFT JOIN `key` k USING (key_id) 
              WHERE k.key_id IS NULL 
              GROUP BY v.key_id ;
              

              上述查询的EXPLAIN PLAN比添加要好得多

              ORDER BY COUNT(IFNULL(k.key_id,1)) DESC ;
              

              由于您在 key_id 上没有分区(在您的情况下分区太多)并且希望在删除过程中保持数据库运行,因此可以选择在不同 key_id 删除之间使用 SLEEP() 删除,以避免压倒服务器.不要忘记密切关注二进制日志以避免磁盘填充。

              最快的方法是:

              1. 停止应用程序,以免更改数据。
              2. 使用

                从 VALUE 表中转储 key_id 和 value_id,仅在 KEY 表中匹配 key_id

                mysqldump YOUR_DATABASE_NAME 值 --where="key_id in (select key_id from YOUR_DATABASE_NAME.key)" --lock-all --opt --quick --quote-names --skip-extended-insert > VALUE_DATA.txt

              3. 截断 VALUE 表

              4. 加载步骤 2 中导出的数据
              5. 启动应用程序

              与往常一样,在具有生产数据和相同基础架构的开发/测试环境中尝试此操作,以便计算停机时间。

              希望这会有所帮助。

              【讨论】:

              • 也许我没看错,但在第 1 步和第 5 步之间,似乎数据库实际上已关闭,没有应用程序可以访问它的数据。我做不到,整个数据库和这两个表都需要在整个过程中启动并运行。
              • 是的。其他选项是根据 key_id 对 VALUE 表进行分区,但在您的情况下分区太多,或者循环删除基于 key_id 的行并将 SLEEP() 放入循环中,如解释的那样。您是否知道必须从 VALUE 表中删除多少个 key_id?
              • 大约有一半的值需要被删除。该表无法分区,这样做需要对表进行完全重建,并且在发生这种情况时,表已关闭且无法访问。这个问题是要删除数据而不取下表或数据库。如果允许的话,还有更简单的方法,比如一个巨大的删除。
              • 您查看过Percona's pt-archiver utility 吗?您不必将数据传输到另一个表中。阅读网站上的说明。
              • 我查看了 pt-archiver,对我来说似乎太慢了。否则这是一个非常好的工具。
              【解决方案11】:

              我只是好奇在 VALUE 表的 key_id 上添加非唯一索引会产生什么影响。选择性根本不高(~0.001),但我很好奇这将如何影响连接性能。

              【讨论】:

                【解决方案12】:

                为什么不根据 key_id 模块等规则将 VALUE 表拆分为多个 2 的幂(例如 256)?

                【讨论】:

                  猜你喜欢
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 1970-01-01
                  • 2016-07-03
                  • 1970-01-01
                  • 1970-01-01
                  相关资源
                  最近更新 更多