更快的删除匹配行方法?

61

当涉及到数据库时,我是一个相对新手。我们正在使用MySQL,我目前正在尝试加速一条似乎需要很长时间运行的SQL语句。我在SO上搜索了类似的问题,但没有找到。

目标是从表A中删除所有具有与表B匹配的ID的行。

我目前正在执行以下操作:

DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE b.id = a.id);
表a中大约有10万行,表b中大约有2.2万行。列'id'是两个表的主键。
在我的测试环境——Pentium D, XP SP3, 2GB内存, MySQL 5.0.67上,这个语句大约需要3分钟的运行时间。这似乎对我来说太慢了。也许不是,但我希望能加快速度。是否有更好/更快的方法来完成这个任务?

编辑:

以下是一些可能有帮助的额外信息。表A和表B具有相同的结构,因为我已经按照以下方式创建了表B:

CREATE TABLE b LIKE a;

Table a(以及表b)有一些索引,可以帮助加快对其进行的查询。再次说明,我在数据库方面是一个相对初学者,仍在学习中。我不知道这对事情有多大影响(如果有的话)。我认为它会产生影响,因为索引也需要清理,对吧?我还想知道是否有其他数据库设置可能会影响速度。

另外,我正在使用INNO DB。


这里有一些可能对您有帮助的附加信息。

表A的结构类似于以下内容(我稍微进行了清理):

DROP TABLE IF EXISTS `frobozz`.`a`;
CREATE TABLE  `frobozz`.`a` (
  `id` bigint(20) unsigned NOT NULL auto_increment,
  `fk_g` varchar(30) NOT NULL,
  `h` int(10) unsigned default NULL,
  `i` longtext,
  `j` bigint(20) NOT NULL,
  `k` bigint(20) default NULL,
  `l` varchar(45) NOT NULL,
  `m` int(10) unsigned default NULL,
  `n` varchar(20) default NULL,
  `o` bigint(20) NOT NULL,
  `p` tinyint(1) NOT NULL,
  PRIMARY KEY  USING BTREE (`id`),
  KEY `idx_l` (`l`),
  KEY `idx_h` USING BTREE (`h`),
  KEY `idx_m` USING BTREE (`m`),
  KEY `idx_fk_g` USING BTREE (`fk_g`),
  KEY `fk_g_frobozz` (`id`,`fk_g`),
  CONSTRAINT `fk_g_frobozz` FOREIGN KEY (`fk_g`) REFERENCES `frotz` (`g`)
) ENGINE=InnoDB AUTO_INCREMENT=179369 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

我怀疑问题的一部分原因是这个表有很多索引。 表B看起来与表A相似,但只包含列idh
此外,性能分析结果如下:
starting 0.000018
checking query cache for query 0.000044
checking permissions 0.000005
Opening tables 0.000009
init 0.000019
optimizing 0.000004
executing 0.000043
end 0.000005
end 0.000002
query end 0.000003
freeing items 0.000007
logging slow query 0.000002
cleaning up 0.000002

解决

感谢所有的回复和评论,它们确实让我思考了这个问题。向dotjoe致敬,他通过简单地问一个问题“是否有其他表引用了a.id?”让我远离了这个问题。

问题在于表A上有一个DELETE触发器,它调用了一个存储过程来更新另外两个表C和D。表C在存储过程中与a.id有一个FK关联,在对与该id相关的一些操作完成后,它有一个语句:

DELETE FROM c WHERE c.id = theId;

我查看了EXPLAIN语句,并将其重写为:

EXPLAIN SELECT * FROM c WHERE c.other_id = 12345;

所以,我可以看到这个程序在做什么,并给了我以下信息:

id            1
select_type   SIMPLE
table         c
type          ALL
possible_keys NULL
key           NULL
key_len       NULL
ref           NULL
rows          2633
Extra         using where

这告诉我制作它是一个痛苦的操作,因为它将被调用22500次(对于要删除的给定数据集),这就是问题所在。一旦我在那个other_id列上创建了一个索引并重新运行了EXPLAIN,我得到了:

id            1
select_type   SIMPLE
table         c
type          ref
possible_keys Index_1
key           Index_1
key_len       8
ref           const
rows          1
Extra         

实际上,情况好多了,非常棒。

我添加了Index_1,我的删除时间与mattkemp报告的时间一致。这是我在最后一分钟强行加入一些额外功能时犯下的一个非常微妙的错误。正如Daniel所说,大部分建议的替代DELETE/SELECT语句实际上花费的时间基本相同,正如soulmerge所提到的,该语句基本上是我能够根据需要构建的最佳语句。一旦我为这个其他表C提供了索引,我的DELETE就很快了。

事后分析:
从这次练习中得出了两个教训。首先,很明显我没有利用EXPLAIN语句的威力来更好地了解我的SQL查询的影响。那是一个新手的错误,所以我不会因此而自责。我会从这个错误中学习。其次,有问题的代码是“快速完成”的结果,不充分的设计/测试导致这个问题没有更早地出现。如果我生成了几个相当大的测试数据集作为这个新功能的测试输入,我就不会浪费我的时间和你的时间。我的DB端测试缺乏我应用端已经具备的深度。现在我有机会改进它。

参考: EXPLAIN语句


12
感谢您礼貌回答并提供有用的后续数据。+1 - barfoon
1
以下查询需要多长时间:SELECT b.id FROM b JOIN a ON b.id = a.id。 - mbeckish
1
这是来自mbeckish的一个好问题。请执行"SET profiling = 1;",运行"SELECT b.id FROM b JOIN a ON b.id = a.id",然后执行"SHOW PROFILE;"。您可以在您的问题中发布输出... - Stefan Gehrig
使用我填充好的表(请参见我的答案),"SELECT count(b.id) FROM b JOIN a ON b.id = a.id;" 只需要 0.07 秒。 - mattkemp
3
你运行删除操作时,我假设你是唯一连接的人。是否有其他表引用了a.id字段? - dotjoe
对于问题的清晰解释和跟进,点个赞。 - nickf
14个回答

84

从InnoDB中删除数据是您可以要求的最昂贵的操作。就像您已经发现的那样,查询本身并不是问题——大多数查询都会被优化为相同的执行计划。

虽然很难理解为什么所有情况下的DELETE都是最慢的,但有一个非常简单的解释。InnoDB是一个事务性存储引擎。这意味着,如果您的查询在一半时中止,所有记录仍将保持原样,就好像什么也没有发生过一样。一旦完成,所有记录都将在同一时刻消失。在DELETE期间,连接到服务器的其他客户端将看到记录,直到DELETE完成。

为了实现这一点,InnoDB使用一种称为MVCC(多版本并发控制)的技术。它基本上为每个连接提供了整个数据库的快照视图,就像事务的第一条语句启动时一样。为了实现这一点,在InnoDB内部,每个记录可以有多个值——每个快照对应一个值。这也是为什么在InnoDB上进行COUNT需要一些时间——它取决于您在那个时间看到的快照状态。

对于您的DELETE事务,根据查询条件确定的每条记录都会被标记为删除。由于其他客户端可能同时访问数据,因此它无法立即将它们从表中删除,因为它们必须看到各自的快照以保证删除的原子性。

一旦所有记录都被标记为删除,事务就成功提交了。即使如此,它们也不能立即从实际的数据页面中删除,在所有使用DELETE事务之前的快照值处理过的其他事务结束之前,它们仍需保留。

因此,实际上您的3分钟并不是真正的慢,考虑到所有记录都必须被修改以便以安全方式准备它们进行删除的事务。在语句运行时,您可能会听到硬盘的声音。这是因为需要访问所有行。

为了提高性能,您可以尝试增加服务器的InnoDB缓冲池大小,并尽量限制对数据库的其他访问,从而减少InnoDB必须维护每个记录的历史版本数。通过额外的内存,InnoDB可能能够将您的表(大部分)读入内存,并避免一些磁盘搜索时间。


非常有用的信息,感谢您抽出时间写得如此简明扼要。 - Jas Panesar
1
你确定要增加InnoDB缓冲池大小吗?MySQL文档建议通过增加key_buffer_size系统变量的大小来增加关键字缓存的大小。https://dev.mysql.com/doc/refman/5.0/en/delete-speed.html - user2602807
1
如果您阅读该参数的文档,您会注意到它仅适用于MyISAM表。 - Daniel Schneller

10

试试这个:

DELETE a
FROM a
INNER JOIN b
 on a.id = b.id

使用子查询通常比连接更慢,因为它们会针对外部查询中的每个记录运行。


谢谢回复,Chris。不过这似乎并没有加速。它仍然需要3分钟左右的时间。 - itsmatt
我本以为这个答案比其他答案更好,但是MySQL一定会将它们全部优化成相同的语句。虽然我很难相信它“应该”需要那么长时间。我不太了解MySQL中的索引。将一个列设置为PK是否也会将其索引化?如果没有,请在两个表的“id”列上创建一个显式索引。 - Ross
@Ross 主键在MySQL中本质上与唯一索引相同,只是它是一个特殊的索引。 - mattkemp

10

当我需要操作超大量的数据时(在这里是一个包含150000行的样本测试表格),我通常会采取以下操作:

drop table if exists employees_bak;
create table employees_bak like employees;
insert into employees_bak 
    select * from employees
    where emp_no > 100000;

rename table employees to employees_todelete;
rename table employees_bak to employees;
drop table employees_todelete;
在这种情况下,SQL将50000行数据筛选到备份表中。 级联查询在我的慢速机器上只需5秒钟即可完成。 您可以通过自己的筛选查询来替换“插入到选择”查询。 这是在大型数据库上执行大量删除的技巧!;=)

有趣的想法,汤姆。感谢您发布它。 - itsmatt
由于您没有提到事务,我假设您没有使用任何事务。这里的警告是,如果在您插入并重命名表之间有东西要插入到员工中,就可能会出现竞争条件。结果可能会失去一些员工。也许,在您的情况下,您可以防止新员工被插入,但作为一般解决方案,这是需要小心的事情。 此外,您可以批量重命名表操作以使它们成为原子操作。例如,RENAME a to c,b to a,c to b将交换表a和b。 - Artem Russakovskii
2
通常在执行此类操作之前,我会锁定表格。我知道事务的存在并仍在使用它们。但是有一些原因(我不会在这里考虑),导致我选择使用我的解决方案而非事务。 - Tom Schaefer
这是一个很好的答案。只需创建一个新表,插入您想要保留的记录,然后删除原始表,将临时表重命名为原始表即可。完成。 - Siwei
这是该线程上最好的答案。只需添加锁定表语句即可完整! - Gustavo Emmel

9

你的三分钟时间看起来很慢。我猜测id列没有被正确地索引。如果您可以提供您使用的确切表定义,那将非常有帮助。

我创建了一个简单的Python脚本来生成测试数据,并运行了多个不同版本的删除查询来处理相同的数据集。以下是我的表定义:

drop table if exists a;
create table a
 (id bigint unsigned  not null primary key,
  data varchar(255) not null) engine=InnoDB;

drop table if exists b;
create table b like a;

接下来,我向a表中插入了10万行数据,并向b表中插入了2.5万行数据(其中有2.25万行也在a表中)。以下是不同删除命令的结果。顺便说一下,在每次运行之间,我都会清空并重新填充表格。

mysql> DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE a.id=b.id);
Query OK, 22500 rows affected (1.14 sec)

mysql> DELETE FROM a USING a LEFT JOIN b ON a.id=b.id WHERE b.id IS NOT NULL;
Query OK, 22500 rows affected (0.81 sec)

mysql> DELETE a FROM a INNER JOIN b on a.id=b.id;
Query OK, 22500 rows affected (0.97 sec)

mysql> DELETE QUICK a.* FROM a,b WHERE a.id=b.id;
Query OK, 22500 rows affected (0.81 sec)

所有测试均在一台Intel Core2四核2.5GHz,2GB RAM的计算机上运行,使用Ubuntu 8.10和MySQL 5.0。请注意,一个SQL语句的执行仍然是单线程的。
更新:
我更新了我的测试,使用了itsmatt的模式。我稍微修改了它,去掉了自动递增(我正在生成合成数据)和字符集编码(无法工作-没有深入研究)。这是我的新表定义:
drop table if exists a;
drop table if exists b;
drop table if exists c;

create table c (id varchar(30) not null primary key) engine=InnoDB;

create table a (
  id bigint(20) unsigned not null primary key,
  c_id varchar(30) not null,
  h int(10) unsigned default null,
  i longtext,
  j bigint(20) not null,
  k bigint(20) default null,
  l varchar(45) not null,
  m int(10) unsigned default null,
  n varchar(20) default null,
  o bigint(20) not null,
  p tinyint(1) not null,
  key l_idx (l),
  key h_idx (h),
  key m_idx (m),
  key c_id_idx (id, c_id),
  key c_id_fk (c_id),
  constraint c_id_fk foreign key (c_id) references c(id)
) engine=InnoDB row_format=dynamic;

create table b like a;

我随后使用a表中有10万行和b表中有2.5万行(每次测试之间重新填充)重新运行了相同的测试。

mysql> DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE a.id=b.id);
Query OK, 22500 rows affected (11.90 sec)

mysql> DELETE FROM a USING a LEFT JOIN b ON a.id=b.id WHERE b.id IS NOT NULL;
Query OK, 22500 rows affected (11.48 sec)

mysql> DELETE a FROM a INNER JOIN b on a.id=b.id;
Query OK, 22500 rows affected (12.21 sec)

mysql> DELETE QUICK a.* FROM a,b WHERE a.id=b.id;
Query OK, 22500 rows affected (12.33 sec)

正如您所看到的,这比以前慢了很多,可能是由于多个索引造成的。但是,它远远没有达到三分钟的标准。

您可能还想考虑将longtext字段移动到模式的末尾。我记得mySQL表现更好,如果所有大小受限制的字段都在前面,而文本、blob等在末尾。


1
作者已经说过ID是主键。它默认被索引 - 在InnoDB中它是一个聚集索引。 - Daniel Schneller

3
您正在对“a”中的每一行执行关于“b”的子查询。
尝试使用以下方法:
DELETE FROM a USING a LEFT JOIN b ON a.id = b.id WHERE b.id IS NOT NULL;

感谢您的回复,Evert。然而,这似乎并没有加速它。而且它也需要大约3分钟才能完成。 - itsmatt
你使用的是哪种存储引擎? - Evert

3

谢谢,Webrsk。我会尝试一下的。感谢提供链接。 - itsmatt
只是一个跟进。我也尝试了这个,执行时间基本相同 - 2分钟55秒。因此,对于测试集来说,这大约比之前快了5秒钟。 - itsmatt
2
DELETE QUICK 只在 MyISAM 表中有用,正如手册所建议的那样。此外,它可能会导致浪费索引,除非您优化表格,因此我建议避免使用它,除非您确切知道自己在做什么。 - Artem Russakovskii

3

我知道由于OP的索引遗漏,这个问题已经被解决了,但我想提供以下额外的建议,对于这个问题的更一般的情况也是有效的。

在我的经验中,我亲自处理了从一个表中删除许多存在于另一个表中的行的情况,如果您希望删除大量行,则最好采用以下方法,特别是这种技术将改善复制从库滞后,因为每个单独的变异器查询运行的时间越长,滞后就越严重(复制是单线程的)。

所以,这里是:首先进行SELECT,作为单独的查询,在脚本/应用程序中记住返回的ID,然后继续分批删除(例如,每次50000行)。

  • 每个删除语句都不会锁定表太长时间,因此不会让复制滞后失控。如果您依赖于复制来提供相对最新的数据,则这尤其重要。使用批处理的好处在于,如果发现每个DELETE查询仍然需要太长时间,可以将其调整为较小的查询而不会触及任何DB结构。
  • 使用单独的SELECT的另一个好处是,SELECT本身可能需要很长时间才能运行,特别是如果由于某种原因无法使用最佳的DB索引。如果SELECT是DELETE的内部查询,当整个语句迁移到从库时,它将不得不再次执行SELECT,潜在地使从库滞后,因为它必须再次执行长时间的SELECT。从库滞后再次严重。如果使用单独的SELECT查询,则此问题将消失,因为您传递的只是ID列表。

如果我的逻辑有误,请告诉我。

有关复制滞后及其应对方法的更多讨论,类似于这个问题,请参见MySQL Slave Lag (Delay) Explained And 7 Ways To Battle It

P.S. 当然要注意的一件事是,在SELECT完成和DELETE开始之间进行表的潜在编辑。我将让您使用与您的应用程序相关的事务和/或逻辑来处理此类细节。


2
DELETE FROM a WHERE id IN (SELECT id FROM b)

感谢您的回复,chaos。然而,这似乎并没有加快速度。它仍然需要3分钟左右的时间。所有建议都需要相同的时间,这非常奇怪。嗯... - itsmatt
我唯一的建议是,在查询之前将键放在桌子上,然后重新添加它们。 - chaos
嗯... 这很有趣。我得试试看。我会告诉你的。 - itsmatt
1
这是一个可怕的建议,因为任何ALTER TABLE语句(例如删除键)都会从头开始重建表格(如果您进行两次ALTER,则需要重建两次)。如果您的表格大小相当大,这个查询将需要很长时间。此外,它将锁定对表格的任何写入,直到ALTER TABLE完成。 - Artem Russakovskii

2
也许在运行如此庞大的查询之前,您应该重建索引。嗯,您应该定期重建它们。
REPAIR TABLE a QUICK;
REPAIR TABLE b QUICK;

然后运行上述任何一个查询(即)。
DELETE FROM a WHERE id IN (SELECT id FROM b)

当我尝试对任何一个表进行修复时,我会收到“该表的存储引擎不支持修复”作为Msg_text。我猜这在Inno DB中不受支持。不过我需要再确认一下。 - itsmatt
您可以尝试执行 "ANALYZE TABLE a" 和/或 "OPTIMIZE TABLE a" 命令。这样可以更新键分布。 - Scoregraphic

2
查询本身已经处于最佳状态,更新索引会导致整个操作需要那么长时间。在操作之前,您可以 禁用该表上的键,这样应该可以加快速度。如果您不需要立即使用它们,稍后可以将其重新启用。
另一种方法是向您的表添加一个“删除”标志列,并调整其他查询以考虑该值。Mysql中最快的布尔类型是CHAR(0)NULL(true ='',false = NULL)。这将是一个快速的操作,您可以随后删除这些值。
以下是相同思路的SQL语句表达:
ALTER TABLE a ADD COLUMN deleted CHAR(0) NULL DEFAULT NULL;

-- The following query should be faster than the delete statement:
UPDATE a INNER JOIN b SET a.deleted = '';

-- This is the catch, you need to alter the rest
-- of your queries to take the new column into account:
SELECT * FROM a WHERE deleted IS NULL;

-- You can then issue the following queries in a cronjob
-- to clean up the tables:
DELETE FROM a WHERE deleted IS NOT NULL;

如果这也不是你想要的,你可以查看mysql文档中关于删除语句速度的内容。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接