15

我有两张桌子。我们称它们为 KEY 和 VALUE。
KEY 很小,大约有 1.000.000 条记录。
价值是巨大的,比如 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,并有限制地将它们一个一个删除。这似乎很慢,有点倒退。

还有其他选择吗?一些可以提供帮助的好技巧?

4

12 回答 12

23

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

一个很好的帮助工具是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的一部分,这是一组免费 (GPL) 脚本,可帮助执行 MySQL 和兼容数据库的常见任务。

于 2013-10-24T22:53:48.817 回答
8

直接来自MySQL 文档

如果要从大表中删除许多行,则可能会超出 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;

在 RENAME 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;

根据他们在这里写的内容,重命名操作很快,记录数不影响它。

于 2013-10-24T22:39:39.333 回答
5

这个有限制怎么办?

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;
于 2013-10-22T16:41:54.397 回答
2

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

于 2013-10-24T11:32:40.157 回答
1

可能不是通过 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 的行,依此类推。

于 2013-10-18T11:52:12.653 回答
1

您可以尝试在单独的事务批次中删除。这适用于 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
于 2013-10-18T12:06:42.587 回答
1

创建一个临时表!

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;
于 2013-10-24T09:27:02.113 回答
1

对我来说,这是一种任务,我希望在日志文件中看到它的进度。我会避免在纯 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

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

于 2013-10-24T18:28:13.053 回答
1

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

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 以获得可接受的性能,然后重新运行几次。

还要记住,这种大规模删除将为每一行执行锁定和备份.. 将每行的执行时间加倍......

有一些高级方法可以防止这种情况,但最简单的解决方法是围绕这个查询放置一个事务。

于 2013-10-25T07:44:00.590 回答
0

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

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

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 ;

上述查询的解释计划比添加要好得多

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. 开始申请

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

希望这可以帮助。

于 2013-10-22T16:52:03.660 回答
0

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

于 2013-10-28T16:03:48.997 回答
0

你为什么不根据一些规则将你的 VALUE 表分成几个表,比如 key_id 模块的某个 2 的幂(例如 256)?

于 2013-10-29T04:49:52.057 回答