Проблемы MySQL оптимизатора

в 12:14, , рубрики: mysql, mysql performance, метки:

Что я успел понять про MySQL за несколько лет его разработки:

  • развивать не ломая обратной совместимости его нельзя
  • MySQL со сломанной обратной совместимостью никому не нужен.

Я опишу две серьёзные проблемы ДНК MySQL, с которыми косвенно сталкивается любой пользователь MySQL 5.1 и 5.5 (насчёт 5.6 не проверял, но не думаю, что этот момент поменялся).

Как вообще MySQL работает?

Есть сам MySQL — это

  • парсер
  • оптимизатор запросов
  • репликация
  • системный каталог

Есть и другие вещи, но основные — именно эти.

Есть Storage Engine: плагин, реализующий следующую функциональность:

  • транзакции
  • хранение таблиц на диске
  • индексы
  • статистика

Список не исчерпывающий, да и не очень корретно поделённый на обязанности, но для нашего рассмотрения удовлетворительный.

API для Storage Engine — это такая святая корова, которую нельзя трогать.
Я хочу привести один прекрасный, замечательный пример того, как этот Storage Engine API «прекрасно» сделан и какие с ним возникают проблемы.

Конкретно данные проблемы я изучил пока боролся с вот этим багом: bugs.mysql.com/bug.php?id=63320
Если кто чувствует в себе силы и желание разобраться как с багом, так с исходным кодом — могу пожелать удачи.
Кто ленивый и доверяет моему рассказу, могут просто читать дальше.

Итак, какую информацию оптимизатору может/умеет сообщать Storage Engine?

  • количество записей в таблице (пусть это будет row_count)
  • целочисленное значение rows_per_key (отношение количество_записей_всего к количество_уникальных_записей).

Обратите внимание — количество уникальных записей узнать напрямую нельзу.
MySQL считает его как количество_записей в таблице / rows_per_key

Запишем это в виде кода (как это в результате вычисляется):

int unique_row_count_mysql = row_count / rows_per_key;
int rows_per_key = row_count / unique_row_count_engine;
int unique_row_count_mysql = row_count / ((int) (row_count / unique_row_count_engine));

Поясняю:

a2 = (int)b / ((int) (b / a));

Просто из-за отсутствия данной функции мы теряем в точности округляя число два раза подряд.
Поясню пример. Пусть есть таблица c миллионом записей.
Рассмотрим различное количество уникальных ключей.

Всего записей Уникальных записей rows_per_key Уникальных записей (по мнению оптимизатора)
1000000 100000 10 100000
1000000 200000 5 200000
1000000 300000 3 333333
1000000 400000 2 500000
1000000 500000 2 500000
1000000 600000 1 1000000
1000000 700000 1 1000000
1000000 800000 1 1000000
1000000 900000 1 1000000
1000000 1000000 1 1000000

Нравится?
НИКАКИХ причин терять точность КРОМЕ Storage Engine API — нету.
Трогать API нельзя.
Данная проблема применима для любого Storage Engine.

Это полдела. Едем дальше.
Для выбора эффективного плана оптимизатору недостаточно знать количество уникальных записей и количество записей всего.
Часто* ему требуется знать количество NULL'ов, количество не NULL'ов, в случае составных ключей — различные сочетания.
Часто — это в следующих случаях:

  • inner join («просто» join)
  • outer join (left/right/full)
  • subquery (in/not in/all/any/exists/not exists)

Имея статистику по null'ам, можно строить различные эффективные оптимизации.
Про это чуть позже, сначала опишем проблему.

В Storage Engine API _нет_ возможности различать null'ы от остальных записей.
Почти нет.
InnoDB и MyISAM реализуют две глобальных переменных: innodb_stats_method и myisam_stats_method.
Это глобальные переменные имеют следующие значения:

  • nulls ignore
  • nulls equal
  • nulls not equal.

Когда я в первый раз это увидел в исходном коде, я не поверил собственным глазам.
Эти переменные влияют на сбор статистики для ВСЕХ таблиц и запросов имеющих null'ы в ключах join'ов и подзапросах.

Чем это плохо?
Вот есть простой запрос:

select * from a join b on a.id=b.id

Если стоит nulls ignore — всё отлично
Если стоит nulls equal — все отлично
Если стоит nulls not equal — всё очень плохо, оптимизатор выберёт пессимистичный план — предполагая, что записей на выходе джойна будет много.

Другой запрос

select * from a left join b on a.id=b.id

Если стоит nulls not equal — всё отлично
Если стоит nulls equal или nulls ignore — все очень плохо оптимизатор выберёт оптимистичный план — предполагая, что записей на выходе джойна будет мало.

С подзапросами ситуация бывает и так, и эдак.
Собственно говоря, баг, на который я ссылался — это про особенности вычисления числа записей исходя из настройки innodb_stats_method.
В архитектуре изначально не было предусмотрено разделение null'ов от всего остального, и теперь разработчики мучаются, придумывая более хитрые костыли и эвристики, чтобы промахиваться поменьше.

С моей точки зрения — это мёртвому припарки.
Оптимизатор MySQL, построенный поверх AST (abstract syntax tree) — мёртвый, его невозможно развивать и улучшать. Это огромная дыра, которая пожирает кучу времени и не даёт возможности улучшить производительность запросов.

Сами по себе Storage Engine быстрые, но вот пользоваться ими оптимизатор не умеет. Нет API, нет возможности, нет нормальной статистики.

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

Автор: zabivator

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js