MySQL 5.5 "выберите отличное" действительно медленно - PullRequest
5 голосов
/ 19 апреля 2011

Мое приложение делает изрядную сумму:

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, но это не имело заметного значения.

Это похоже на этот баг , хотя неясно, к каким версиям это относится, или что происходит (никто не трогал его в течение года, хотя это "серьезно / высоко / высоко").

Есть ли способ, кроме простого изменения моего запроса, заставить MySQL быть более умным в этом вопросе?

UPDATE:

В соответствии с просьбой, вот способ воспроизвести его, более или менее. Я написал этот сценарий SQL для генерации 1 миллиона строк фиктивных данных (на их выполнение требуется 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
      • EXPLAIN это: ключ: ix_a, Extra: использование индекса
      • требуется менее 1,0 секунды для запуска
    • select count(id) from x
      • EXPLAIN это: ключ: ix_a, Extra: использование индекса
      • для запуска требуется менее 0,5 с
  • 5.5.11
    • select count(distinct id) from x
      • ОБЪЯСНЕНИЕ: ключ: ПЕРВИЧНЫЙ, Дополнительно: использование индекса для группировки по
      • требуется более 7,0 секунд для запуска
    • select count(id) from x
      • EXPLAIN: key: ix_a, Extra: использование индекса
      • требуется менее 0,5 секунды для запуска

EDIT:

Если я изменю запрос в 5.5, сказав

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

работает намного быстрее. Индексы b и c также работают (в разной степени), и даже форсирование индекса PRIMARY помогает.

Ответы [ 6 ]

1 голос
/ 09 июня 2011

Я не знаю, поняли ли вы, но подсчет строк в большой базе данных с InnoDB идет медленно, даже без отдельного ключевого слова.InnoDB не кэширует количество строк в метаданных таблицы, MyISAM делает.

Я бы посоветовал вам сделать одну из двух вещей

1) создать триггер, который вставляет / обновляет отдельные значения в другую таблицу навставка.

2) подчините другой сервер MySQL вашей базе данных, но измените тип таблицы только на ведомом устройстве на MyISAM и выполните ваш запрос там (это, вероятно, излишне).

1 голос
/ 20 апреля 2011

Я не уверен, зачем вам нужен DISTINCT для уникального первичного ключа. Похоже, что MySQL рассматривает ключевое слово DISTINCT как оператор и теряет возможность использовать индекс (как и любая операция с полем). Другие механизмы SQL также иногда не очень хорошо оптимизируют поиск по выражениям, поэтому это не так. сюрприз.


Я отмечаю ваш комментарий в другом ответе о том, что это артефакт вашего ORM. Вы когда-нибудь читали знаменитый блог Leaky Abstractions Джоэла Спольски? Я думаю, что ты здесь. Иногда вы тратите больше времени на исправление инструмента, чем тратите на решение проблемы, которую используете инструмент для решения.

1 голос
/ 19 апреля 2011

Я не даю никаких обещаний, что это будет лучше, но в качестве возможного обходного пути вы можете попробовать:

SELECT COUNT(*)
    FROM (SELECT id
              FROM x
              GROUP BY id) t
0 голосов
/ 11 января 2012
select count(*)
from ( select distinct(id) from x)
0 голосов
/ 09 июня 2011

Творческое использование автоинкрементных полей
Обратите внимание, что ваш идентификатор - автоинкремент.
Это добавит +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 

Убедитесь, что вы извлекаете 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!*/

Поместить начальный идентификатор в таблицу счетчиков.

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, потому что вы используете идентификатор автоинкремента для отслеживания количества удалений.

0 голосов
/ 19 апреля 2011

Возможно, я неправильно прочитал ваш вопрос, но если id является первичным ключом таблицы x, то следующие два запроса логически эквивалентны:

select count(distinct id) from x;

select count(*) from x;

... независимо от того, является лиоптимизатор понимает это.Различный обычно подразумевает сортировку или сканирование индекса по порядку, что значительно медленнее, чем просто подсчет строк.

...