如何优化删除查询以在不创建索引的情况下删除重复项?
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_len
和 ref
表明它能够为两列使用索引。
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 事务。
我是 运行 从 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_len
和 ref
表明它能够为两列使用索引。
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 DELETESELECT
使用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 事务。