MySQL 5.5“select distinct”真的很慢

时间:2011-04-19 18:18:37

标签: mysql nhibernate primary-key innodb distinct

我的应用程序做了相当多的事情之一是:

select count(distinct id) from x;

idx的主键。使用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,并且惊讶地发现相同的查询速度慢很多倍。启动缓存后,与之前的5秒相比,大约需要90秒。该计划现在看起来像这样:

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();

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

  • 48年5月1日
    • select count(distinct id) from x
      • EXPLAIN是:key:ix_a,Extra:使用索引
      • 运行时间不足1.0秒
    • select count(id) from x
      • EXPLAIN是:key:ix_a,Extra:使用索引
      • 运行
      • 不到0.5秒
  • 5.5.11
    • select count(distinct id) from x
      • EXPLAIN是:key:PRIMARY,Extra:使用index by group-by
      • 需要超过7.0秒才能运行
    • select count(id) from x
      • EXPLAIN是:key:ix_a,Extra:使用索引
      • 运行
      • 不到0.5秒

修改

如果我通过说

修改5.5中的查询
select count(distinct id) from x force index (ix_a);

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

6 个答案:

答案 0 :(得分:1)

我没有做出任何更好的承诺,但作为可能的解决办法,你可以尝试:

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

答案 1 :(得分:1)

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


我在另一个答案中注意到你的评论,这是你的ORM的神器。你有没有读过Joel Spolsky着名的Leaky Abstractions博客?我想你在那儿。有时你最终会花费更多的时间来理清工具而不是花在你使用工具解决的问题上。

答案 2 :(得分:1)

我不知道你是否已经实现了,但即使没有distinct关键字,使用InnoDB计算大型数据库上的行也很慢。 InnoDB不会将行计数缓存在表元数据中,MyISAM会这样做。

我建议你做两件事之一

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

2)从另一个MySQL服务器到你的数据库,但是只改变奴隶上的表类型,改为MyISAM并在那里执行你的查询(这可能是一种过度杀伤)。

答案 3 :(得分:0)

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

select count(distinct id) from x;

select count(*) from x;

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

答案 4 :(得分:0)

自动增量字段的广告使用
请注意,您的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 

确保在第一次删除(!)之前将starting_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来跟踪删除的数量。

答案 5 :(得分:0)

select count(*)
from ( select distinct(id) from x)