【问题标题】:Why does DISTINCT make this query take 10x longer than without?为什么 DISTINCT 使这个查询比没有的要长 10 倍?
【发布时间】:2015-12-28 16:06:23
【问题描述】:

我有这个 mysql 查询:

SELECT DISTINCT post.postId,hash,previewUrl,lastRetrieved
FROM post INNER JOIN (tag as t1,taggedBy as tb1,tag as t2,taggedBy as tb2,tag as t3,taggedBy as tb3)
ON post.id=tb1.postId AND tb1.tagId=t1.id AND post.id=tb2.postId AND tb2.tagId=t2.id AND post.id=tb3.postId AND tb3.tagId=t3.id
WHERE ((t1.name="a" AND t2.name="b") OR t3.name="c") 
ORDER BY post.postId DESC LIMIT 0,100;

运行该查询大约需要 15 秒,而相同的查询没有 DISTINCT 只需不到一秒。


EXPLAIN 查询的输出 with DISTINCT:

+----+-------------+-------+--------+---------------------+---------+---------+--------------------------+------+-----------------------+
| id | select_type | table | type   | possible_keys       | key     | key_len | ref                      | rows | Extra                 |
+----+-------------+-------+--------+---------------------+---------+---------+--------------------------+------+-----------------------+
|  1 | SIMPLE      | post  | index  | PRIMARY             | postId  | 4       | NULL                     |    1 | Using temporary       |
|  1 | SIMPLE      | tb1   | ref    | PRIMARY,tagId       | PRIMARY | 4       | e621datamirror.post.id   |   13 | Using index; Distinct |
|  1 | SIMPLE      | t1    | eq_ref | PRIMARY,name,name_2 | PRIMARY | 4       | e621datamirror.tb1.tagId |    1 | Distinct              |
|  1 | SIMPLE      | tb2   | ref    | PRIMARY,tagId       | PRIMARY | 4       | e621datamirror.post.id   |   13 | Using index; Distinct |
|  1 | SIMPLE      | t2    | eq_ref | PRIMARY,name,name_2 | PRIMARY | 4       | e621datamirror.tb2.tagId |    1 | Distinct              |
|  1 | SIMPLE      | tb3   | ref    | PRIMARY,tagId       | PRIMARY | 4       | e621datamirror.post.id   |   13 | Using index; Distinct |
|  1 | SIMPLE      | t3    | eq_ref | PRIMARY,name,name_2 | PRIMARY | 4       | e621datamirror.tb3.tagId |    1 | Using where; Distinct |
+----+-------------+-------+--------+---------------------+---------+---------+--------------------------+------+-----------------------+
7 rows in set (0.01 sec)

EXPLAIN 查询的输出没有 DISTINCT:

+----+-------------+-------+--------+---------------------+---------+---------+--------------------------+------+-------------+
| id | select_type | table | type   | possible_keys       | key     | key_len | ref                      | rows | Extra       |
+----+-------------+-------+--------+---------------------+---------+---------+--------------------------+------+-------------+
|  1 | SIMPLE      | post  | index  | PRIMARY             | postId  | 4       | NULL                     |    1 | NULL        |
|  1 | SIMPLE      | tb1   | ref    | PRIMARY,tagId       | PRIMARY | 4       | e621datamirror.post.id   |   13 | Using index |
|  1 | SIMPLE      | t1    | eq_ref | PRIMARY,name,name_2 | PRIMARY | 4       | e621datamirror.tb1.tagId |    1 | NULL        |
|  1 | SIMPLE      | tb2   | ref    | PRIMARY,tagId       | PRIMARY | 4       | e621datamirror.post.id   |   13 | Using index |
|  1 | SIMPLE      | t2    | eq_ref | PRIMARY,name,name_2 | PRIMARY | 4       | e621datamirror.tb2.tagId |    1 | NULL        |
|  1 | SIMPLE      | tb3   | ref    | PRIMARY,tagId       | PRIMARY | 4       | e621datamirror.post.id   |   13 | Using index |
|  1 | SIMPLE      | t3    | eq_ref | PRIMARY,name,name_2 | PRIMARY | 4       | e621datamirror.tb3.tagId |    1 | Using where |
+----+-------------+-------+--------+---------------------+---------+---------+--------------------------+------+-------------+

CREATE TABLE `post` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `postId` int(11) NOT NULL,
  `hash` varchar(32) COLLATE utf8_bin NOT NULL,
  `previewUrl` varchar(512) COLLATE utf8_bin NOT NULL,
  `lastRetrieved` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `postId` (`postId`),
  UNIQUE KEY `hash` (`hash`),
  KEY `postId_2` (`postId`),
  KEY `postId_3` (`postId`)
) ENGINE=InnoDB AUTO_INCREMENT=692561 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

CREATE TABLE `tag` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) COLLATE utf8_bin NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`),
  KEY `name_2` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=157876 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

CREATE TABLE `taggedBy` (
  `postId` int(11) NOT NULL,
  `tagId` int(11) NOT NULL,
  PRIMARY KEY (`postId`,`tagId`),
  KEY `tagId` (`tagId`),
  CONSTRAINT `taggedBy_ibfk_1` FOREIGN KEY (`postId`) REFERENCES `post` (`id`) ON DELETE CASCADE,
  CONSTRAINT `taggedBy_ibfk_2` FOREIGN KEY (`tagId`) REFERENCES `tag` (`id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

是什么导致这个查询如此缓慢?如何加快速度?

我希望我已经提供了足够的信息,所以你们可以给我一些有意义的答案。如果我遗漏了一些东西,我会很乐意添加它。

【问题讨论】:

  • 因为distinct 删除了重复的行,数据库必须做更多的工作。
  • 不是全选,而是 distinct 对每一行进行全选,看看是否有其他类似的值。
  • dev.mysql.com/doc/refman/5.1/en/internal-temporary-tables.html 此查询似乎满足制作磁盘临时表的条件。您可以通过查看相关状态变量是否已更改来检查是否是这种情况。如果是这样,您希望删除创建此类表的一些条件,因为您可以想象,这样做很慢。
  • 这里的 DISTINCT 不会导致 less 工作吗?这不像 mysql 愚蠢到首先选择 all 可能的结果,then 过滤掉不同的结果,然后 then 限制为 100 个项目。我知道事实上 mysql 比这更聪明,因为省略限制会使查询花费的方式 way 更长。在任何情况下,我都不会期望减速 10 倍。
  • 尝试将DISTINCT 分解出一个额外的级别?你必须超越你的内心LIMIT,以确保你得到正确数量的结果。即SELECT DISTINCT ... FROM (SELECT ... LIMIT 1000) LIMIT 100

标签: mysql sql database performance database-performance


【解决方案1】:

即使在@SlimGhost 的合理(但已删除)答案中,也正在讨论几件事。

DISTINCT 与 GROUP BY

虽然GROUP BY有时可以用来代替DISTINCT,但不要这样做;它们适用于不同的事物。

它们都需要一些形式的额外努力。 (我稍后会谈到 10 倍。)两者都必须发现共同值——无论是在整行中(对于DISTINCT)还是对于分组项。这可以通过至少两种方式之一来完成。 (可能大多数引擎都内置了这些选项。)注意DISTINCTGROUP BY 逻辑上必须在WHERE 之后,但在ORDER BYLIMIT 之前。

  • 在生成输出时保留某种内部关联数组。如果优化器可以看到不会有“太多”可能的不同值,这是实用的。
  • 对输出进行排序;然后对输出进行重复数据删除或分组。无论大小如何,这都有效。

ORDER BY + LIMIT

请注意,查询在 4 列上执行 DISTINCTpost.postId, hash, previewUrl, lastRetrieved。这些是全部在post 中还是分散在7 个表中尚不清楚。 (请通过限定每一列来澄清。)

假设需要执行 JOIN 以找到 4 列。

假设没有DISTINCT。现在,操作是

  1. ORDER BY post.postID 顺序浏览post
  2. 对于每个这样的行,执行 JOIN 并检查 WHERE。
  3. 100 行通过 WHERE 后,停止。

但是对于DISTINCT,优化器无法做出这样一个简化的假设来阻止。而是:

  1. ORDER BY post.postID 顺序浏览post。 (从 t1/t2/t3 开始是不可能的,因为OR。)实际上,尚不清楚优化器是否会按此顺序进行。
  2. 对于每个这样的行,执行 JOIN 并检查 WHERE。
  3. DISTINCT做点什么。
  4. 100 行通过 WHERE 后,停止。注意:这可能涉及来自post 的更多行(可能是 10 倍?)

请记住,优化器不知道postId 是否与hash 1:1 等。因此,它不能做出简化假设。假设 JOIN 中有 200 行,最小的 postId,而 hash 恰好按降序排列。闻起来像是需要“排序”。

EXPLAIN FORMAT=JSON SELECT ...可能给你一些这些细节。

哎哟。您同时拥有idUNIQUE(postid)?去掉id,把postId变成PRIMARY KEY。仅此一项,可能会加快速度。

hash 的哈希是什么?

请使用JOIN ... ON ... 语法。

postId 上有 3 个索引;去掉多余的两个。

为什么要使用 DISTINCT?

现在我看到所有SELECTed 列都来自一个表,而且它们显然很容易区分,为什么还要考虑使用DISTINCT

(更新)

加入

FROM post INNER JOIN (tag as t1,taggedBy as tb1,...
                   ON post.id=tb1.postId AND tb1.tagId=t1.id AND ...
 -->
FROM post
JOIN tag       AS  t1 ON post.id = tb1.postId
JOIN taggedBy  AS tb1 ON tb2.tagId = t2.id
...  (each ON is next to the JOIN it applies to)

加速技术

SELECT p2.postId, p2.hash, p2.previewUrl, p2.lastRetrieved
    FROM (
        SELECT DISTINCT postId           -- Only the PRIMARY KEY
            FROM post
            JOIN ... etc
            WHERE ... ...
            ORDER BY postId
            LIMIT 100
         ) x
    JOIN post AS p2  ON x.postId = p2.id   -- self join for getting rest of fields
    ORDER BY x.postId   -- assuming you need the ordering

这会将DISTINCT 放在内部查询中,在这里您只获取一列(postId)。 (我不确定这种技术是否对你的有很大帮助。)

【讨论】:

  • 这 4 列都在 post 表上,我确实发布了数据库模式。 postId 与 id 分开是有原因的。我无法完全控制 postId,这使其成为外键的错误选择。 postId 可能会改变。散列通常是 jpeg 文件的散列。我正在使用 DISTINCT,因为不使用它会返回很多重复的行。如果很容易获得 100 个独特的结果,您可能想告诉我如何。据我所知,我正在使用 JOIN ON 语法?无论如何,感谢您花时间写出如此详尽的答案!
  • 您能否将postId 更改为PRIMARY KEY 并将id 降级为INDEX? (是的,即使 idAUTO_INCREMENT 也可以。)
  • InnoDB PRIMARY KEY 与数据“聚集”在一起;我有点需要这里的性能。
猜你喜欢
  • 2011-08-27
  • 2014-07-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-11-27
  • 2011-12-21
  • 1970-01-01
相关资源
最近更新 更多