【问题标题】:MySQL 5.5 "select distinct" is really slowMySQL 5.5“选择不同”真的很慢
【发布时间】:2011-04-19 18:18:37
【问题描述】:

我的应用程序做了很多事情是:

select count(distinct id) from x;

id 是表 x 的主键。使用 MySQL 5.1(和 5.0),它看起来像这样:

mysql> explain SELECT count(distinct id) from x;
+----+-------------+----------+-------+---------------+-----------------+---------+------+---------+-------------+
| id | select_type | table    | type  | possible_keys | key             | key_len | ref  | rows    | Extra       |
+----+-------------+----------+-------+---------------+-----------------+---------+------+---------+-------------+
|  1 | SIMPLE      | x        | index | NULL          | ix_blahblahblah | 1       | NULL | 1234567 | Using index |
+----+-------------+----------+-------+---------------+-----------------+---------+------+---------+-------------+

在 InnoDB 上,这并不是很出色,但也不错。

这周我正在试用 MySQL 5.5.11,并且惊讶地发现同一个查询要慢很多倍。缓存启动后,大约需要 90 秒,而之前需要 5 秒。该计划现在看起来像这样:

mysql> explain select count(distinct id) from x;
+----+-------------+----------+-------+---------------+---------+---------+------+---------+-------------------------------------+
| id | select_type | table    | type  | possible_keys | key     | key_len | ref  | rows    | Extra                               |
+----+-------------+----------+-------+---------------+---------+---------+------+---------+-------------------------------------+
|  1 | SIMPLE      | x        | range | NULL          | PRIMARY | 4       | NULL | 1234567 | Using index for group-by (scanning) |
+----+-------------+----------+-------+---------------+---------+---------+------+---------+-------------------------------------+

让它再次快速运行的一种方法是使用select count(id) from x,这是安全的,因为id 是主键,但我正在经历一些抽象层(如NHibernate),这使得它变得不平凡任务。

我尝试了analyze table x,但没有任何明显的不同。

它看起来有点像this bug,但不清楚适用于哪些版本,或者发生了什么(一年内没有人碰过它,但它是“严重/高/高”)。

除了简单地更改我的查询之外,还有什么方法可以让 MySQL 在这方面变得更聪明?

更新:

根据要求,这里或多或少有一种重现它的方法。我编写了这个 SQL 脚本来生成 100 万行虚拟数据(运行需要 10 或 15 分钟):

delimiter $$
drop table if exists x;
create table x (
  id integer unsigned not null auto_increment,
  a integer,
  b varchar(100),
  c decimal(9,2),
  primary key (id),
  index ix_a (a),
  index ix_b (b),
  index ix_c (c)
) engine=innodb;
drop procedure if exists fill;
create procedure fill()
begin
  declare i int default 0;
  while i < 1000000 do
    insert into x (a,b,c) values (1,"one",1.0);
    set i = i+1;
  end while;
end$$
delimiter ;
call fill();

完成后,我观察到这种行为:

  • 5.1.48
    • select count(distinct id) from x
      • 解释是:键:ix_a,额外:使用索引
      • 运行时间不到 1.0 秒
    • select count(id) from x
      • 解释是:键:ix_a,额外:使用索引
      • 运行时间不到 0.5 秒
  • 5.5.11
    • select count(distinct id) from x
      • 解释是:键:PRIMARY,额外:使用索引进行分组
      • 运行时间超过 7.0 秒
    • select count(id) from x
      • 解释是:键:ix_a,额外:使用索引
      • 运行时间不到 0.5 秒

编辑:

如果我通过说修改 5.5 中的查询

select count(distinct id) from x force index (ix_a);

它运行得更快。索引 b 和 c 也有效(在不同程度上),甚至强制索引 PRIMARY 也有帮助。

【问题讨论】:

  • 两张x表是不是一模一样?请在两个数据库上运行SHOW CREATE TABLE x\G 并发布结果。我对 ix_blahblahblah 索引特别感兴趣,该索引用于 5.1 但不是 5.5。
  • Ike:我不想发布我正在处理的实际代码,但我会尝试提出类似的问题来解决这个问题。
  • Ike:添加了简化示例!该表比我的实际数据要小一些(行少列),但 5.5 的性能下降仍然接近 10 倍。
  • 5.1和5.5的配置一样吗?如果 5.5 对缓存和密钥存储等内容的限制更小,您会看到这样的结果。
  • 马克:他们是一样的。当我安装 5.5 时,我手动复制了 5.1 配置中似乎对查询性能很重要的所有部分。刚才我复制了其余的选项,很好地衡量,然后重新启动,我看到的性能和以前一样。

标签: mysql nhibernate primary-key innodb distinct


【解决方案1】:

我没有承诺这会更好,但作为一种可能的解决方法,您可以尝试:

SELECT COUNT(*)
    FROM (SELECT id
              FROM x
              GROUP BY id) t

【讨论】:

  • 有趣的想法,但没有运气:这也是在 90+ 秒的范围内。
【解决方案2】:

我不确定为什么您需要在唯一主键上使用 DISTINCT。看起来 MySQL 将 DISTINCT 关键字视为运算符并失去了使用索引的能力(就像对字段的任何操作一样。)其他 SQL 引擎有时也不能很好地优化表达式的搜索,所以它不是一个惊喜。


我注意到您在另一个答案中关于这是您的 ORM 工件的评论。你读过 Joel Spolsky 著名的 Leaky Abstractions 博客吗?我想你在那里。有时,您最终花在整理工具上的时间比花在使用该工具解决的问题上的时间还多。

【讨论】:

  • 我不确定你的意思是什么。我的 ORM 比我的 RDBMS 问题少。我显然可以轻松绕过抽象层的任何部分,除了 SQL(正如 Joel 所观察到的),但即使我可以在我的 SQL 语句中放入内联汇编语言,这也不是这里的 正确 答案。在一些明显的情况下,MySQL 5.5 比 MySQL 5.1(以及 5.0 和 Postgres 以及我尝试过的所有其他东西)要差得多,并且“使用旧版本”是一个非常简单的解决方案。你有更好的答案吗?
  • 我首先回答了你的问题,假设它在标题中有所描述;实际上,您的问题与 mysql 无关(答案是将“DISTINCT”排除在您的查询之外),真正的问题似乎是 NHibernate。但如果它有一个直接插入 SQL 的出口,为什么不直接使用它呢?最终,NHibernate 将需要适应 MySQL,反之亦然。在您的上下文中,SQL 插入相当于程序集插入。不知道,也许对您来说,最好还是使用旧版本。 YMMV。但这并不是说 MySQL 做的事情是非法的。
  • 我不知道你为什么说它“与 mysql 无关”。这绝对是关于 MySQL 的,要求我不能随意修改查询。显然,问题不仅仅是“这个表中有多少条记录?”,因为即使在 90 秒时,这也比我输入这个问题所花费的时间要少得多。我的系统在有时需要 DISTINCT 的情况下构建任意查询,而最容易识别这一点的地方是 MySQL,事实上,最高 5.1 的 MySQL 似乎正是这样做的。
  • 正如我(几乎)上面所说的,NHibernate 和 MySQL 之间存在不兼容(“阻抗不匹配”)。你可以责怪任何一个。您显然选择责怪 MySQL。尽管我几乎看不出他们应该如何预期可以创建的大量表达式,包括(如本例中的)冗余,并同等有效地对待它们。
  • 我在责怪 MySQL,因为在不平凡的情况下需要 DISTINCT(如果你真的想要,我可以发布一个大而复杂的案例,它与这个简单的案例有同样的问题)。我认为 MySQL 可以预料到这种特殊情况,因为在此之前的每个版本的 MySQL 都这样做了,并且 MySQL 5.5 的所有发行说明和变更日志都表明它已经提高了性能,没有提到任何回归.如果我升级了 NHibernate 并且性能下降了一个数量级,我会责怪 NHibernate。
【解决方案3】:

我不知道你是否意识到,但是使用 InnoDB 计算大型数据库上的行数很慢,即使没有 distinct 关键字。 InnoDB 不会缓存表元数据中的行数,MyISAM 会。

我建议你做两件事之一

1) 创建一个触发器,在插入时将不同的计数插入/更新到另一个表中。

2) 将另一个 MySQL 服务器从属服务器到您的数据库,但仅将从属服务器上的表类型更改为 MyISAM 并在那里执行您的查询(这可能是矫枉过正)。

【讨论】:

  • 我已经意识到,配置 #2 实际上就是我们现在所拥有的。 (#1 不是一个选项,因为它不仅仅是我们想要运行的一个查询;这只是一个示例。)这是一个很大的问题,维护起来是一场小噩梦,我们正试图摆脱它。对于直接向上计数的表行,查询缓存可以缓解大部分问题,但这并不是一个完美的解决方案。
【解决方案4】:

我可能看错了你的问题,但是如果id 是表x 的主键,那么以下两个查询在逻辑上是等价的:

select count(distinct id) from x;

select count(*) from x;

...不管优化器是否意识到这一点。 Distinct 通常意味着按顺序对索引进行排序或扫描,这比仅计算行数要慢得多。

【讨论】:

  • 是的,这是真的。我的 ORM 和其他工具生成前者。 MySQL 5.5(完全不同于 5.1 和 5.0)似乎不明白它与后者相同。我正在寻求一种方法让 MySQL 相信这一事实。
  • @ken,啊,我错过了关于 DAL 的部分。您不能配置 DAL 以了解 id 是唯一的,从而删除不同的吗?还是真实案例比你描述的更复杂?
  • 它已经知道这是一个PK。如果在这种情况下有一些神奇的标志可以让 NH 跳过 DISTINCT,我会全力以赴! (我现在正在下载它的源代码...... :-)
  • @ken,我从未使用过 nhibernate。我在您的问题中添加了 NH 标签,它可能会吸引合适的人。能否也分享一下生成查询的NH相关代码?
  • 我们在 NHibernate 周围有相当多的基础设施——公平地说,这可能是我们使用 NH 的方式,而不是 NH 本身。这就是为什么我首先要寻找 MySQL 解决方案。
【解决方案5】:

创造性地使用自动增量字段
请注意,您的 id 是自动递增的。
它会在每次插入后添加 +1。

但是它不会重复使用数字,因此如果您删除了一行,则需要对其进行跟踪。
我的想法是这样的。

 Count(rows) = Max(id) - number of deletions - starting(id) + 1

使用更新的场景

使用每个表的总数创建一个单独的表。

table counts 
  id integer autoincrement primary key
  tablename varchar(45)  /*not needed if you only need to count 1 table*/
  start_id integer default maxint
  delete_count 

确保在第一个 delete(!) 之前将 start_id 提取到表中并执行

INSERT INTO counts (tablename, start_id, delete_count)
  SELECT 'x', MIN(x.id), 0
  FROM x;

现在创建一个after delete 触发器。

DELIMITER $$

CREATE TRIGGER ad_x_each AFTER DELETE ON x FOR EACH ROW
BEGIN
  UPDATE counts SET delete_count = delete_count + 1 WHERE tablename = 'x';
END $$

DELIMITER ;

IF you want to have the count, you do

SELECT max(x.id) - c.start_id + 1 - c.delete_count as number_of_rows
FROM x 
INNER JOIN counts c ON (c.tablename = 'x') 

这将立即为您提供计数,需要一个触发器来计数每个插入。

插入场景

如果您有很多删除,您可以通过在触发器中执行 insert 而不是 update 并选择来加快处理速度

TABLE count_x  /*1 counting table per table to keep track of*/
  id integer autoincrement primary key /*make sure this field starts at 1*/
  start_id integer default maxint  /*do not put an index on this field!*/

将起始 id 播种到计数表中。

INSERT INTO counts (start_id) SELECT MIN(x.id) FROM x;

现在创建一个after delete 触发器。

DELIMITER $$

CREATE TRIGGER ad_x_each AFTER DELETE ON x FOR EACH ROW
BEGIN
  INSERT INTO count_x (start_id) VALUES (default);     
END $$

DELIMITER ;

SELECT max(x.id) - min(c.start_id) + 1 - max(c.id) as number of rows
FROM x
JOIN count_x as c  ON (c.id > 0)

您必须测试哪种方法最适合您。

请注意,在插入场景中您不需要 delete_count,因为您使用自动递增 id 来跟踪删除次数。

【讨论】:

  • 我认为基于触发器的机制可以工作,但它比这更复杂。 MyISAM 不重用数字,但 InnoDB 在服务器启动时执行 SELECT MAX,因此如果服务器恰好在 DELETE 和 INSERT 之间重新启动,它确实会重用 autoinc ID。
【解决方案6】:
select count(*)
from ( select distinct(id) from x)

【讨论】:

    猜你喜欢
    • 2022-01-18
    • 1970-01-01
    • 1970-01-01
    • 2018-04-25
    • 1970-01-01
    • 2015-08-26
    • 1970-01-01
    • 2016-08-08
    • 1970-01-01
    相关资源
    最近更新 更多