MySQL MyISAM медленный запрос count() несмотря на индекс покрытия
Я дергаю себя за волосы, пытаясь понять, что я делаю неправильно. Таблица очень проста:
CREATE TABLE `icd_index` (
`icd` char(5) NOT NULL,
`core_id` int(11) NOT NULL,
`dx_order` tinyint(4) NOT NULL,
PRIMARY KEY (`icd`,`dx_order`,`core_id`),
KEY `core` (`core_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
Как вы можете видеть, я создал покрывающий индекс, который охватывает все три столбца таблицы, плюс дополнительный индекс на core_id
для потенциальных объединений. Это таблица ссылок "один ко многим" с каждым core_id
сопоставлены с одним или несколькими icd
, Таблица содержит 65 миллионов строк.
Итак, вот в чем проблема. Допустим, я хочу знать, сколько людей имеют код icd 25000. [Это диабет, если вам интересно]. Я пишу запрос, который выглядит так:
SELECT COUNT(core_id) FROM icd_index WHERE icd='25000'
Это займет более 60 секунд. Я думал, что, поскольку столбец icd был первым в покрытом индексе, подсчет будет быстрым.
Еще более запутанно, когда я однажды выполнил запрос, теперь он выполняется очень быстро. Я предположил, что это потому, что запрос был кэширован, но даже если я RESET QUERY CACHE
запрос теперь выполняется за доли секунды. Но, если я подожду достаточно долго, это снова замедлится - и я не могу понять, почему.
Я упускаю что-то очевидное. Нужен ли мне индекс на icd
в одиночестве? Это лучшая производительность, которую я могу получить с 65M строк? Почему выполнение запроса и сброс кеша влияют на скорость? Сохраняются ли результаты в индексе?
РЕДАКТИРОВАТЬ: я использую MySQL 5.6 (в случае, если это имеет значение).
Вот EXPLAIN
запроса:
id select_type table type possible_keys key key_len ref rows Extra
1 SIMPLE icd_index ref PRIMARY PRIMARY 15 const 910104 Using where; Using index
3 ответа
Вот что происходит.
The SELECT COUNT (...) icd_index where icd='25000'
будет использовать индекс, который является BTree отдельно от данных. Но он сканирует это следующим образом:
- Найдите первую запись, имеющую icd='25000'. Это почти мгновенно.
- Сканирование вперед до тех пор, пока не найдет изменение в icd. Это будет сканировать только в индексе, не касаясь данных. Согласно EXPLAIN, будет около 910 104 индексных записей для сканирования.
Теперь давайте посмотрим на BTree для этого индекса. Исходя из полей в индексе, каждая строка будет ровно 22 байта, плюс будут некоторые издержки (оценка 40%). Индексный блок MyISAM составляет 1 КБ (см. InnoDB 16 КБ). Я бы оценил 33 строки на блок. 910,104/33 говорит о том, что для выполнения COUNT необходимо прочитать 27K блоков. (Заметка COUNT(core_id)
нужно проверить core_id
для того, чтобы быть нулевым, COUNT(*)
не; это небольшая разница.) Чтение 27K блоков на обычном жестком диске занимает около 270 секунд. Вам повезло сделать это за 60 секунд.
Второй запуск обнаружил все эти блоки в key_buffer (предполагая, что key_buffer_size составляет не менее 27 МБ), поэтому ему не пришлось ждать диск. Следовательно, это было намного быстрее. (Это игнорирование кеша запросов, который у вас хватило на то, чтобы очистить или использовать SQL_NO_CACHE.)
5.6 оказывается неуместным (но спасибо за упоминание этого), поскольку этот процесс не изменился с 4.0 или ранее (за исключением того, что utf8 не существовало; подробнее об этом ниже).
Переключение на InnoDB помогло бы несколькими способами. ПЕРВИЧНЫЙ КЛЮЧ будет "кластеризован" с данными, а не хранится как отдельный BTree. Следовательно, как только данные или PK кэшируются, другой сразу становится доступным. Количество блоков будет больше похоже на 5 КБ, но они будут 16 КБ. Они могут загружаться быстрее, если кеш холодный.
Вы спрашиваете: "Нужен ли мне индекс только для icd? ". Что ж, это уменьшит размер BTIS MyISAM примерно до 21 байта на строку, поэтому размер BTree будет примерно 21/27, не так уж много улучшений (по крайней мере, для ситуация с холодным кэшем).
Другая мысль, если icd
всегда числовой и всегда числовой, чтобы использовать MEDIUMINT UNSIGNED
и придерживаться ZEROFILL
если он может иметь ведущие нули.
К сожалению, я не смог заметить набор символов. (Я исправил цифры выше, но позвольте мне уточнить.)
- CHAR (5) допускает 5 символов.
- Ascii занимает 1 байт на символ.
- utf8 занимает до 3 байтов на символы.
- Итак, CHAR(5) CHARACTER SET utf8 всегда занимает 15байтов.
Изменение столбца на CHAR(5) CHARACTER SET ascii
сократил бы его до 5 байтов.
Изменение его на MEDIUMINT UNSIGNED ZEROFILL уменьшит его до 3 байтов.
Сжатие данных ускорило бы ввод-вывод примерно на пропорциональную величину (после разрешения еще 6 байтов для двух других полей.
Спасибо всем выше за вашу помощь. Учитывая приведенный выше совет, я полностью перестроил базу данных примерно так:
- Я убедил администратора сервера увеличить мою оперативную память до 6G.
- Я переключил все таблицы на InnoDB с набором символов ASCII.
- Когда я переместил данные из MyISAM в InnoDB, я отсортировал все данные в порядке индекса покрытия, прежде чем вставить их в новую таблицу, поэтому новая таблица будет полностью отсортирована правильно. Не знаю, действительно ли это помогает, но казалось, что это не повредит.
- Я возился с настройками БД, в частности с размером InnoDB Buffer Pool, и увеличил его до 256M.
Святая Матерь Божья, сейчас действительно быстро. Простой подсчет запросов теперь выполняется менее чем за 2 секунды. Не уверен, что из вышеперечисленного было наиболее эффективным (но запрос был быстрым до увеличения размера пула буферов)
У меня было то же самое, что и с одним из моих запросов. Таблица MyISAM использовала сортировку файлов для выполнения простого оператора SELECT.
Я закончил тем, что переключился на InnoDB, и проблема исчезла. Я не знаю почему.