MySQL JOIN не фильтрует предложение WHERE с операторами ‹ › после перехода с MySQL 5.6 на 5.7

Мы обновляем наши системы БД до версии MySQL 5.7, начиная с версии MySQL 5.6, и после обновления несколько запросов стали выполняться очень медленно.

После некоторого исследования мы сузили его до нескольких запросов JOIN, которые внезапно больше не слушают предложение «WHERE» при использовании оператора «больше чем» > или «меньше чем» ‹. При использовании оператора '=' он работает так, как ожидалось. При запросе большой таблицы это вызывало постоянную 100% загрузку ЦП.

Запросы были упрощены, чтобы объяснить проблему под рукой; при использовании объяснения мы получаем следующие результаты:

explain 
        select * from TableA as A
                left join
                (
                    select
                        DATE_FORMAT(created_at,'%H:%i:00') as `time`
                    FROM
                        TableB
                    WHERE
                        created_at < DATE_ADD(CURDATE(), INTERVAL -3 HOUR) 
                )
                as V ON V.time = A.time

Вывод

id  select_type table   partitions  type    possible_keys   key key_len ref rows    filtered    Extra
1   SIMPLE  A   NULL    ALL NULL    NULL    NULL    NULL    10080   100.00  NULL
1   SIMPLE  TableB  NULL    index   created_at  created_at  4   NULL    488389  100.00  Using where; Using index; Using join buffer (Block Nested Loop)

Как видите, он запрашивает/сопоставляет 488389 строк и не использует предложение where, поскольку это общее количество записей в этой таблице.

А теперь запустим тот же запрос, но с помощью команды LIMIT 99999999 или оператора '=':

explain 
        select * from TableA as A
                left join
                (
                    select
                        DATE_FORMAT(created_at,'%H:%i:00') as `time`
                    FROM
                        TableB
                    WHERE
                        created_at < DATE_ADD(CURDATE(), INTERVAL -3 HOUR) LIMIT 999999999
                )
                as V ON V.time = A.time

Вывод

id  select_type table   partitions  type    possible_keys   key key_len ref rows    filtered    Extra
1   PRIMARY A   NULL    ALL NULL    NULL    NULL    NULL    10080   100.00  NULL
1   PRIMARY <derived2>  NULL    ALL NULL    NULL    NULL    NULL    244194  100.00  Using where; Using join buffer (Block Nested Loop)
2   DERIVED TableB  NULL    range   created_at  created_at  4   NULL    244194  100.00  Using where; Using index

Вы можете видеть, что внезапно совпадают только строки «244194», которые являются частью таблицы, или с оператором «=»:

id  select_type table   partitions  type    possible_keys   key key_len ref rows    filtered    Extra
1   SIMPLE  A   NULL    ALL NULL    NULL    NULL    NULL    10080   100.00  NULL
1   SIMPLE  TableB  NULL    ref created_at  created_at  4   const   1   100.00  Using where; Using index

Всего 1 ряд, как и ожидалось.

Итак, вопрос сейчас заключается в том, что мы запрашивали неверным образом и только сейчас выяснили это при обновлении, или что-то изменилось с MySQL 5.6? Кажется странным, что оператор = работает, но и > почему-то игнорируются, разве что при использовании LIMIT?..

Мы искали вокруг и не смогли найти причину этой проблемы, и мы не хотели бы использовать решение limit 9999999 в нашем коде по очевидным причинам.

Примечание. При выполнении только запроса внутри соединения он также работает должным образом.

Примечание Мы также провели тот же тест на MariaDB 10.1 с той же проблемой.


person Nick    schedule 27.05.2016    source источник
comment
Вы обновили индексы? Он утверждает, что использует индекс,   -  person Kickstart    schedule 27.05.2016
comment
Да, индексы присутствуют и обновляются в рассматриваемых полях   -  person Nick    schedule 30.05.2016


Ответы (3)


В MySQL 5.7 производные таблицы (подзапросы в предложении FROM) будут объединены во внешний запрос, если это возможно. Обычно это является преимуществом, поскольку позволяет избежать сохранения результата подзапроса во временной таблице. Однако для вашего запроса MySQL 5.6 создаст индекс для этой временной таблицы, который можно будет использовать для выполнения соединения.

Проблема с объединенным запросом заключается в том, что индекс в TableB.created_at нельзя использовать, когда столбец является параметром функции. Если вы можете изменить запрос так, чтобы преобразование выполнялось в столбце с левой стороны соединения, для доступа к таблице с правой стороны можно использовать индекс. Что-то типа:

   select * from TableA as A
            left join
            (
                select created_at as time
                FROM TableB
                WHERE created_at < DATE_ADD(CURDATE(), INTERVAL -3 HOUR) 
            )
            as V ON V.time = func(A.time)

В качестве альтернативы, если вы можете использовать внутреннее соединение вместо левого соединения, MySQL может изменить порядок соединения, чтобы для соединения можно было использовать индекс в tableA.time.

Если подзапрос использует LIMIT, его нельзя объединить. Следовательно, используя LIMIT, вы получите тот же план запроса, что и в MySQL 5.6.

person oysteing    schedule 02.10.2016
comment
Спасибо! Это, наконец, привело к решению нашей проблемы :-) - person Nick; 14.10.2016

Вывод explain row — это всего лишь предположение о том, сколько строк будет затронуто. Он основан на статистических данных, которые были сброшены вместе с вашим обновлением. И если бы мне пришлось угадывать, сколько строк из всех ваших существующих строк старше вчерашних 9 вечера, я бы тоже предположил, что это ближе ко «всем строкам», чем к «только некоторым строкам». Причина, по которой «limit 99999999» отображает другой счетчик строк, та же самая: он просто предполагает, что ограничение будет иметь эффект; в этом случае mysql предполагает, что это будет ровно половина строк (что было бы, если это правда, странное совпадение), и, конечно, на самом деле он не смотрит на предельное значение, поскольку 999999999 ничего не ограничивает, когда у вас всего 500 тыс. строк; и даже «1» в случае «=» - это просто предположение (и чаще может быть 0, чем 1, а иногда и больше).

Эта оценка поможет выбрать правильный план выполнения, и ошибиться в этом предположении — просто проблема, если он выберет неправильный; ваш план выполнения выглядит хорошо, и в противном случае не так много вариантов сделать это. Он делает именно так, как и ожидалось: сканирует индекс на все даты, используя индекс на created_at. Поскольку вы выполняете левое соединение, вы не можете пропустить значения из tableA, даже если вы начнете с внутреннего запроса, поэтому альтернативного плана выполнения действительно нет. (На самом деле оптимизатор был изменен в 5.7., но здесь это не имеет никакого эффекта.)

Если это ваш фактический запрос, нет реальной причины, по которой он должен быть медленнее, чем раньше (только в отношении этого запроса; конечно, есть много общих параметров производительности, которые могут иметь косвенный эффект, например, стратегии кэширования, размеры буферов и т. д. ., но со стандартными опциями здесь это не должно влиять).

Если нет, и вы, например. на самом деле использовать дополнительные столбцы из TableB в подзапросе (часто трудно угадать, какие, возможно, важные вещи были «упрощены» в вопросах), и, следовательно, требуется доступ к фактической таблице, это может зависеть от того, как структурированы ваши данные (или лучше: в каком порядке вы это добавили). И вы можете попробовать Optimize table TableB, чтобы сделать вашу таблицу и индексы свежими и новыми, это не повредит (но заблокирует вашу таблицу на некоторое время).

С mysql 5.7. теперь вы можете добавлять сгенерированные столбцы, поэтому, возможно, стоит попробовать сгенерировать очищенный столбец time as DATE_FORMAT(created_at,'%H:%i:00'), чтобы вам больше не приходилось его вычислять. И, возможно, добавьте его в свой индекс, чтобы вам больше не нужно было его сортировать, чтобы улучшить block nested join, но это может зависеть от вашего фактического запроса и от того, как часто вы его используете (индексы со спамом увеличивают накладные расходы и занимают место).

person Solarflare    schedule 27.05.2016
comment
Спасибо за информацию, но если я запускаю запросы, упомянутые выше, я вижу те же результаты, 230 мс с предложением limit 99999 и около 3-4 минут без него. В то время как те же запросы завершаются примерно через ~ 230 мс в MySQL 5.6. Пробовал оптимизировать обе таблицы, увы, безрезультатно. Мы действительно могли бы заглянуть в сгенерированные столбцы, только без индексов, так как эти запросы выполняются каждую минуту. - person Nick; 30.05.2016
comment
@Ник Ну, так не должно быть. С оценкой скорости жесткого диска ~ 30 МБ / с, 4 байта на строку (в индексе, и mysql говорит, что использует индекс), это будет означать, что вы можете прочитать 500 000 значений 2700 раз за 180 секунд (не считая того, что они будут кэшироваться после первое чтение), значит, происходит что-то еще. Если вы довольны тем, что он работает с limit, это нормально, в противном случае попробуйте: set @@profiling = 1; do your query; show profile; do your 2nd query; show profile; show profiles; set @@profiling = 1;, чтобы увидеть, на что тратится время. И сначала запустите свой запрос limit/запустите его дважды, чтобы он не был одобрен кешем. - person Solarflare; 30.05.2016

Используйте JOIN вместо LEFT JOIN, если только вам не нужно, чтобы "правильная" таблица была необязательной.

Избегайте JOIN ( SELECT ... ). Хотя в версиях 5.6 и 5.7 были добавлены некоторые возможности для его обработки, обычно лучше превратить подзапрос в более простой JOIN.

Ваше выражение времени ведет к 9 вечера вчера; вы имели в виду "3 часа назад" вместо этого?

Посмотрите, дает ли это желаемые результаты и работает быстрее:

select  A.*, DATE_FORMAT(B.created_at,'%H:%i:00') as `time`
    from  TableA as A
    JOIN  TableB as B  ON B.time = A.time
    WHERE  B.created_at < NOW() - INTERVAL 3 HOUR   -- (assuming "3 hours ago")

Что касается 5.6 против 5.7... 5.7 имеет новый, "лучший" оптимизатор, основанный на "стоимостной модели". Однако ваш конкретный запрос делает практически невозможным для оптимизатора получение хороших затрат. Я предполагаю, что 5,6 произошло на лучшем EXPLAIN, а 5,7 произошло на худшем. Упрощая запрос, я думаю, что у обоих оптимизаторов будет больше шансов выполнить запрос быстрее.

Вам нужны эти индексы:

B:  INDEX(time, created_at) -- in that order
A:  INDEX(time)
person Rick James    schedule 04.06.2016