【问题标题】:Faster way to delete matching rows?删除匹配行的更快方法?
【发布时间】:2010-10-23 04:30:19
【问题描述】:

在数据库方面,我是一个相对新手。我们正在使用 MySQL,我目前正在尝试加速似乎需要一段时间才能运行的 SQL 语句。我在 SO 上四处寻找类似的问题,但没有找到。

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

我目前正在做以下事情:

DELETE FROM a WHERE EXISTS (SELECT b.id FROM b WHERE b.id = a.id);

表 a 中大约有 100K 行,表 b 中大约有 22K 行。 'id' 列是两个表的 PK。

这个语句在我的测试机器上运行大约需要 3 分钟 - Pentium D、XP SP3、2GB ram、MySQL 5.0.67。这对我来说似乎很慢。也许不是,但我希望加快速度。有没有更好/更快的方法来实现这一点?


编辑:

一些可能有用的附加信息。表 A 和表 B 的结构与我创建表 B 的操作相同:

CREATE TABLE b LIKE a;

表 a(以及表 b)有一些索引来帮助加快对其进行的查询。同样,我是 DB 工作的相对新手,仍在学习。我不知道这对事物有多大影响,如果有的话。我认为它确实有效果,因为索引也必须清理,对吧?我还想知道是否有任何其他可能影响速度的数据库设置。

另外,我正在使用 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 看起来与表 B 相似,但它只包含 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

已解决

感谢所有回复和 cmets。他们当然让我思考这个问题。感谢 dotjoe 通过提出简单的问题“是否有其他表引用 a.id?”让我摆脱了这个问题

问题是表 A 上有一个 DELETE TRIGGER,它调用存储过程来更新另外两个表 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 列上创建了一个 INDEX 并重新运行 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 和我的删除时间与 ma​​ttkemp 报告的时间一致。这对我来说是一个非常微妙的错误,因为在最后一分钟增加了一些额外的功能。事实证明,正如 Daniel 所说,大多数建议的替代 DELETE/SELECT 语句最终花费的时间基本相同,并且正如 soulmerge 所提到的,该语句很漂亮根据我需要做的事情,我将能够构建的最好的东西。一旦我为另一个表 C 提供了索引,我的 DELETE 速度很快。

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

Reference: EXPLAIN Statement

【问题讨论】:

  • +1 礼貌的回答和良好的跟进数据
  • 以下查询需要多长时间:SELECT b.id FROM b JOIN a ON b.id = a.id
  • 这是来自 mbeckish 的一个好问题。请同时执行“SET profiling = 1;”,运行“SELECT b.id FROM b JOIN a ON b.id = a.id”,然后执行“SHOW PROFILE;”。您可以在问题中发布输出...
  • 我的表已满(见我的回答)“SELECT count(b.id) FROM b JOIN a ON b.id = a.id;”耗时 0.07 秒。
  • 我假设您是运行删除时唯一连接的人?是否有任何其他表引用 a.id?

标签: mysql performance sql-delete sql-execution-plan


【解决方案1】:

从 InnoDB 中删除数据是您可以请求的最昂贵的操作。正如您已经发现的那样,查询本身不是问题 - 无论如何,它们中的大多数都将针对相同的执行计划进行优化。

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

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

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

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

事实上,考虑到所有记录都必须进行修改以便准备好以交易安全的方式将其删除,因此实际上您的 3 分钟并没有那么慢。语句运行时,您可能会“听到”您的硬盘在工作。这是由访问所有行引起的。 为了提高性能,您可以尝试增加服务器的 InnoDB 缓冲池大小,并尝试在 DELETE 时限制对数据库的其他访问,从而减少 InnoDB 必须为每条记录维护的历史版本数量。 借助额外的内存,InnoDB 可能能够(大部分)将您的表读入内存并避免一些磁盘查找时间。

【讨论】:

  • 内容丰富,感谢您抽出宝贵时间写得如此简洁。
  • 您确定要增加 InnoDB 缓冲池大小吗? MySQL DOC 说:通过增加 key_buffer_size 系统变量来增加 key cache 的大小。 dev.mysql.com/doc/refman/5.0/en/delete-speed.html
  • 如果您阅读该参数的文档,您会发现它仅适用于 MyISAM 表。
【解决方案2】:

试试这个:

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

使用子查询往往比连接慢,因为它们是针对外部查询中的每条记录运行的。

【讨论】:

  • 感谢您的回复,克里斯。不过,这似乎并没有加快速度。也花了 3 分钟。
  • 我认为这个答案是对其他答案的改进,但 MySQL 必须将它们全部优化到同一个语句中。我发现很难相信它“应该”需要那么长时间。我对 MySQL 中的索引不太了解。将一列设为 PK 是否也将其编入索引?如果没有,请在两个表的“id”列上创建显式索引。
  • @Ross 主键与 MySQL 中的唯一索引基本相同,只是它是一个特殊的索引。
【解决方案3】:

你的三分钟时间似乎很慢。我的猜测是 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;

然后我将 100k 行插入到 a 中,将 25k 行插入到 b 中(其中 22.5k 也在 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)

所有测试均在配备 Ubuntu 8.10 和 MySQL 5.0 的 Intel Core2 四核 2.5GHz、2GB RAM 上运行。注意,一条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 中使用 100k 行,在 b 中使用 25k 行(并在运行之间重新填充)。

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)

正如您所见,这比以前慢了很多,可能是由于多个索引。但是,距离三分钟大关还差得很远。

您可能想要查看的其他内容是将长文本字段移动到架构的末尾。我似乎记得,如果所有大小受限的字段都在前,而 text、blob 等在最后,那么 mySQL 的性能会更好。

【讨论】:

  • 作者已经说过ID是主键。这是默认索引的 - 在 InnoDB 中它是一个聚集索引。
【解决方案4】:

当我必须处理超大数据时,这就是我经常做的事情(这里:一个有 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 秒内在我的慢速机器上执行。 您可以通过自己的过滤查询替换插入到 select 中。

这就是对大型数据库执行批量删除的技巧!;=)

【讨论】:

  • 有趣的想法,汤姆。感谢您发布它。
  • 既然你没有提到交易,我假设你没有使用任何交易。这里需要注意的是,如果在您插入和重命名表之间有东西要插入员工中,则可能存在竞争条件。因此,您可能会失去一些员工。也许,在您的情况下,您会阻止插入新员工,但作为一般解决方案,这是需要小心的。此外,您可以批量重命名表操作以使其成为原子操作。例如,将 a 重命名为 c、b 重命名为 a、c 重命名为 b 将交换表 a 和 b。
  • 通常我会在执行此类操作之前锁定表。我知道交易,我仍在使用它们。但是有一些我不会在这里考虑的原因来执行我的解决方案而不是交易。
  • 这是一个很好的答案。只需创建一个新表,插入要保留的记录,然后删除原始表,将临时表重命名为原始表。完成。
  • 这是该主题的最佳答案。只需添加锁表语句即可完成!
【解决方案5】:

您正在为“a”中的每一行在“b”上执行子查询。

试试:

DELETE FROM a USING a LEFT JOIN b ON a.id = b.id WHERE b.id IS NOT NULL;

【讨论】:

  • 感谢您的回复,埃弗特。不过,这似乎并没有加快速度。它也需要 3 分钟才能完成。
  • 你用的是什么存储引擎?
【解决方案6】:

试试这个:

DELETE QUICK A.* FROM A,B WHERE A.ID=B.ID

它比普通查询快得多。

语法参考:http://dev.mysql.com/doc/refman/5.0/en/delete.html

【讨论】:

  • 谢谢,韦伯斯克。我会试试看。感谢链接。
  • 只是一个后续。我也试过这个,执行时间基本上是一样的——2分55秒。所以这对于测试集来说快了大约 5 秒。
  • DELETE QUICK 仅在 MyISAM 表中有用,正如手册所建议的那样。此外,除非您 OPTIMIZE TABLE,否则它可能会导致索引浪费,所以我会避免使用它,除非您确切知道自己在做什么。
【解决方案7】:

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

我亲自处理过必须从一个表中删除另一个表中存在的许多行的问题,根据我的经验,最好执行以下操作,特别是如果您希望删除很多行。最重要的是,这种技术将改善复制从属延迟,因为每个单个 mutator 查询运行的时间越长,延迟就会越差(复制是单线程的)。

所以,这里是:首先做一个 SELECT,作为一个单独的查询,记住在您的脚本/应用程序中返回的 ID,然后继续批量删除(例如,一次 50,000 行)。 这将实现以下目标:

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

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

有关复制延迟的更多讨论以及解决方法,类似于此,请参阅MySQL Slave Lag (Delay) Explained And 7 Ways To Battle It

附:当然,要注意的一件事是,在 SELECT 完成和 DELETE 开始之间可能对表进行编辑。我会让你通过使用与你的应用程序相关的事务和/或逻辑来处理这些细节。

【讨论】:

    【解决方案8】:
    DELETE FROM a WHERE id IN (SELECT id FROM b)
    

    【讨论】:

    • 感谢您的回复,混乱。不过,这似乎并没有加快速度。它也花了3分钟。很奇怪,所有的建议也在同一时间。嗯……
    • 那么,我唯一的建议是在您进行查询之前将键放在表上,然后重新添加它们。
    • 嗯...这很有趣。我得试试。我会告诉你的。
    • 这是一个可怕的建议,因为任何 ALTER TABLE 语句,比如删除键,都会从头开始重建表(如果你 ALTER 两次,那就是重建两次)。如果您的表大小合适,则此查询将花费很长时间。此外,它将锁定对表的任何写入,直到 ALTER TABLE 完成。
    【解决方案9】:

    也许您应该在运行如此庞大的查询之前重建索引。好吧,你应该定期重建它们。

    REPAIR TABLE a QUICK;
    REPAIR TABLE b QUICK;
    

    然后运行上述任何查询(即)

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

    【讨论】:

    • 当我尝试对任一表执行此操作时,我得到“表的存储引擎不支持修复”作为 Msg_text。我想这不支持 Inno DB。我会不过必须检查一下。
    • 您可以尝试“ANALYZE TABLE a”和/或“OPTIMIZE TABLE a”。这应该会更新密钥分配。
    【解决方案10】:

    查询本身已经处于最佳状态,更新索引会导致整个操作花费这么长时间。您可以在操作之前在该表上disable the keys ,这应该会加快速度。如果您不立即需要它们,您可以稍后重新打开它们。

    另一种方法是在您的表中添加 deleted 标志列并调整其他查询,以便它们考虑该值。 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 文档对 speed of delete statements 的看法。

    【讨论】:

      【解决方案11】:

      顺便说一句,在我的博客上发布上述内容后,来自 Percona 的 Baron Schwartz 让我注意到他的 maatkit 已经有一个专门用于此目的的工具 - mk-archiver。 http://www.maatkit.org/doc/mk-archiver.html.

      这很可能是您完成这项工作的最佳工具。

      【讨论】:

        【解决方案12】:

        显然,构建DELETE 操作基础的SELECT 查询速度非常快,因此我认为外键约束或索引是查询速度极慢的原因。

        试试

        SET foreign_key_checks = 0;
        /* ... your query ... */
        SET foreign_key_checks = 1;
        

        这将禁用对外键的检查。不幸的是,您无法使用 InnoDB 表禁用(至少我不知道如何)密钥更新。使用 MyISAM 表,您可以执行类似的操作

        ALTER TABLE a DISABLE KEYS
        /* ... your query ... */
        ALTER TABLE a ENABLE KEYS 
        

        我实际上并没有测试这些设置是否会影响查询持续时间。但值得一试。

        【讨论】:

        • 禁用外键检查几乎总是不好的建议。它们的存在是有原因的,并且为了性能而禁用它们 a) 很少会产生明显的速度提升,更重要的是 b) 允许任何人在“防护罩已关闭”时对您的数据完整性造成严重破坏。
        • 当恢复备份时,您知道例如一致,禁用密钥检查不会造成任何伤害(有时是恢复备份的唯一方法)。在实时系统上这样做确实会造成严重破坏。如果它产生速度改进,必须通过运行基准检查。
        【解决方案13】:

        使用终端连接数据库并执行下面的命令,查看每个结果时间,你会发现删除10、100、1000、10000、100000条记录的次数没有相乘。

          DELETE FROM #{$table_name} WHERE id < 10;
          DELETE FROM #{$table_name} WHERE id < 100;
          DELETE FROM #{$table_name} WHERE id < 1000;
          DELETE FROM #{$table_name} WHERE id < 10000;
          DELETE FROM #{$table_name} WHERE id < 100000;
        

        删除1万条记录的时间不是删除10万条记录的10倍。 那么,除了想办法更快地删除记录外,还有一些间接的方法。

        1、我们可以将table_name重命名为table_name_bak,然后从table_name_bak中选择记录为table_name。

        2、要删除10000条记录,我们可以删除1000条记录10次。有一个示例 ruby​​ 脚本可以做到这一点。

        #!/usr/bin/env ruby
        require 'mysql2'
        
        
        $client = Mysql2::Client.new(
          :as => :array,
          :host => '10.0.0.250',
          :username => 'mysql',
          :password => '123456',
          :database => 'test'
        )
        
        
        $ids = (1..1000000).to_a
        $table_name = "test"
        
        until $ids.empty?
          ids = $ids.shift(1000).join(", ")
          puts "delete =================="
          $client.query("
                        DELETE FROM #{$table_name}
                        WHERE id IN ( #{ids} )
                        ")
        end
        

        【讨论】:

          【解决方案14】:

          MySQL通过id字段删除单表多Row表单的基本技巧

          DELETE FROM tbl_name WHERE id <= 100 AND id >=200; 该查询负责从某个表中删除 100 AND 200 之间的匹配条件

          【讨论】:

          • 这个答案说查询是删除记录between 100 AND 200,但查询是删除记录id &lt;= 100 AND id &gt;=200在100和200之外。这个答案是错误的。
          猜你喜欢
          • 2018-06-11
          • 1970-01-01
          • 1970-01-01
          • 2015-09-06
          • 2022-08-24
          • 1970-01-01
          • 2010-11-12
          • 2015-10-30
          • 2019-10-24
          相关资源
          最近更新 更多