如何优化删除查询以在不创建索引的情况下删除重复项?

how do I optimize a delete query to remove duplicates without creating an index?

我是 运行 从 table 中删除重复项(用户定义的)的查询,该查询具有约 3M 条记录。查询是:

DELETE t1
FROM       'path_alias_revision' t1
INNER JOIN 'path_alias_revision' t2
WHERE t1.id < t2.id AND  t1.path=t2.path AND binary(t1.alias) = binary(t2.alias)

show create table的输出:

---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Table               | Create Table                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
+---------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| path_alias_revision | CREATE TABLE `path_alias_revision` (
  `id` int(10) unsigned NOT NULL,
  `revision_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `langcode` varchar(12) CHARACTER SET ascii NOT NULL,
  `path` varchar(255) DEFAULT NULL,
  `alias` varchar(255) DEFAULT NULL,
  `status` tinyint(4) NOT NULL,
  `revision_default` tinyint(4) DEFAULT NULL,
  PRIMARY KEY (`revision_id`),
  KEY `path_alias__id` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=80213807 DEFAULT CHARSET=utf8mb4 COMMENT='The revision table for path_alias entities.' |
+---------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

解释输出:

explain DELETE t1 FROM  path_alias_revision t1 INNER JOIN path_alias_revision t2 WHERE t1.id < t2.id AND  t1.path=t2.path AND binary(t1.alias) = binary(t2.alias);
+----+-------------+-------+------------+------+----------------+------+---------+------+---------+----------+------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys  | key  | key_len | ref  | rows    | filtered | Extra                                          |
+----+-------------+-------+------------+------+----------------+------+---------+------+---------+----------+------------------------------------------------+
|  1 | DELETE      | t1    | NULL       | ALL  | path_alias__id | NULL | NULL    | NULL | 3105455 |   100.00 | NULL                                           |
|  1 | SIMPLE      | t2    | NULL       | ALL  | path_alias__id | NULL | NULL    | NULL | 3105455 |     3.33 | Range checked for each record (index map: 0x2) |
+----+-------------+-------+------------+------+----------------+------+---------+------+---------+----------+------------------------------------------------+

我无法判断查询是挂起还是只是花费了很长时间。 show processlist 的输出是:


MySQL [acquia]> show processlist \G;
*************************** 1. row ***************************
     Id: 11
   User: acquia
   Host: 172.18.0.3:37498
     db: acquia
Command: Query
   Time: 602
  State: Sending data
   Info: DELETE t1
FROM       path_alias_revision t1
INNER JOIN path_alias_revision t2
WHERE      t1.id < t2.
*************************** 2. row ***************************
     Id: 15
   User: acquia
   Host: 172.18.0.3:37512
     db: acquia
Command: Query
   Time: 0
  State: starting
   Info: show processlist
2 rows in set (0.000 sec)

ERROR: No query specified

我可以做些什么来改进这个查询?我知道我可以将我想要保留的数据移动到临时 table 并重命名它,但我想了解这里发生了什么。 我已将一些 mysql 属性升级为:

max_allowed_packet = 128M
innodb_buffer_pool_chunk_size = 128M
innodb_buffer_pool_size = 8G

但这并没有帮助。

更新:问题是在我写完答案后编辑的。 OP 添加了一个条件,即他们不想创建索引。但这是优化 DELETE 查询的解决方案。我将在下面留下我的原始答案。


您缺少索引,而且如果您想优化 alias 列的二进制比较,那么您应该更改其排序规则,以便索引基于二进制字节。

mysql> alter table path_alias_revision 
  modify column alias varchar(255) collate utf8mb4_bin, 
  add index (path, alias);

现在您可以看到 EXPLAIN 中的改进。当然,查询仍然需要为 t1 执行 table-scan,但它可以使用索引来查找匹配的行。 ken_lenref 表明它能够为两列使用索引。

explain DELETE t1 FROM path_alias_revision t1 
INNER JOIN path_alias_revision t2 
WHERE t1.id < t2.id AND  t1.path=t2.path AND t1.alias = t2.alias

*************************** 1. row ***************************
           id: 1
  select_type: DELETE
        table: t1
   partitions: NULL
         type: ALL
possible_keys: path_alias__id,path
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 1
     filtered: 100.00
        Extra: Using where
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: t2
   partitions: NULL
         type: ref
possible_keys: path_alias__id,path
          key: path
      key_len: 1791
          ref: test2.t1.path,test2.t1.alias
         rows: 1
     filtered: 100.00
        Extra: Using where

在我的测试中,它显示 rows: 1 因为我没有创建测试数据。

您没有提到要删除多少重复行。所以这是一个猜测。 InnoDb(存储引擎)将您的整个 DELETE 语句放入单个事务中。它建立一个事务日志并立即提交整个事情(看在 ACID 的份上)。该交易可能非常庞大,并且会使用资源(IO 和 CPU)。

避免超额交易的技巧?分块删除。

试试这个:首先获取需要删除的行的主键。

/* make a temp table with the PK values for the rows you want to delete
 * this may take a lot of time but that's OK */

CREATE TEMPORARY TABLE path_revision_alias_dups
SELECT t1.revision_id
  FROM       'path_alias_revision' t1
  INNER JOIN 'path_alias_revision' t2
  WHERE t1.id < t2.id AND  t1.path=t2.path AND binary(t1.alias) = binary(t2.alias);

现在我们有一个临时文件 table,其中包含要删除的行。让我们使用它。

您将分块进行删除(此处为 1000 行的分块)。因此,您需要一遍又一遍地重复 SQL 的下一个大块,直到没有任何内容可删除。

/* retrieve a subset -- a chunk -- of the IDs to delete, 1000 at a time */
CREATE TEMPORARY TABLE dups_to_delete_now
SELECT revision_id
  FROM path_revision_alias_dups
 LIMIT 1000;

/* delete the rows from your table */
DELETE FROM path_alias_revision
 WHERE revision_id IN (SELECT * FROM dups_to_delete_now);

/* and delete the batch from your first temp table */
DELETE FROM path_revision_alias_dups
 WHERE revision_id IN (SELECT * FROM dups_to_delete_now);

/* clean up, ready for the next chunk */
DROP TABLE dups_to_delete_now;

这是在进行 large-scale table 维护时使用的相当常见的查询模式。

我建议的第一个 CREATE TEMPORARY TABLE 可能会花费太长时间,因为您无法添加任何索引。它可能:没有索引,查询的复杂度是 O(n2)。如果确实需要太长时间,您将需要在某个地方建立索引。

最初的尝试,涉及 t1.id < t2.id,效率极低;在 million-row table 上执行 万亿 次操作。 (我希望参考手册没有包含它。)

有很多方法可以更有效地完成 DELETE。还有一种方法可以在不锁定 table 的情况下添加 INDEX(如果那是你的 真实 恐惧)。

此外,WHERE binary(alias) ... 不会使用 INDEX(alias) !

那么,怎么办??

  • 如果要删除大部分 table,请不要使用 DELETE;而是将 SELECT 用于新的 table,然后再使用 table。这几乎适用于任何将删除“大部分”table 的大删除。 (什么是“很多”?我没有什么好的数字;可能是 1/3,可能是 1/2,当然是 3/4。)

  • 不要使用t1.id < t2.id技巧;它可能是有史以来为大型 table 发明的最糟糕的产品,即使已编入索引也是如此。

  • 如果您无法摆脱 binary(alias),那么让我们开始,通过创建一个新的 table 和table.

    的主键
    CREATE TABLE helper (
        balias ... NOT NULL, -- from binary(alias)
        id ... NOT NULL,  -- whatever is the PK of your table
        PRIMARY KEY(balias)
    ) ENGINE=InnoDB;
    

现在填充它:

  INSERT INTO helper ( balias, id )
      SELECT binary(alias), id  FROM t1;

然后用那个table看看要做什么

  • DELETE(如果只是小号删除);使用 multi-table DELETE
  • SELECT 使用 LEFT JOIN 来“保留”(如果数量很大)的​​行;交换 tables.

如果您不关心要保留哪些副本,这里有另一种解决此问题的方法。

首先,让我们做一个子查询来检索您实际想要保留的行的 PK 值。

                 SELECT MAX(revision_id) revision_id
                   FROM path_alias_revision
                  GROUP BY path, binary(alias)

这将从每组重复行中仅选择一个 revision_id,即最大的一个。它会生成一个怪物排序操作,需要一段时间。但本质上排序是 O(n log(n)) 而不是 O(n2)复杂性。

然后您可以使用该子查询来 运行 您的删除。像这样。

DELETE FROM path_alias_revision 
  WHERE revision_id NOT IN (
                 SELECT MAX(revision_id) revision_id
                   FROM path_alias_revision
                  GROUP BY path, binary(alias));

因为 revision_id 是主键,所以 NOT IN() 谓词可以访问现有索引。

如果要删除的行数不多,您或许可以一次完成所有操作。如果它很大,您需要将删除分块以避免 too-large 事务。